@tsln/console 0.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mateo Murphy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @tsln/console
2
+
3
+ The Max console as a [Console API](https://developer.mozilla.org/docs/Web/API/console) object
4
+ for `[v8]` scripts: `log`, `info`, `warn`, `error`, `assert`, `trace`, `dir`, `table`, `group`,
5
+ `count`, `time` and the rest. Values are formatted by `inspect()`, which handles circular
6
+ references, functions, Map and Set, errors, and summarises Max and Live objects (`Maxobj`, `Dict`,
7
+ `LiveAPI`, `LiveObject`) instead of expanding their internals. A leading string can hold format
8
+ specifiers (`%s`, `%d`, `%i`, `%f`, `%o`, `%O`, `%j`, `%c`, `%%`).
9
+
10
+ ```ts
11
+ import console = require("./vendor/console");
12
+
13
+ console.log("tracks:", song.tracks); // tracks: [LiveObject<Track> live_set tracks 0, ...]
14
+ console.log("%d of %d", done, total);
15
+ console.error("no input on", index); // in red
16
+ console.warn("careful"); // "warning: careful", since the Max console has no warning level
17
+ console.group("connecting");
18
+ console.log("indented");
19
+ console.groupEnd();
20
+ console.time("render");
21
+ console.timeEnd("render"); // render: 12 ms
22
+ console.dir(deep, { depth: 6 }); // more than log() shows
23
+ ```
24
+
25
+ The module is also callable, as a shorthand for `log()`, so `import log = require("./vendor/console")`
26
+ and `log("loaded")` work too.
27
+
28
+ ## As the global `console`
29
+
30
+ A bundled npm package looks for `console` as a global. `install()` provides it, keeping a console
31
+ Max supplies itself and adding only the methods it lacks:
32
+
33
+ ```ts
34
+ console.install();
35
+ import thing = require("./vendor/thing");
36
+ ```
37
+
38
+ ## Log file
39
+
40
+ `logToFile(path)` also writes every line to a file, timestamped and with its level; `logToFile(null)`
41
+ stops. Use an absolute path.
42
+
43
+ ## Install
44
+
45
+ ```sh
46
+ npm install --save-dev @tsln/console
47
+ ```
48
+
49
+ Max's `require()` searches Max's file path and knows nothing of `node_modules`, so the package's
50
+ single CommonJS file, `dist/index.js`, has to be copied or bundled to somewhere Max can find it
51
+ (the `max-msp` repository bundles it to `js/vendor/console.js` with esbuild), with a declaration
52
+ file next to it for the types:
53
+
54
+ ```ts
55
+ // src/vendor/console.d.ts
56
+ import console = require("@tsln/console");
57
+ export = console;
58
+ ```
59
+
60
+ The package depends on `@tsln/max-types` for the Max globals it calls (`post`, `error`, `File`).
61
+
62
+ Where the Console API has no Max equivalent: `table` posts the data like `log`, and `clear` does
63
+ nothing, since a script can't clear the Max console.
@@ -0,0 +1,110 @@
1
+ /// <reference types="@tsln/max-types" preserve="true" />
2
+ interface InspectOptions {
3
+ /** How many levels of nested objects and arrays to expand. Default 3. */
4
+ depth?: number;
5
+ /** How many array items or object keys to show before summarizing the rest. Default 50. */
6
+ maxItems?: number;
7
+ }
8
+ /**
9
+ * Formats any value as readable text for debugging. Unlike JSON.stringify it handles
10
+ * circular references (`[Circular]`), functions, undefined, Map/Set, errors, and
11
+ * summarizes Max and Live objects instead of expanding their internals.
12
+ *
13
+ * @example
14
+ * inspect({ a: 1, list: [1, 2] }); // { a: 1, list: [1, 2] }
15
+ */
16
+ declare function inspect(value: unknown, options?: InspectOptions): string;
17
+ /**
18
+ * Also writes everything posted through this module to a file, one timestamped line per
19
+ * call. Lines are appended, and the file is created if it doesn't exist. Pass null to stop.
20
+ *
21
+ * Use an absolute path; how Max resolves relative paths for writing isn't documented.
22
+ * If the file can't be opened, an error is posted and file logging stops.
23
+ *
24
+ * @param path the file to write to, e.g. "C:/Users/me/Documents/spat.log"
25
+ * @example
26
+ * console.logToFile("C:/Users/me/Documents/spat.log");
27
+ * console.log("connecting", tracks); // console, and "2026-09-14T10:15:00.000Z [log] connecting [...]" in the file
28
+ */
29
+ declare function logToFile(path: string | null): void;
30
+ /**
31
+ * Posts values to the Max console, formatted with inspect(). Strings are posted as they are,
32
+ * and a leading string can hold format specifiers (`%s`, `%d`, `%o`, ...).
33
+ *
34
+ * @example
35
+ * console.log("tracks:", song.tracks); // tracks: [LiveObject<Track> live_set tracks 0, ...]
36
+ * console.log("%d of %d", done, total);
37
+ */
38
+ declare function log(...values: unknown[]): void;
39
+ /** Same as log(); the Console API has both. */
40
+ declare function info(...values: unknown[]): void;
41
+ /** Same as log(); the Console API has both. */
42
+ declare function debug(...values: unknown[]): void;
43
+ /** Posts values like log(), prefixed with "warning:", as the Max console has no warning level. */
44
+ declare function warn(...values: unknown[]): void;
45
+ /**
46
+ * Posts values to the Max console as an error (in red), formatted like log().
47
+ *
48
+ * @example
49
+ * console.error("no audio input", index);
50
+ */
51
+ declare function logError(...values: unknown[]): void;
52
+ /** Posts values as an error when the condition is false; nothing otherwise. */
53
+ declare function assert(condition: unknown, ...values: unknown[]): void;
54
+ /** Posts values as an error, followed by the stack of the call. */
55
+ declare function trace(...values: unknown[]): void;
56
+ /**
57
+ * Posts one value expanded with inspect(), with its options: `console.dir(x, { depth: 6 })`
58
+ * shows more of a deep object than log() would.
59
+ */
60
+ declare function dir(value: unknown, options?: InspectOptions): void;
61
+ /** Posts the data like log(); the Max console has no tables. */
62
+ declare function table(data: unknown): void;
63
+ /** Starts an indented group of output, headed by the label if there is one. */
64
+ declare function group(...label: unknown[]): void;
65
+ /** Ends the innermost group. */
66
+ declare function groupEnd(): void;
67
+ /** Posts how many times count() has been called with the label. */
68
+ declare function count(label?: unknown): void;
69
+ /** Resets the counter for the label. */
70
+ declare function countReset(label?: unknown): void;
71
+ /** Starts a timer under the label. Max reports in milliseconds, as a browser does. */
72
+ declare function time(label?: unknown): void;
73
+ /** Posts the time since time(label), keeping the timer running. */
74
+ declare function timeLog(label?: unknown, ...values: unknown[]): void;
75
+ /** Posts the time since time(label) and stops the timer. */
76
+ declare function timeEnd(label?: unknown): void;
77
+ /** Does nothing: a script can't clear the Max console. Here so callers don't have to check. */
78
+ declare function clear(): void;
79
+ /**
80
+ * Makes this the global `console`, for bundled npm packages and other code that expects
81
+ * one. A console Max provides itself is kept, and only the methods it lacks are added, so
82
+ * this is safe to call whether or not Max has grown one.
83
+ */
84
+ declare function install(): void;
85
+ declare const _default: typeof log & {
86
+ log: typeof log;
87
+ info: typeof info;
88
+ debug: typeof debug;
89
+ warn: typeof warn;
90
+ error: typeof logError;
91
+ assert: typeof assert;
92
+ trace: typeof trace;
93
+ dir: typeof dir;
94
+ dirxml: typeof log;
95
+ table: typeof table;
96
+ group: typeof group;
97
+ groupCollapsed: typeof group;
98
+ groupEnd: typeof groupEnd;
99
+ count: typeof count;
100
+ countReset: typeof countReset;
101
+ time: typeof time;
102
+ timeLog: typeof timeLog;
103
+ timeEnd: typeof timeEnd;
104
+ clear: typeof clear;
105
+ } & {
106
+ logToFile: typeof logToFile;
107
+ inspect: typeof inspect;
108
+ install: typeof install;
109
+ };
110
+ export = _default;
package/dist/index.js ADDED
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+
3
+ // src/index.ts
4
+ function inspect(value, options = {}) {
5
+ const depth = options.depth ?? 3;
6
+ const maxItems = options.maxItems ?? 50;
7
+ const ancestors = /* @__PURE__ */ new Set();
8
+ function format(value2, level, quoteStrings) {
9
+ switch (typeof value2) {
10
+ case "string":
11
+ return quoteStrings ? JSON.stringify(value2) : value2;
12
+ case "number":
13
+ case "boolean":
14
+ case "undefined":
15
+ return String(value2);
16
+ case "bigint":
17
+ return `${value2}n`;
18
+ case "symbol":
19
+ return value2.toString();
20
+ case "function":
21
+ return `[Function ${value2.name || "anonymous"}]`;
22
+ }
23
+ if (value2 === null) return "null";
24
+ const object = value2;
25
+ if (ancestors.has(object)) return "[Circular]";
26
+ try {
27
+ const summary = summarize(object);
28
+ if (summary !== void 0) return summary;
29
+ const name = object.constructor?.name;
30
+ const isPlain = !name || name === "Object";
31
+ if (level >= depth) {
32
+ return Array.isArray(object) ? `[Array(${object.length})]` : `[${name || "Object"}]`;
33
+ }
34
+ ancestors.add(object);
35
+ try {
36
+ if (Array.isArray(object)) {
37
+ return `[${joinItems(object, (item) => format(item, level + 1, true))}]`;
38
+ }
39
+ if (object instanceof Map) {
40
+ const entries2 = joinItems(
41
+ [...object],
42
+ ([k, v]) => `${format(k, level + 1, true)} => ${format(v, level + 1, true)}`
43
+ );
44
+ return `Map(${object.size}) ${braces(entries2)}`;
45
+ }
46
+ if (object instanceof Set) {
47
+ return `Set(${object.size}) ${braces(joinItems([...object], (item) => format(item, level + 1, true)))}`;
48
+ }
49
+ const entries = joinItems(
50
+ Object.keys(object),
51
+ (key) => `${key}: ${format(object[key], level + 1, true)}`
52
+ );
53
+ return `${isPlain ? "" : `${name} `}${braces(entries)}`;
54
+ } finally {
55
+ ancestors.delete(object);
56
+ }
57
+ } catch (e) {
58
+ return `[unprintable: ${e}]`;
59
+ }
60
+ }
61
+ function joinItems(items, formatItem) {
62
+ const parts = items.slice(0, maxItems).map(formatItem);
63
+ if (items.length > maxItems) parts.push(`... ${items.length - maxItems} more`);
64
+ return parts.join(", ");
65
+ }
66
+ function braces(contents) {
67
+ return contents ? `{ ${contents} }` : "{}";
68
+ }
69
+ function summarize(object) {
70
+ if (object instanceof Error) {
71
+ return `${object.name}: ${object.message}`;
72
+ }
73
+ if (object instanceof Date) {
74
+ return object.toISOString();
75
+ }
76
+ if (typeof LiveAPI !== "undefined" && object.api instanceof LiveAPI) {
77
+ return Number(object.api.id) === 0 ? "LiveObject (no object)" : `LiveObject<${object.api.type}> ${object.api.unquotedpath}`;
78
+ }
79
+ if (typeof LiveAPI !== "undefined" && object instanceof LiveAPI) {
80
+ return `LiveAPI<${object.type}> ${object.unquotedpath}`;
81
+ }
82
+ if (typeof Dict !== "undefined" && object instanceof Dict) {
83
+ return `Dict "${object.name}" ${object.stringify_compressed()}`;
84
+ }
85
+ if (typeof object.maxclass === "string" && typeof object.getattr === "function") {
86
+ return `Maxobj<${object.maxclass}>${object.varname ? ` "${object.varname}"` : ""}`;
87
+ }
88
+ return void 0;
89
+ }
90
+ return format(value, 0, false);
91
+ }
92
+ function formatValues(values) {
93
+ const rest = [...values];
94
+ const parts = [];
95
+ if (typeof rest[0] === "string" && rest[0].includes("%")) {
96
+ const template = rest.shift();
97
+ parts.push(
98
+ template.replace(/%([sdifoOjc%])/g, (match, spec) => {
99
+ if (spec === "%") return "%";
100
+ if (spec === "c") {
101
+ rest.shift();
102
+ return "";
103
+ }
104
+ if (rest.length === 0) return match;
105
+ const value = rest.shift();
106
+ switch (spec) {
107
+ case "s":
108
+ return typeof value === "string" ? value : inspect(value, { depth: 1 });
109
+ case "d":
110
+ case "i":
111
+ return typeof value === "bigint" ? `${value}n` : String(Math.trunc(Number(value)));
112
+ case "f":
113
+ return String(Number(value));
114
+ case "j":
115
+ try {
116
+ return JSON.stringify(value);
117
+ } catch {
118
+ return "[Circular]";
119
+ }
120
+ default:
121
+ return inspect(value, { depth: 4 });
122
+ }
123
+ })
124
+ );
125
+ }
126
+ for (const value of rest) {
127
+ parts.push(inspect(value));
128
+ }
129
+ return parts.join(" ");
130
+ }
131
+ function maxError(...args) {
132
+ const fn = globalThis.error ?? error;
133
+ fn(...args);
134
+ }
135
+ var logFilePath = null;
136
+ function logToFile(path) {
137
+ logFilePath = path;
138
+ }
139
+ function appendToLogFile(level, text) {
140
+ if (!logFilePath) {
141
+ return;
142
+ }
143
+ let file = new File(logFilePath, "readwrite");
144
+ if (!file.isopen) {
145
+ file = new File(logFilePath, "write");
146
+ }
147
+ if (!file.isopen) {
148
+ const path = logFilePath;
149
+ logFilePath = null;
150
+ maxError(`log file: couldn't open ${path}; file logging stopped`, "\n");
151
+ return;
152
+ }
153
+ try {
154
+ file.position = file.eof;
155
+ file.writeline(`${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${text}`);
156
+ } finally {
157
+ file.close();
158
+ }
159
+ }
160
+ var groupDepth = 0;
161
+ function write(level, values) {
162
+ const text = formatValues(values);
163
+ const line = " ".repeat(groupDepth) + (level === "warn" ? `warning: ${text}` : text);
164
+ if (level === "error") {
165
+ maxError(line, "\n");
166
+ } else {
167
+ post(line, "\n");
168
+ }
169
+ appendToLogFile(level, text);
170
+ }
171
+ function log(...values) {
172
+ write("log", values);
173
+ }
174
+ function info(...values) {
175
+ write("info", values);
176
+ }
177
+ function debug(...values) {
178
+ write("debug", values);
179
+ }
180
+ function warn(...values) {
181
+ write("warn", values);
182
+ }
183
+ function logError(...values) {
184
+ write("error", values);
185
+ }
186
+ function assert(condition, ...values) {
187
+ if (condition) return;
188
+ write("error", values.length ? ["Assertion failed:", ...values] : ["Assertion failed"]);
189
+ }
190
+ function trace(...values) {
191
+ const stack = (new Error().stack ?? "").split("\n").slice(2).map((line) => line.trim()).filter(Boolean);
192
+ const text = values.length ? formatValues(values) : "Trace";
193
+ write("error", [stack.length ? `${text}
194
+ ${stack.join("\n ")}` : text]);
195
+ }
196
+ function dir(value, options) {
197
+ write("log", [inspect(value, options)]);
198
+ }
199
+ function table(data) {
200
+ write("log", [data]);
201
+ }
202
+ function group(...label) {
203
+ if (label.length) write("log", label);
204
+ groupDepth++;
205
+ }
206
+ function groupEnd() {
207
+ groupDepth = Math.max(0, groupDepth - 1);
208
+ }
209
+ var counts = /* @__PURE__ */ new Map();
210
+ var timers = /* @__PURE__ */ new Map();
211
+ function count(label = "default") {
212
+ const key = String(label);
213
+ const n = (counts.get(key) ?? 0) + 1;
214
+ counts.set(key, n);
215
+ write("log", [`${key}: ${n}`]);
216
+ }
217
+ function countReset(label = "default") {
218
+ counts.delete(String(label));
219
+ }
220
+ function time(label = "default") {
221
+ const key = String(label);
222
+ if (timers.has(key)) {
223
+ warn(`Timer '${key}' already exists`);
224
+ return;
225
+ }
226
+ timers.set(key, Date.now());
227
+ }
228
+ function elapsed(label) {
229
+ const key = String(label);
230
+ const start = timers.get(key);
231
+ if (start === void 0) {
232
+ warn(`Timer '${key}' does not exist`);
233
+ return void 0;
234
+ }
235
+ return [key, `${key}: ${Date.now() - start} ms`];
236
+ }
237
+ function timeLog(label = "default", ...values) {
238
+ const result = elapsed(label);
239
+ if (result) write("log", [result[1], ...values]);
240
+ }
241
+ function timeEnd(label = "default") {
242
+ const result = elapsed(label);
243
+ if (!result) return;
244
+ timers.delete(result[0]);
245
+ write("log", [result[1]]);
246
+ }
247
+ function clear() {
248
+ }
249
+ var methods = {
250
+ log,
251
+ info,
252
+ debug,
253
+ warn,
254
+ error: logError,
255
+ assert,
256
+ trace,
257
+ dir,
258
+ dirxml: log,
259
+ table,
260
+ group,
261
+ groupCollapsed: group,
262
+ groupEnd,
263
+ count,
264
+ countReset,
265
+ time,
266
+ timeLog,
267
+ timeEnd,
268
+ clear
269
+ };
270
+ function install() {
271
+ const target = globalThis;
272
+ if (!target.console) {
273
+ target.console = { ...methods };
274
+ return;
275
+ }
276
+ for (const [name, fn] of Object.entries(methods)) {
277
+ if (typeof target.console[name] !== "function") {
278
+ target.console[name] = fn;
279
+ }
280
+ }
281
+ }
282
+ module.exports = Object.assign(log, methods, { logToFile, inspect, install });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@tsln/console",
3
+ "version": "0.0.0",
4
+ "description": "The Max console as a Console API object for [v8] scripts, with an install() that makes it the global console for bundled packages",
5
+ "license": "MIT",
6
+ "author": "Mateo Murphy",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/tsln-lab/max-packages.git",
10
+ "directory": "packages/console"
11
+ },
12
+ "keywords": [
13
+ "max",
14
+ "msp",
15
+ "max-msp",
16
+ "max-for-live",
17
+ "v8",
18
+ "console",
19
+ "logging",
20
+ "typescript"
21
+ ],
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "CHANGELOG.md",
28
+ "LICENSE"
29
+ ],
30
+ "sideEffects": false,
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@tsln/max-types": "^0.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "esbuild": "^0.28.2",
39
+ "typescript": "7.0.2"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.build.json && esbuild src/index.ts --bundle --format=cjs --platform=neutral --target=es2022 --outfile=dist/index.js --log-level=warning",
43
+ "typecheck": "tsc -p tsconfig.json",
44
+ "test": "node tests/console.test.cjs"
45
+ }
46
+ }