@lakutata/logger 0.0.0-stage → 3.0.0-beta.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.
Files changed (41) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +137 -2
  3. package/THIRD_PARTY_NOTICES.md +158 -0
  4. package/dist/cjs/Logger.d.ts +291 -0
  5. package/dist/cjs/Logger.js +185 -0
  6. package/dist/cjs/exports/Logger.d.ts +2 -0
  7. package/dist/cjs/exports/Logger.js +20 -0
  8. package/dist/cjs/lib/Colors.d.ts +8 -0
  9. package/dist/cjs/lib/Colors.js +48 -0
  10. package/dist/cjs/lib/Destination.d.ts +38 -0
  11. package/dist/cjs/lib/Destination.js +103 -0
  12. package/dist/cjs/lib/Format.d.ts +7 -0
  13. package/dist/cjs/lib/Format.js +106 -0
  14. package/dist/cjs/lib/Pretty.d.ts +9 -0
  15. package/dist/cjs/lib/Pretty.js +265 -0
  16. package/dist/cjs/lib/Record.d.ts +25 -0
  17. package/dist/cjs/lib/Record.js +139 -0
  18. package/dist/cjs/lib/Serializers.d.ts +26 -0
  19. package/dist/cjs/lib/Serializers.js +150 -0
  20. package/dist/cjs/lib/Stringify.d.ts +5 -0
  21. package/dist/cjs/lib/Stringify.js +110 -0
  22. package/dist/cjs/package.json +1 -0
  23. package/dist/esm/Logger.js +182 -0
  24. package/dist/esm/exports/Logger.js +1 -0
  25. package/dist/esm/lib/Colors.js +45 -0
  26. package/dist/esm/lib/Destination.js +99 -0
  27. package/dist/esm/lib/Format.js +103 -0
  28. package/dist/esm/lib/Pretty.js +262 -0
  29. package/dist/esm/lib/Record.js +134 -0
  30. package/dist/esm/lib/Serializers.js +143 -0
  31. package/dist/esm/lib/Stringify.js +107 -0
  32. package/dist/types/Logger.d.ts +291 -0
  33. package/dist/types/exports/Logger.d.ts +2 -0
  34. package/dist/types/lib/Colors.d.ts +8 -0
  35. package/dist/types/lib/Destination.d.ts +38 -0
  36. package/dist/types/lib/Format.d.ts +7 -0
  37. package/dist/types/lib/Pretty.d.ts +9 -0
  38. package/dist/types/lib/Record.d.ts +25 -0
  39. package/dist/types/lib/Serializers.d.ts +26 -0
  40. package/dist/types/lib/Stringify.d.ts +5 -0
  41. package/package.json +43 -4
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stringify = stringify;
4
+ //Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
5
+ const MAXIMUM_DEPTH = 5;
6
+ const MAXIMUM_BREADTH = 100;
7
+ const TypedArrayToStringTag = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Int8Array.prototype), Symbol.toStringTag).get;
8
+ /**
9
+ * The count of the omitted items
10
+ * @param count
11
+ */
12
+ function itemCount(count) {
13
+ return count === 1 ? '1 item' : `${count} items`;
14
+ }
15
+ /**
16
+ * Serialize a value which JSON cannot (circular references, bigints):
17
+ * sorted keys, circular references as "[Circular]", limited depth and breadth
18
+ * @param key
19
+ * @param value
20
+ * @param stack
21
+ */
22
+ function safeStringify(key, value, stack) {
23
+ switch (typeof value) {
24
+ case 'string':
25
+ return JSON.stringify(value);
26
+ case 'number':
27
+ return isFinite(value) ? String(value) : 'null';
28
+ case 'boolean':
29
+ return value ? 'true' : 'false';
30
+ case 'bigint':
31
+ return String(value);
32
+ case 'object': {
33
+ if (value === null)
34
+ return 'null';
35
+ if (typeof value.toJSON === 'function') {
36
+ value = value.toJSON(key);
37
+ if (typeof value !== 'object')
38
+ return safeStringify(key, value, stack);
39
+ if (value === null)
40
+ return 'null';
41
+ }
42
+ if (stack.includes(value))
43
+ return '"[Circular]"';
44
+ let result = '';
45
+ if (Array.isArray(value)) {
46
+ if (!value.length)
47
+ return '[]';
48
+ if (MAXIMUM_DEPTH < stack.length + 1)
49
+ return '"[Array]"';
50
+ stack.push(value);
51
+ const count = Math.min(value.length, MAXIMUM_BREADTH);
52
+ for (let index = 0; index < count; index++) {
53
+ result += `${index ? ',' : ''}${safeStringify(String(index), value[index], stack) ?? 'null'}`;
54
+ }
55
+ if (value.length - 1 > MAXIMUM_BREADTH)
56
+ result += `,"... ${itemCount(value.length - MAXIMUM_BREADTH - 1)} not stringified"`;
57
+ stack.pop();
58
+ return `[${result}]`;
59
+ }
60
+ let keys = Object.keys(value);
61
+ const keyCount = keys.length;
62
+ if (!keyCount)
63
+ return '{}';
64
+ if (MAXIMUM_DEPTH < stack.length + 1)
65
+ return '"[Object]"';
66
+ let separator = '';
67
+ let count = Math.min(keyCount, MAXIMUM_BREADTH);
68
+ if (value.length !== undefined && TypedArrayToStringTag.call(value) !== undefined && value.length !== 0) {
69
+ const items = Math.min(value.length, MAXIMUM_BREADTH);
70
+ for (let index = 0; index < items; index++)
71
+ result += `${index ? ',' : ''}"${index}":${value[index]}`;
72
+ keys = keys.slice(value.length);
73
+ count -= value.length;
74
+ separator = ',';
75
+ }
76
+ keys.sort();
77
+ stack.push(value);
78
+ for (let index = 0; index < count; index++) {
79
+ const item = safeStringify(keys[index], value[keys[index]], stack);
80
+ if (item === undefined)
81
+ continue;
82
+ result += `${separator}${JSON.stringify(keys[index])}:${item}`;
83
+ separator = ',';
84
+ }
85
+ if (keyCount > MAXIMUM_BREADTH)
86
+ result += `${separator}"...":"${itemCount(keyCount - MAXIMUM_BREADTH)} not stringified"`;
87
+ stack.pop();
88
+ return `{${result}}`;
89
+ }
90
+ default:
91
+ return undefined;
92
+ }
93
+ }
94
+ /**
95
+ * Serialize a value to JSON, the values JSON cannot serialize are serialized safely
96
+ * @param value
97
+ */
98
+ function stringify(value) {
99
+ try {
100
+ return JSON.stringify(value);
101
+ }
102
+ catch {
103
+ try {
104
+ return safeStringify('', value, []);
105
+ }
106
+ catch {
107
+ return '"[unable to serialize, circular reference is too complex to analyze]"';
108
+ }
109
+ }
110
+ }
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,182 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Application, Component, DTO } from '@lakutata/core';
11
+ import { Configurable, Singleton } from '@lakutata/core/decorator/di';
12
+ import { Stream } from 'node:stream';
13
+ import { logBindings, buildRecord, LEVELS } from './lib/Record.js';
14
+ import { REDACTED_HEADERS } from './lib/Serializers.js';
15
+ import { pretty } from './lib/Pretty.js';
16
+ import { COLOR_PALETTE, PLAIN_PALETTE } from './lib/Colors.js';
17
+ import { Destination } from './lib/Destination.js';
18
+ //The loggers with buffered lines to write when the process exits
19
+ const activeLoggers = new Set();
20
+ process.once('exit', () => activeLoggers.forEach((logger) => logger.flush()));
21
+ /**
22
+ * The logger component of the applications built with `lakutata`: the package presets it as the `log` component (and
23
+ * creates it first), so the framework and the application log through it. It writes the pino log format: one JSON
24
+ * object per line, with `level` (10 trace, 20 debug, 30 info, 40 warn, 50 error, 60 fatal), `time` (milliseconds since the epoch),
25
+ * `pid`, `hostname`, `name` (the application's name), the properties of the logged object and `msg`; with `pretty` on
26
+ * (the default), it writes readable lines instead: `[HH:MM:ss.SSS] LEVEL (name/pid): message`, then one indented line
27
+ * per property, in the local time zone.
28
+ *
29
+ * Configure it through the `log` entry of the application's `components` (no `class` needed:
30
+ * `log: {level: 'info', pretty: false}`), and get it with `@Inject('log')` or `getObject('log')`; it is a singleton.
31
+ * The lines are buffered and written at the next turn of the event loop (or once 16 KB are pending), and the pending
32
+ * lines are written synchronously when the process exits or the component is destroyed; `sync: true` writes each line
33
+ * at once instead. A stream with a file descriptor (stdout, stderr, an opened file) is written synchronously through
34
+ * it, so the lines keep their order.
35
+ *
36
+ * An HTTP request or response logged as the object of a line is written under `req` or `res` with the values of its
37
+ * credential headers (`Authorization`, `Cookie`, `Set-Cookie`, `Proxy-Authorization`) replaced by `[Redacted]` (see
38
+ * the `redactedHeaders` option). Nothing else is redacted and there are no child loggers: the other properties are
39
+ * written as is, so log the fields you need rather than secrets or whole objects holding them.
40
+ * @example
41
+ * ```typescript
42
+ * import {Application, Component} from 'lakutata'
43
+ * import {Inject} from 'lakutata/decorator/di'
44
+ * import {Logger} from 'lakutata/com/logger'
45
+ *
46
+ * class Orders extends Component {
47
+ * @Inject('log')
48
+ * protected readonly log: Logger
49
+ *
50
+ * public pay(orderId: number, amount: number): void {
51
+ * //{"level":30,...,"orderId":42,"msg":"order 42 paid: 9.90"}
52
+ * this.log.info({orderId: orderId}, 'order %d paid: %s', orderId, amount.toFixed(2))
53
+ * }
54
+ *
55
+ * public fail(error: Error): void {
56
+ * //The error serialized under "err", its message as the message unless one is given
57
+ * this.log.error(error, 'payment failed')
58
+ * }
59
+ * }
60
+ *
61
+ * Application.run(() => ({
62
+ * id: 'shop.app',
63
+ * name: 'Shop',
64
+ * components: {
65
+ * //The preset logger: JSON lines from the info level in production
66
+ * log: {level: 'info', pretty: process.env.NODE_ENV !== 'production'},
67
+ * orders: {class: Orders}
68
+ * }
69
+ * }))
70
+ * ```
71
+ */
72
+ let Logger = class Logger extends Component {
73
+ #threshold;
74
+ #bindings;
75
+ #pretty;
76
+ #outputs;
77
+ #redacted;
78
+ /**
79
+ * Set up the destinations and the level, and register the logger to flush its pending lines when the process exits.
80
+ * @protected
81
+ */
82
+ async init() {
83
+ this.#threshold = LEVELS[this.level];
84
+ this.#bindings = logBindings(Application.appName);
85
+ this.#redacted = new Set(this.redactedHeaders.map((name) => name.toLowerCase()));
86
+ this.#outputs = this.destinations.map((stream) => ({
87
+ destination: new Destination(stream, this.sync),
88
+ palette: this.pretty ? (stream === process.stdout && this.colorize ? COLOR_PALETTE : PLAIN_PALETTE) : undefined
89
+ }));
90
+ this.#pretty = this.#outputs.some((output) => !!output.palette);
91
+ activeLoggers.add(this);
92
+ }
93
+ /**
94
+ * Write the pending lines and stop flushing at the process exit.
95
+ * @protected
96
+ */
97
+ async destroy() {
98
+ activeLoggers.delete(this);
99
+ this.flush();
100
+ }
101
+ /**
102
+ * Build a log line and write it to every destination, unless its level is below the configured `level`.
103
+ * @param level The numeric level of the line: 10 trace, 20 debug, 30 info, 40 warn, 50 error, 60 fatal.
104
+ * @param args The arguments of the log method: `[obj, msg, ...args]` or `[msg, ...args]`.
105
+ * @protected
106
+ */
107
+ write(level, args) {
108
+ if (level < this.#threshold)
109
+ return;
110
+ const record = buildRecord(level, this.#bindings, args, this.#pretty, this.#redacted);
111
+ let prettyLines;
112
+ for (const output of this.#outputs) {
113
+ if (!output.palette) {
114
+ output.destination.write(record.line);
115
+ continue;
116
+ }
117
+ prettyLines ??= new Map();
118
+ let line = prettyLines.get(output.palette);
119
+ if (line === undefined) {
120
+ //The parsed line is changed by the formatting, it is used once
121
+ line = pretty(record.line.slice(0, -1), output.palette, prettyLines.size ? undefined : record.log);
122
+ prettyLines.set(output.palette, line);
123
+ }
124
+ output.destination.write(line);
125
+ }
126
+ }
127
+ /**
128
+ * Write the buffered lines now, synchronously for the streams with a file descriptor (before a `process.exit()`
129
+ * in a signal handler, for instance). Does nothing with `sync: true`.
130
+ */
131
+ flush() {
132
+ this.#outputs?.forEach((output) => output.destination.flush());
133
+ }
134
+ fatal(obj, ...msg) {
135
+ this.write(60, [obj, ...msg]);
136
+ //The process may end without a turn of the event loop
137
+ this.flush();
138
+ }
139
+ error(obj, ...msg) {
140
+ return this.write(50, [obj, ...msg]);
141
+ }
142
+ warn(obj, ...msg) {
143
+ return this.write(40, [obj, ...msg]);
144
+ }
145
+ info(obj, ...msg) {
146
+ return this.write(30, [obj, ...msg]);
147
+ }
148
+ debug(obj, ...msg) {
149
+ return this.write(20, [obj, ...msg]);
150
+ }
151
+ trace(obj, ...msg) {
152
+ return this.write(10, [obj, ...msg]);
153
+ }
154
+ };
155
+ __decorate([
156
+ Configurable(DTO.Boolean().optional().default(true)),
157
+ __metadata("design:type", Boolean)
158
+ ], Logger.prototype, "pretty", void 0);
159
+ __decorate([
160
+ Configurable(DTO.String().valid(...Object.keys(LEVELS)).optional().default('trace')),
161
+ __metadata("design:type", String)
162
+ ], Logger.prototype, "level", void 0);
163
+ __decorate([
164
+ Configurable(DTO.Boolean().optional().default(true)),
165
+ __metadata("design:type", Boolean)
166
+ ], Logger.prototype, "colorize", void 0);
167
+ __decorate([
168
+ Configurable(DTO.Boolean().optional().default(false)),
169
+ __metadata("design:type", Boolean)
170
+ ], Logger.prototype, "sync", void 0);
171
+ __decorate([
172
+ Configurable(DTO.Array(DTO.InstanceOf(Stream)).optional().default(() => [process.stdout])),
173
+ __metadata("design:type", Array)
174
+ ], Logger.prototype, "destinations", void 0);
175
+ __decorate([
176
+ Configurable(DTO.Array(DTO.String()).optional().default(() => [...REDACTED_HEADERS])),
177
+ __metadata("design:type", Array)
178
+ ], Logger.prototype, "redactedHeaders", void 0);
179
+ Logger = __decorate([
180
+ Singleton()
181
+ ], Logger);
182
+ export { Logger };
@@ -0,0 +1 @@
1
+ export * from '../Logger.js';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Replace the close sequences of a text by the open sequence, so a color is not closed early by a nested one
3
+ * @param text
4
+ * @param index
5
+ * @param close
6
+ * @param open
7
+ */
8
+ function replaceClose(text, index, close, open) {
9
+ let result = '';
10
+ let tail = text;
11
+ while (index >= 0) {
12
+ result += tail.substring(0, index) + open;
13
+ tail = tail.substring(index + close.length);
14
+ index = tail.indexOf(close);
15
+ }
16
+ return result + tail;
17
+ }
18
+ /**
19
+ * An ANSI color
20
+ * @param open
21
+ * @param close
22
+ */
23
+ function color(open, close) {
24
+ const openSequence = `\x1b[${open}m`;
25
+ const closeSequence = `\x1b[${close}m`;
26
+ return (input) => {
27
+ //Empty texts stay uncolored
28
+ if (input === '' || input === undefined)
29
+ return '';
30
+ const text = `${input}`;
31
+ const index = text.indexOf(closeSequence, openSequence.length + 1);
32
+ return index < 0 ? openSequence + text + closeSequence : openSequence + replaceClose(text, index, closeSequence, openSequence) + closeSequence;
33
+ };
34
+ }
35
+ const plain = (input) => input;
36
+ export const PLAIN_PALETTE = {
37
+ level: { default: plain, 60: plain, 50: plain, 40: plain, 30: plain, 20: plain, 10: plain },
38
+ message: plain,
39
+ property: plain
40
+ };
41
+ export const COLOR_PALETTE = {
42
+ level: { default: color(37, 39), 60: color(41, 49), 50: color(31, 39), 40: color(33, 39), 30: color(32, 39), 20: color(34, 39), 10: color(90, 39) },
43
+ message: color(36, 39),
44
+ property: color(35, 39)
45
+ };
@@ -0,0 +1,99 @@
1
+ import { writeSync } from 'node:fs';
2
+ //The buffered text written at once
3
+ const HIGH_WATER_MARK = 16384;
4
+ /**
5
+ * The array the writes wait on while a pipe is full, created when first needed: SharedArrayBuffer is missing where the
6
+ * logger may be loaded without logging (a renderer of Electron which is not cross-origin isolated)
7
+ */
8
+ let sleeper;
9
+ /**
10
+ * Wait synchronously
11
+ * @param ms
12
+ */
13
+ function sleepSync(ms) {
14
+ if (typeof SharedArrayBuffer === 'function') {
15
+ sleeper ??= new Int32Array(new SharedArrayBuffer(4));
16
+ Atomics.wait(sleeper, 0, 0, ms);
17
+ return;
18
+ }
19
+ const until = Date.now() + ms;
20
+ while (Date.now() < until) {
21
+ //Busy wait, without SharedArrayBuffer
22
+ }
23
+ }
24
+ /**
25
+ * A destination of the log lines, buffered unless synchronous
26
+ * The buffered lines are written in the next event loop iteration, and synchronously when the process exits
27
+ * The streams with a file descriptor (stdout, stderr, files) are written through it, so the lines stay in order
28
+ */
29
+ export class Destination {
30
+ #buffer = '';
31
+ #scheduled = false;
32
+ constructor(stream, sync) {
33
+ this.stream = stream;
34
+ this.sync = sync;
35
+ }
36
+ /**
37
+ * The file descriptor of the stream, if it has one
38
+ * @protected
39
+ */
40
+ get fd() {
41
+ const fd = this.stream.fd;
42
+ return typeof fd === 'number' ? fd : undefined;
43
+ }
44
+ /**
45
+ * Write a text to the file descriptor, retrying when the descriptor is busy
46
+ * @param fd
47
+ * @param text
48
+ * @protected
49
+ */
50
+ writeFd(fd, text) {
51
+ let data = Buffer.from(text);
52
+ while (data.length) {
53
+ try {
54
+ data = data.subarray(writeSync(fd, data));
55
+ }
56
+ catch (e) {
57
+ if (e.code !== 'EAGAIN')
58
+ throw e;
59
+ //A pipe is full, wait for its reader
60
+ sleepSync(10);
61
+ }
62
+ }
63
+ }
64
+ /**
65
+ * Write a text
66
+ * @param text
67
+ */
68
+ write(text) {
69
+ if (this.sync)
70
+ return this.output(text);
71
+ this.#buffer += text;
72
+ if (this.#buffer.length >= HIGH_WATER_MARK)
73
+ return this.flush();
74
+ if (this.#scheduled)
75
+ return;
76
+ this.#scheduled = true;
77
+ setImmediate(() => this.flush());
78
+ }
79
+ /**
80
+ * Write a text to the stream, through its file descriptor if it has one
81
+ * @param text
82
+ * @protected
83
+ */
84
+ output(text) {
85
+ const fd = this.fd;
86
+ fd === undefined ? this.stream.write(text) : this.writeFd(fd, text);
87
+ }
88
+ /**
89
+ * Write the buffered text
90
+ */
91
+ flush() {
92
+ this.#scheduled = false;
93
+ if (!this.#buffer)
94
+ return;
95
+ const text = this.#buffer;
96
+ this.#buffer = '';
97
+ this.output(text);
98
+ }
99
+ }
@@ -0,0 +1,103 @@
1
+ //Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
2
+ import { stringify } from './Stringify.js';
3
+ /**
4
+ * Format a log message with its arguments: %s, %d, %f, %i, %o, %O, %j and %%
5
+ * The arguments without placeholder are ignored
6
+ * @param template
7
+ * @param args
8
+ */
9
+ export function format(template, args) {
10
+ if (typeof template === 'object' && template !== null) {
11
+ const count = args.length + 1;
12
+ if (count === 1)
13
+ return template;
14
+ //Serialize the object and the arguments after the first one (the behavior of 2.x)
15
+ const objects = new Array(count);
16
+ objects[0] = stringify(template);
17
+ for (let index = 1; index < count; index++)
18
+ objects[index] = stringify(args[index]);
19
+ return objects.join(' ');
20
+ }
21
+ if (typeof template !== 'string' || !args.length)
22
+ return template;
23
+ let result = '';
24
+ let argument = 0;
25
+ let last = -1;
26
+ const length = template.length;
27
+ for (let index = 0; index < length;) {
28
+ if (template.charCodeAt(index) === 37 && index + 1 < length) {
29
+ last = last > -1 ? last : 0;
30
+ switch (template.charCodeAt(index + 1)) {
31
+ //d, f
32
+ case 100:
33
+ case 102:
34
+ if (argument >= args.length || args[argument] == null)
35
+ break;
36
+ if (last < index)
37
+ result += template.slice(last, index);
38
+ result += Number(args[argument]);
39
+ last = index + 2;
40
+ index++;
41
+ break;
42
+ //i
43
+ case 105:
44
+ if (argument >= args.length || args[argument] == null)
45
+ break;
46
+ if (last < index)
47
+ result += template.slice(last, index);
48
+ result += Math.floor(Number(args[argument]));
49
+ last = index + 2;
50
+ index++;
51
+ break;
52
+ //O, o, j
53
+ case 79:
54
+ case 111:
55
+ case 106: {
56
+ if (argument >= args.length || args[argument] === undefined)
57
+ break;
58
+ if (last < index)
59
+ result += template.slice(last, index);
60
+ const value = args[argument];
61
+ if (typeof value === 'string') {
62
+ result += `'${value}'`;
63
+ }
64
+ else if (typeof value === 'function') {
65
+ result += value.name || '<anonymous>';
66
+ }
67
+ else {
68
+ result += stringify(value);
69
+ }
70
+ last = index + 2;
71
+ index++;
72
+ break;
73
+ }
74
+ //s
75
+ case 115:
76
+ if (argument >= args.length)
77
+ break;
78
+ if (last < index)
79
+ result += template.slice(last, index);
80
+ result += String(args[argument]);
81
+ last = index + 2;
82
+ index++;
83
+ break;
84
+ //%
85
+ case 37:
86
+ if (last < index)
87
+ result += template.slice(last, index);
88
+ result += '%';
89
+ last = index + 2;
90
+ index++;
91
+ argument--;
92
+ break;
93
+ }
94
+ ++argument;
95
+ }
96
+ ++index;
97
+ }
98
+ if (last === -1)
99
+ return template;
100
+ if (last < length)
101
+ result += template.slice(last);
102
+ return result;
103
+ }