@needle-tools/engine 4.7.0-next.22483b4 → 4.7.0-next.4bd7749
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/{needle-engine.bundle-RgZNbAgp.js → needle-engine.bundle-BGHUA0S1.js} +21 -19
- package/dist/{needle-engine.bundle-CP5AJL1A.umd.cjs → needle-engine.bundle-DQ8S9diA.umd.cjs} +9 -7
- package/dist/{needle-engine.bundle-BnGp0ASs.min.js → needle-engine.bundle-kBkeRoLs.min.js} +9 -7
- package/dist/needle-engine.js +2 -2
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/engine_context.js +1 -1
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_three_utils.js +4 -0
- package/lib/engine/engine_three_utils.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.attributes.d.ts +1 -1
- package/lib/engine-components/Camera.js +1 -1
- package/lib/engine-components/Camera.js.map +1 -1
- package/lib/engine-components/CameraUtils.js +5 -3
- package/lib/engine-components/CameraUtils.js.map +1 -1
- package/lib/engine-components/Skybox.js +4 -4
- package/lib/engine-components/Skybox.js.map +1 -1
- package/lib/engine-components/postprocessing/Effects/Antialiasing.js +1 -1
- package/lib/engine-components/postprocessing/Effects/Antialiasing.js.map +1 -1
- package/lib/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.js +1 -1
- package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
- package/lib/engine-components/postprocessing/Volume.js +1 -1
- package/lib/engine-components/postprocessing/Volume.js.map +1 -1
- package/package.json +1 -1
- package/plugins/common/logger.js +321 -0
- package/plugins/types/userconfig.d.ts +4 -2
- package/plugins/vite/alias.js +7 -2
- package/plugins/vite/imports-logger.js +1 -1
- package/plugins/vite/index.js +2 -0
- package/plugins/vite/logger.client.js +262 -0
- package/plugins/vite/logger.js +101 -0
- package/plugins/vite/materialx.js +6 -4
- package/src/engine/engine_context.ts +1 -1
- package/src/engine/engine_three_utils.ts +3 -0
- package/src/engine/webcomponents/needle-engine.attributes.ts +1 -1
- package/src/engine-components/Camera.ts +1 -1
- package/src/engine-components/CameraUtils.ts +5 -3
- package/src/engine-components/Skybox.ts +5 -5
- package/src/engine-components/postprocessing/Effects/Antialiasing.ts +1 -1
- package/src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts +1 -1
- package/src/engine-components/postprocessing/PostProcessingHandler.ts +1 -2
- package/src/engine-components/postprocessing/Volume.ts +1 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { createWriteStream, existsSync, mkdirSync, readdirSync, rmSync, statSync, write } from "fs";
|
|
2
|
+
|
|
3
|
+
const filename_timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
4
|
+
const debug = false;
|
|
5
|
+
|
|
6
|
+
// #region public api
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {"server" | "client" | "client-http"} ProcessType
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
let originalConsoleLog = console.log;
|
|
13
|
+
let originalConsoleError = console.error;
|
|
14
|
+
let originalConsoleWarn = console.warn;
|
|
15
|
+
let originalConsoleInfo = console.info;
|
|
16
|
+
let originalConsoleDebug = console.debug;
|
|
17
|
+
let didPatch = false;
|
|
18
|
+
let unpatchFunction = null;
|
|
19
|
+
|
|
20
|
+
export function patchConsoleLogs() {
|
|
21
|
+
if (didPatch) return unpatchFunction;
|
|
22
|
+
didPatch = true;
|
|
23
|
+
|
|
24
|
+
console.log = (...args) => {
|
|
25
|
+
originalConsoleLog(...args);
|
|
26
|
+
captureLogMessage("server", 'log', args, null);
|
|
27
|
+
};
|
|
28
|
+
console.error = (...args) => {
|
|
29
|
+
originalConsoleError(...args);
|
|
30
|
+
captureLogMessage("server", 'error', args, null);
|
|
31
|
+
};
|
|
32
|
+
console.warn = (...args) => {
|
|
33
|
+
originalConsoleWarn(...args);
|
|
34
|
+
captureLogMessage("server", 'warn', args, null);
|
|
35
|
+
};
|
|
36
|
+
console.info = (...args) => {
|
|
37
|
+
originalConsoleInfo(...args);
|
|
38
|
+
captureLogMessage("server", 'info', args, null);
|
|
39
|
+
};
|
|
40
|
+
console.debug = (...args) => {
|
|
41
|
+
originalConsoleDebug(...args);
|
|
42
|
+
captureLogMessage("server", 'debug', args, null);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Restore original console methods
|
|
46
|
+
unpatchFunction = () => {
|
|
47
|
+
didPatch = false;
|
|
48
|
+
console.log = originalConsoleLog;
|
|
49
|
+
console.error = originalConsoleError;
|
|
50
|
+
console.warn = originalConsoleWarn;
|
|
51
|
+
console.info = originalConsoleInfo;
|
|
52
|
+
console.debug = originalConsoleDebug;
|
|
53
|
+
}
|
|
54
|
+
return unpatchFunction;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
let isCapturing = false;
|
|
59
|
+
/** @type {Set<string>} */
|
|
60
|
+
const isCapturingLogMessage = new Set();
|
|
61
|
+
|
|
62
|
+
/** @type {Array<{ process: ProcessType, key: string, log:any, timestamp:number, connectionId: string | null }>} */
|
|
63
|
+
const queue = new Array();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {ProcessType} process
|
|
67
|
+
* @param {string} key
|
|
68
|
+
* @param {any} log
|
|
69
|
+
* @param {string | null} connectionId - Optional connection ID for client logs.
|
|
70
|
+
* @param {number} [time] - Optional timestamp, defaults to current time.
|
|
71
|
+
*/
|
|
72
|
+
export function captureLogMessage(process, key, log, connectionId, time = Date.now()) {
|
|
73
|
+
|
|
74
|
+
if (isCapturingLogMessage.has(log)) {
|
|
75
|
+
return; // prevent circular logs
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isCapturing) {
|
|
79
|
+
queue.push({ process, key, log, timestamp: Date.now(), connectionId });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
isCapturing = true;
|
|
83
|
+
isCapturingLogMessage.add(log);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
let str = stringifyLog(log);
|
|
87
|
+
if (str.trim().length > 0) {
|
|
88
|
+
// if(process === "server") str = stripAnsiColors(str);
|
|
89
|
+
const prefix = `${getTimestamp(time, true)}, ${process}${connectionId ? (`[${connectionId}]`) : ""}.${key}: `;
|
|
90
|
+
const separator = "";
|
|
91
|
+
const finalLog = indent(`${prefix}${separator}${removeEmptyLinesAtStart(str)}`, prefix.length, separator)
|
|
92
|
+
writeToFile(process, finalLog, connectionId);
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
isCapturing = false;
|
|
96
|
+
isCapturingLogMessage.delete(log);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let queued = queue.pop();
|
|
100
|
+
if (queued) {
|
|
101
|
+
captureLogMessage(queued.process, queued.key, queued.log, queued.connectionId, queued.timestamp);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
// #region stringify log
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Stringifies a log message, handling circular references and formatting.
|
|
111
|
+
* @param {any} log
|
|
112
|
+
* @param {Set<any>} [seen]
|
|
113
|
+
*/
|
|
114
|
+
function stringifyLog(log, seen = new Set(), depth = 0) {
|
|
115
|
+
|
|
116
|
+
const isServer = typeof window === "undefined";
|
|
117
|
+
const stringify_limits = {
|
|
118
|
+
string: isServer ? 100_000 : 2000,
|
|
119
|
+
object_keys: isServer ? 100 : 200,
|
|
120
|
+
array_items: isServer ? 2_000 : 100,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof log === "string") {
|
|
124
|
+
if (log.length > stringify_limits.string) log = `${log.slice(0, stringify_limits.string)}... <truncated ${log.length - stringify_limits.string} characters>`;
|
|
125
|
+
return log;
|
|
126
|
+
}
|
|
127
|
+
if (typeof log === "number" || typeof log === "boolean") {
|
|
128
|
+
return String(log);
|
|
129
|
+
}
|
|
130
|
+
if (log === null) {
|
|
131
|
+
return "null";
|
|
132
|
+
}
|
|
133
|
+
if (log === undefined) {
|
|
134
|
+
return "undefined";
|
|
135
|
+
}
|
|
136
|
+
if (typeof log === "function") {
|
|
137
|
+
return "<function>";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (seen.has(log)) return "<circular>";
|
|
141
|
+
|
|
142
|
+
if (Array.isArray(log)) {
|
|
143
|
+
seen.add(log);
|
|
144
|
+
return stringifyArray(log);
|
|
145
|
+
}
|
|
146
|
+
if (typeof log === "object") {
|
|
147
|
+
seen.add(log);
|
|
148
|
+
// const str = JSON.stringify(log, (key, value) => {
|
|
149
|
+
// if (typeof value === "function") return "<function>";
|
|
150
|
+
// if (typeof value === "string") return stringifyLog(value, seen, depth + 1);
|
|
151
|
+
// if (typeof value === "object") {
|
|
152
|
+
// if (seen.has(value)) return "<circular>";
|
|
153
|
+
// seen.add(value);
|
|
154
|
+
// }
|
|
155
|
+
// return value;
|
|
156
|
+
// });
|
|
157
|
+
// return str;
|
|
158
|
+
const keys = Object.keys(log);
|
|
159
|
+
let res = "{";
|
|
160
|
+
for (let i = 0; i < keys.length; i++) {
|
|
161
|
+
const key = keys[i];
|
|
162
|
+
let value = log[key];
|
|
163
|
+
|
|
164
|
+
if (typeof value === "number") {
|
|
165
|
+
// clamp precision for numbers
|
|
166
|
+
value = Number(value.toFixed(6));
|
|
167
|
+
}
|
|
168
|
+
let str = stringifyLog(value, seen, depth + 1);
|
|
169
|
+
if (typeof value === "object") {
|
|
170
|
+
if (Array.isArray(value)) {
|
|
171
|
+
str = `[${str}]`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (typeof value === "string") {
|
|
175
|
+
str = `"${str}"`;
|
|
176
|
+
}
|
|
177
|
+
if (i > 0) res += ", ";
|
|
178
|
+
res += `"${key}":${str}`;
|
|
179
|
+
}
|
|
180
|
+
res += "}";
|
|
181
|
+
return res;
|
|
182
|
+
// let entries = Object.entries(log).map(([key, value], index) => {
|
|
183
|
+
// if (index > stringify_limits.object_keys) return `"${key}": <truncated>`;
|
|
184
|
+
// return `"${key}": ${stringifyLog(value, seen, depth + 1)}`;
|
|
185
|
+
// });
|
|
186
|
+
// return `{ ${entries.join(", ")} }`;
|
|
187
|
+
}
|
|
188
|
+
return String(log);
|
|
189
|
+
|
|
190
|
+
function stringifyArray(arr) {
|
|
191
|
+
let res = "";
|
|
192
|
+
for (let i = 0; i < arr.length; i++) {
|
|
193
|
+
let entry = arr[i];
|
|
194
|
+
if (res && i > 0) res += ", ";
|
|
195
|
+
if (i > stringify_limits.array_items) {
|
|
196
|
+
res += "<truncated " + (arr.length - i) + ">";
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
res += stringifyLog(entry, seen, depth + 1);
|
|
200
|
+
}
|
|
201
|
+
return res;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
// #region utility functions
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Returns the current timestamp in ISO format.
|
|
212
|
+
* @param {number} [date] - Optional date to format, defaults to current date.
|
|
213
|
+
*/
|
|
214
|
+
function getTimestamp(date, timeOnly = false) {
|
|
215
|
+
const now = date ? new Date(date) : new Date();
|
|
216
|
+
if (timeOnly) {
|
|
217
|
+
return now.toTimeString().split(' ')[0]; // HH:MM:SS
|
|
218
|
+
}
|
|
219
|
+
return now.toISOString();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Indents a string by a specified length.
|
|
225
|
+
* @param {string} str - The string to indent.
|
|
226
|
+
* @param {number} length - The number of spaces to indent each line.
|
|
227
|
+
* @returns {string} The indented string.
|
|
228
|
+
*/
|
|
229
|
+
function indent(str, length, separator = "") {
|
|
230
|
+
const lines = str.split("\n");
|
|
231
|
+
const prefixStr = " ".repeat(length) + separator;
|
|
232
|
+
for (let i = 1; i < lines.length; i++) {
|
|
233
|
+
let entry = lines[i].trim();
|
|
234
|
+
if (entry.length === 0) continue; // skip empty lines
|
|
235
|
+
// indent the line
|
|
236
|
+
lines[i] = prefixStr + entry;
|
|
237
|
+
}
|
|
238
|
+
return lines.join("\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Removes empty lines at the start of a string.
|
|
243
|
+
* @param {string} str - The string to process.
|
|
244
|
+
*/
|
|
245
|
+
function removeEmptyLinesAtStart(str) {
|
|
246
|
+
const lines = str.split("\n");
|
|
247
|
+
for (let i = 0; i < lines.length; i++) {
|
|
248
|
+
const line = lines[i].trim();
|
|
249
|
+
if (line.length > 0) {
|
|
250
|
+
lines[i] = line; // keep the first non-empty line
|
|
251
|
+
return lines.slice(i).join("\n");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Strips ANSI color codes from a string.
|
|
259
|
+
* @param {string} str - The string to process.
|
|
260
|
+
*/
|
|
261
|
+
function stripAnsiColors(str) {
|
|
262
|
+
// This pattern catches most ANSI escape sequences
|
|
263
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
// #region log to file
|
|
268
|
+
|
|
269
|
+
/** @type {Map<string, import("fs").WriteStream>} */
|
|
270
|
+
const filestreams = new Map();
|
|
271
|
+
const fileLogDirectory = "node_modules/.needle/logs";
|
|
272
|
+
// cleanup old log files
|
|
273
|
+
if (existsSync(fileLogDirectory)) {
|
|
274
|
+
const files = readdirSync(fileLogDirectory);
|
|
275
|
+
// sort by age and keep the last 10 files
|
|
276
|
+
files.sort((a, b) => {
|
|
277
|
+
const aStat = statSync(`${fileLogDirectory}/${a}`);
|
|
278
|
+
const bStat = statSync(`${fileLogDirectory}/${b}`);
|
|
279
|
+
return aStat.mtimeMs - bStat.mtimeMs;
|
|
280
|
+
});
|
|
281
|
+
// remove all but the last 30 files
|
|
282
|
+
const filesToKeep = 30;
|
|
283
|
+
for (let i = 0; i < files.length - filesToKeep; i++) {
|
|
284
|
+
rmSync(`${fileLogDirectory}/${files[i]}`, { force: true });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Writes a log message to the file.
|
|
290
|
+
* @param {ProcessType} process
|
|
291
|
+
* @param {string} log
|
|
292
|
+
* @param {string | null} connectionId - Optional connection ID for client logs.
|
|
293
|
+
*/
|
|
294
|
+
function writeToFile(process, log, connectionId) {
|
|
295
|
+
const filename = `${process}.needle.log`; //connectionId && process === "client" ? `${process}-${connectionId}.needle.log` : `${process}.needle.log`;
|
|
296
|
+
|
|
297
|
+
if (!filestreams.has(filename)) {
|
|
298
|
+
if (!existsSync(fileLogDirectory)) {
|
|
299
|
+
mkdirSync(fileLogDirectory, { recursive: true });
|
|
300
|
+
}
|
|
301
|
+
filestreams.set(filename, createWriteStream(`${fileLogDirectory}/${filename_timestamp}.${filename}`, { flags: 'a' }));
|
|
302
|
+
}
|
|
303
|
+
const writeStream = filestreams.get(filename);
|
|
304
|
+
if (!writeStream) {
|
|
305
|
+
if (debug) console.error(`No write stream for process: ${filename}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
writeStream.write(log + '\n');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
// #region process exit
|
|
314
|
+
function onExit() {
|
|
315
|
+
filestreams.forEach((stream) => stream.end());
|
|
316
|
+
filestreams.clear();
|
|
317
|
+
}
|
|
318
|
+
const events = ['SIGTERM', 'SIGINT', 'beforeExit', 'rejectionHandled', 'uncaughtException', 'exit'];
|
|
319
|
+
for (const event of events) {
|
|
320
|
+
process.on(event, onExit);
|
|
321
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { License } from "./license";
|
|
2
|
-
import { NeedlePWAOptions } from "./webmanifest.js";
|
|
3
2
|
|
|
4
3
|
export type needleModules = {
|
|
5
4
|
webpack: object | undefined
|
|
@@ -49,7 +48,7 @@ export type userSettings = {
|
|
|
49
48
|
/** Custom configuration for facebook instant games. */
|
|
50
49
|
facebookInstantGames?: {}
|
|
51
50
|
/** Set to true to create an imports.log file that shows all module imports. The file is generated when stopping the server. */
|
|
52
|
-
|
|
51
|
+
debugImportChains?: boolean;
|
|
53
52
|
|
|
54
53
|
/** Set to true to disable generating the buildinfo.json file in your output directory */
|
|
55
54
|
noBuildInfo?: boolean;
|
|
@@ -118,5 +117,8 @@ export type userSettings = {
|
|
|
118
117
|
*/
|
|
119
118
|
openBrowser?: boolean;
|
|
120
119
|
|
|
120
|
+
/** Automatically import MaterialX for needle engine in 'main.ts' */
|
|
121
121
|
loadMaterialX?: boolean;
|
|
122
|
+
|
|
123
|
+
disableLogging?: boolean;
|
|
122
124
|
}
|
package/plugins/vite/alias.js
CHANGED
|
@@ -42,18 +42,23 @@ const packages_to_resolve = {
|
|
|
42
42
|
because this is automatically done by vite according to whatever we define in our package.json exports
|
|
43
43
|
This did previously prevent us from declaring proper exports in package.json
|
|
44
44
|
*/
|
|
45
|
-
'@needle-tools/engine': (res, packageName, index,
|
|
45
|
+
'@needle-tools/engine': (res, packageName, index, _path) => {
|
|
46
46
|
// Check if the import is something like @needle-tools/engine/engine/engine_utils
|
|
47
47
|
// in which case we want to resolve into the lib directory
|
|
48
|
-
if (
|
|
48
|
+
if (_path.startsWith("@needle-tools/engine/engine")) {
|
|
49
49
|
return res + "/lib";
|
|
50
50
|
}
|
|
51
|
+
const node_modules_path = path.resolve(projectDir, 'node_modules', '@needle-tools/engine');
|
|
52
|
+
if (existsSync(node_modules_path)) {
|
|
53
|
+
return node_modules_path;
|
|
54
|
+
}
|
|
51
55
|
},
|
|
52
56
|
|
|
53
57
|
// Auto resolve to 'node_modules/<name>' or '@needle-tools/engine/node_modules/<name>'
|
|
54
58
|
'peerjs': "auto-resolve",
|
|
55
59
|
'websocket-ts': "auto-resolve",
|
|
56
60
|
'md5': "auto-resolve",
|
|
61
|
+
'three-mesh-bvh': "auto-resolve",
|
|
57
62
|
'postprocessing': "auto-resolve",
|
|
58
63
|
'@dimforge/rapier3d-compat': "auto-resolve",
|
|
59
64
|
}
|
package/plugins/vite/index.js
CHANGED
|
@@ -67,6 +67,7 @@ import { needleServer } from "./server.js";
|
|
|
67
67
|
import { needleNPM } from "./npm.js";
|
|
68
68
|
import { needleTransformCode } from "./transform.js";
|
|
69
69
|
import { needleMaterialXLoader } from "./materialx.js";
|
|
70
|
+
import { needleLogger } from "./logger.js";
|
|
70
71
|
export { needleServer } from "./server.js";
|
|
71
72
|
|
|
72
73
|
|
|
@@ -111,6 +112,7 @@ export const needlePlugins = async (command, config = undefined, userSettings =
|
|
|
111
112
|
userSettings = { ...defaultUserSettings, ...userSettings };
|
|
112
113
|
|
|
113
114
|
const array = [
|
|
115
|
+
needleLogger(command, config, userSettings),
|
|
114
116
|
needleDefines(command, config, userSettings),
|
|
115
117
|
needleLicense(command, config, userSettings),
|
|
116
118
|
needleViteAlias(command, config, userSettings),
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Patches console methods to capture log messages and send them to the server.
|
|
4
|
+
* This is useful for debugging and logging in the client.
|
|
5
|
+
* @param {"log" | "warn" | "info" | "debug" | "error" | "internal"} level
|
|
6
|
+
* @param {any} message - The log message to capture.
|
|
7
|
+
*/
|
|
8
|
+
function sendLogToServer(level, ...message) {
|
|
9
|
+
if ("hot" in import.meta) {
|
|
10
|
+
message = stringifyLog(message);
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
import.meta.hot.send("needle:client-log", { level, message: message });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// const obj = {
|
|
17
|
+
// hello: "world"
|
|
18
|
+
// }
|
|
19
|
+
// obj["test"] = obj;
|
|
20
|
+
// sendLogToServer("internal", "Test circular reference", obj);
|
|
21
|
+
|
|
22
|
+
if (import.meta && "hot" in import.meta) {
|
|
23
|
+
|
|
24
|
+
const originalLog = console.log;
|
|
25
|
+
const originalWarn = console.warn;
|
|
26
|
+
const originalInfo = console.info;
|
|
27
|
+
const originalDebug = console.debug;
|
|
28
|
+
const originalError = console.error;
|
|
29
|
+
|
|
30
|
+
console.log = (...args) => {
|
|
31
|
+
originalLog(...args);
|
|
32
|
+
sendLogToServer("log", ...args);
|
|
33
|
+
}
|
|
34
|
+
console.warn = (...args) => {
|
|
35
|
+
originalWarn(...args);
|
|
36
|
+
sendLogToServer("warn", ...args);
|
|
37
|
+
}
|
|
38
|
+
console.info = (...args) => {
|
|
39
|
+
originalInfo(...args);
|
|
40
|
+
sendLogToServer("info", ...args);
|
|
41
|
+
}
|
|
42
|
+
console.debug = (...args) => {
|
|
43
|
+
originalDebug(...args);
|
|
44
|
+
sendLogToServer("debug", ...args);
|
|
45
|
+
}
|
|
46
|
+
console.error = (...args) => {
|
|
47
|
+
originalError(...args);
|
|
48
|
+
sendLogToServer("error", ...args);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
sendLogToServer("internal", `Page loaded
|
|
53
|
+
URL: ${window.location.href}
|
|
54
|
+
UserAgent: ${navigator.userAgent}
|
|
55
|
+
Screen: ${window.innerWidth} x ${window.innerHeight}px
|
|
56
|
+
Device Pixel Ratio: ${window.devicePixelRatio}
|
|
57
|
+
Device Memory: ${"deviceMemory" in navigator ? navigator.deviceMemory : "Not available"} GB
|
|
58
|
+
Online: ${navigator.onLine}
|
|
59
|
+
Language: ${navigator.language}
|
|
60
|
+
Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}
|
|
61
|
+
Connection: ${"connection" in navigator ? JSON.stringify(navigator.connection) : "Not available"}
|
|
62
|
+
User Activation: ${"userActivation" in navigator ? JSON.stringify(navigator.userActivation) : "Not available"}
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
if ("gpu" in navigator) {
|
|
66
|
+
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
navigator.gpu.requestAdapter()
|
|
69
|
+
.then(adapter => adapter ? adapter.requestDevice() : null)
|
|
70
|
+
.then(device => {
|
|
71
|
+
if (device) {
|
|
72
|
+
const adapterInfo = device.adapterInfo;
|
|
73
|
+
if (adapterInfo) {
|
|
74
|
+
sendLogToServer("internal", [`WebGPU adapter info`, {
|
|
75
|
+
vendor: adapterInfo.vendor,
|
|
76
|
+
architecture: adapterInfo.architecture,
|
|
77
|
+
device: adapterInfo.device,
|
|
78
|
+
description: adapterInfo.description,
|
|
79
|
+
features: adapterInfo.features,
|
|
80
|
+
limits: adapterInfo.limits
|
|
81
|
+
}]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
// silently fail
|
|
89
|
+
sendLogToServer("error", `Error during initial log: ${e.message}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
window.addEventListener('error', (event) => {
|
|
93
|
+
const errorMessage = event.error ? event.error.stack || event.error.message : event.message;
|
|
94
|
+
sendLogToServer("error", errorMessage);
|
|
95
|
+
});
|
|
96
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
97
|
+
const reason = event.reason ? event.reason.stack || event.reason.message : "Unhandled rejection without reason";
|
|
98
|
+
sendLogToServer("error", `Unhandled promise rejection: ${reason}`);
|
|
99
|
+
});
|
|
100
|
+
window.addEventListener('beforeunload', () => {
|
|
101
|
+
sendLogToServer("internal", "Page is unloading");
|
|
102
|
+
});
|
|
103
|
+
document.addEventListener('visibilitychange', () => {
|
|
104
|
+
console.log("Visibility changed:", document.visibilityState);
|
|
105
|
+
if (document.visibilityState === 'hidden') {
|
|
106
|
+
sendLogToServer("internal", "Page is hidden");
|
|
107
|
+
}
|
|
108
|
+
else if (document.visibilityState === 'visible') {
|
|
109
|
+
sendLogToServer("internal", "Page is visible again");
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
sendLogToServer("internal", `Page visibility changed to ${document.visibilityState}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
window.addEventListener("focus", () => {
|
|
116
|
+
sendLogToServer("internal", "Page gained focus");
|
|
117
|
+
});
|
|
118
|
+
window.addEventListener("blur", () => {
|
|
119
|
+
sendLogToServer("internal", "Page lost focus");
|
|
120
|
+
});
|
|
121
|
+
window.addEventListener('load', () => {
|
|
122
|
+
sendLogToServer("internal", "Page fully loaded");
|
|
123
|
+
});
|
|
124
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
125
|
+
sendLogToServer("internal", "DOM fully loaded and parsed");
|
|
126
|
+
});
|
|
127
|
+
window.addEventListener('online', () => {
|
|
128
|
+
sendLogToServer("internal", "Browser is online");
|
|
129
|
+
});
|
|
130
|
+
window.addEventListener('offline', () => {
|
|
131
|
+
sendLogToServer("warn", "Browser is offline");
|
|
132
|
+
});
|
|
133
|
+
window.addEventListener('resize', () => {
|
|
134
|
+
sendLogToServer("internal", `Window resized to ${window.innerWidth}x${window.innerHeight}px`);
|
|
135
|
+
});
|
|
136
|
+
window.addEventListener('orientationchange', () => {
|
|
137
|
+
sendLogToServer("internal", `Orientation changed to ${screen.orientation.type}`);
|
|
138
|
+
});
|
|
139
|
+
window.addEventListener('fullscreenchange', () => {
|
|
140
|
+
if (document.fullscreenElement) {
|
|
141
|
+
sendLogToServer("internal", "Entered fullscreen mode");
|
|
142
|
+
} else {
|
|
143
|
+
sendLogToServer("internal", "Exited fullscreen mode");
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
// url change event
|
|
149
|
+
window.addEventListener('hashchange', () => {
|
|
150
|
+
sendLogToServer("internal", `URL hash changed to ${location.hash}`);
|
|
151
|
+
});
|
|
152
|
+
window.addEventListener('popstate', () => {
|
|
153
|
+
sendLogToServer("internal", `History state changed: ${JSON.stringify(history.state)}`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// #region copied from common/logger.js
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Stringifies a log message, handling circular references and formatting.
|
|
169
|
+
* @param {any} log
|
|
170
|
+
* @param {Set<any>} [seen]
|
|
171
|
+
*/
|
|
172
|
+
function stringifyLog(log, seen = new Set(), depth = 0) {
|
|
173
|
+
|
|
174
|
+
const isServer = typeof window === "undefined";
|
|
175
|
+
const stringify_limits = {
|
|
176
|
+
string: isServer ? 100_000 : 2000,
|
|
177
|
+
object_keys: isServer ? 100 : 200,
|
|
178
|
+
array_items: isServer ? 2_000 : 100,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof log === "string") {
|
|
182
|
+
if (log.length > stringify_limits.string) log = `${log.slice(0, stringify_limits.string)}... <truncated ${log.length - stringify_limits.string} characters>`;
|
|
183
|
+
return log;
|
|
184
|
+
}
|
|
185
|
+
if (typeof log === "number" || typeof log === "boolean") {
|
|
186
|
+
return String(log);
|
|
187
|
+
}
|
|
188
|
+
if (log === null) {
|
|
189
|
+
return "null";
|
|
190
|
+
}
|
|
191
|
+
if (log === undefined) {
|
|
192
|
+
return "undefined";
|
|
193
|
+
}
|
|
194
|
+
if (typeof log === "function") {
|
|
195
|
+
return "<function>";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (seen.has(log)) return "<circular>";
|
|
199
|
+
|
|
200
|
+
if (Array.isArray(log)) {
|
|
201
|
+
seen.add(log);
|
|
202
|
+
return stringifyArray(log);
|
|
203
|
+
}
|
|
204
|
+
if (typeof log === "object") {
|
|
205
|
+
seen.add(log);
|
|
206
|
+
// const str = JSON.stringify(log, (key, value) => {
|
|
207
|
+
// if (typeof value === "function") return "<function>";
|
|
208
|
+
// if (typeof value === "string") return stringifyLog(value, seen, depth + 1);
|
|
209
|
+
// if (typeof value === "object") {
|
|
210
|
+
// if (seen.has(value)) return "<circular>";
|
|
211
|
+
// seen.add(value);
|
|
212
|
+
// }
|
|
213
|
+
// return value;
|
|
214
|
+
// });
|
|
215
|
+
// return str;
|
|
216
|
+
const keys = Object.keys(log);
|
|
217
|
+
let res = "{";
|
|
218
|
+
for (let i = 0; i < keys.length; i++) {
|
|
219
|
+
const key = keys[i];
|
|
220
|
+
let value = log[key];
|
|
221
|
+
|
|
222
|
+
if (typeof value === "number") {
|
|
223
|
+
// clamp precision for numbers
|
|
224
|
+
value = Number(value.toFixed(6));
|
|
225
|
+
}
|
|
226
|
+
let str = stringifyLog(value, seen, depth + 1);
|
|
227
|
+
if (typeof value === "object") {
|
|
228
|
+
if (Array.isArray(value)) {
|
|
229
|
+
str = `[${str}]`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (typeof value === "string") {
|
|
233
|
+
str = `"${str}"`;
|
|
234
|
+
}
|
|
235
|
+
if (i > 0) res += ", ";
|
|
236
|
+
res += `"${key}":${str}`;
|
|
237
|
+
}
|
|
238
|
+
res += "}";
|
|
239
|
+
return res;
|
|
240
|
+
// let entries = Object.entries(log).map(([key, value], index) => {
|
|
241
|
+
// if (index > stringify_limits.object_keys) return `"${key}": <truncated>`;
|
|
242
|
+
// return `"${key}": ${stringifyLog(value, seen, depth + 1)}`;
|
|
243
|
+
// });
|
|
244
|
+
// return `{ ${entries.join(", ")} }`;
|
|
245
|
+
}
|
|
246
|
+
return String(log);
|
|
247
|
+
|
|
248
|
+
function stringifyArray(arr) {
|
|
249
|
+
let res = "";
|
|
250
|
+
for (let i = 0; i < arr.length; i++) {
|
|
251
|
+
let entry = arr[i];
|
|
252
|
+
if (res && i > 0) res += ", ";
|
|
253
|
+
if (i > stringify_limits.array_items) {
|
|
254
|
+
res += "<truncated " + (arr.length - i) + ">";
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
res += stringifyLog(entry, seen, depth + 1);
|
|
258
|
+
}
|
|
259
|
+
return res;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|