@nordcraft/core 2.0.2 → 2.0.4
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/dist/api/ToddleApiV2.d.ts +1 -1
- package/dist/api/apiTypes.d.ts +3 -2
- package/dist/api/apiTypes.js +3 -0
- package/dist/api/apiTypes.js.map +1 -1
- package/dist/utils/getNodeSelector.js +10 -1
- package/dist/utils/getNodeSelector.js.map +1 -1
- package/dist/utils/measure.d.ts +1 -1
- package/dist/utils/measure.js +18 -4
- package/dist/utils/measure.js.map +1 -1
- package/package.json +1 -1
- package/src/api/apiTypes.ts +6 -2
- package/src/utils/getNodeSelector.test.ts +9 -0
- package/src/utils/getNodeSelector.ts +10 -1
- package/src/utils/measure.test.ts +149 -0
- package/src/utils/measure.ts +23 -4
|
@@ -63,7 +63,7 @@ export declare class ToddleApiV2<Handler> implements ApiRequest {
|
|
|
63
63
|
}>;
|
|
64
64
|
get redirectRules(): Nullable<Record<string, {
|
|
65
65
|
formula: Formula;
|
|
66
|
-
statusCode?: Nullable<
|
|
66
|
+
statusCode?: Nullable<Formula>;
|
|
67
67
|
index: number;
|
|
68
68
|
}>>;
|
|
69
69
|
get isError(): Nullable<{
|
package/dist/api/apiTypes.d.ts
CHANGED
|
@@ -42,7 +42,8 @@ export declare enum ApiMethod {
|
|
|
42
42
|
HEAD = "HEAD",
|
|
43
43
|
OPTIONS = "OPTIONS"
|
|
44
44
|
}
|
|
45
|
-
export
|
|
45
|
+
export declare const REDIRECT_STATUS_CODES: readonly [300, 301, 302, 303, 304, 307, 308];
|
|
46
|
+
export type RedirectStatusCode = (typeof REDIRECT_STATUS_CODES)[number];
|
|
46
47
|
export type ApiParserMode = 'auto' | 'text' | 'json' | 'event-stream' | 'json-stream' | 'blob';
|
|
47
48
|
export interface ApiBase extends NordcraftMetadata {
|
|
48
49
|
url?: Nullable<Formula>;
|
|
@@ -101,7 +102,7 @@ export interface ApiRequest extends ApiBase {
|
|
|
101
102
|
}>;
|
|
102
103
|
redirectRules?: Nullable<Record<string, {
|
|
103
104
|
formula: Formula;
|
|
104
|
-
statusCode?: Nullable<
|
|
105
|
+
statusCode?: Nullable<Formula>;
|
|
105
106
|
index: number;
|
|
106
107
|
}>>;
|
|
107
108
|
isError?: Nullable<{
|
package/dist/api/apiTypes.js
CHANGED
package/dist/api/apiTypes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiTypes.js","sourceRoot":"","sources":["../../src/api/apiTypes.ts"],"names":[],"mappings":"AAkCA,MAAM,CAAN,IAAY,SAQX;AARD,WAAY,SAAS;IACnB,wBAAW,CAAA;IACX,0BAAa,CAAA;IACb,8BAAiB,CAAA;IACjB,wBAAW,CAAA;IACX,4BAAe,CAAA;IACf,0BAAa,CAAA;IACb,gCAAmB,CAAA;AACrB,CAAC,EARW,SAAS,KAAT,SAAS,QAQpB"}
|
|
1
|
+
{"version":3,"file":"apiTypes.js","sourceRoot":"","sources":["../../src/api/apiTypes.ts"],"names":[],"mappings":"AAkCA,MAAM,CAAN,IAAY,SAQX;AARD,WAAY,SAAS;IACnB,wBAAW,CAAA;IACX,0BAAa,CAAA;IACb,8BAAiB,CAAA;IACjB,wBAAW,CAAA;IACX,4BAAe,CAAA;IACf,0BAAa,CAAA;IACb,gCAAmB,CAAA;AACrB,CAAC,EARW,SAAS,KAAT,SAAS,QAQpB;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;CACzB,CAAA"}
|
|
@@ -2,7 +2,10 @@ import { variantSelector } from '../styling/variantSelector';
|
|
|
2
2
|
export function getNodeSelector(path, { componentName, nodeId, variant } = {}) {
|
|
3
3
|
let selector = `[data-id="${path}"]`;
|
|
4
4
|
if (componentName) {
|
|
5
|
-
selector
|
|
5
|
+
// Do not allow classes to start with a number, for example a page named "404" would result in a selector starting with a number which is invalid in CSS.
|
|
6
|
+
selector += startsWithNumber(componentName)
|
|
7
|
+
? `._${componentName}`
|
|
8
|
+
: `.${componentName}`;
|
|
6
9
|
}
|
|
7
10
|
if (nodeId) {
|
|
8
11
|
selector += `\\:${nodeId}`;
|
|
@@ -14,4 +17,10 @@ export function getNodeSelector(path, { componentName, nodeId, variant } = {}) {
|
|
|
14
17
|
}
|
|
15
18
|
return selector;
|
|
16
19
|
}
|
|
20
|
+
function startsWithNumber(str) {
|
|
21
|
+
if (!str)
|
|
22
|
+
return false;
|
|
23
|
+
const code = str.charCodeAt(0);
|
|
24
|
+
return code >= 48 && code <= 57;
|
|
25
|
+
}
|
|
17
26
|
//# sourceMappingURL=getNodeSelector.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getNodeSelector.js","sourceRoot":"","sources":["../../src/utils/getNodeSelector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAqB,MAAM,4BAA4B,CAAA;AAe/E,MAAM,UAAU,eAAe,CAC7B,IAAY,EACZ,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,GAAwB,EAAE;IAE5D,IAAI,QAAQ,GAAG,aAAa,IAAI,IAAI,CAAA;IACpC,IAAI,aAAa,EAAE,CAAC;QAClB,QAAQ,IAAI,IAAI,aAAa,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"getNodeSelector.js","sourceRoot":"","sources":["../../src/utils/getNodeSelector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAqB,MAAM,4BAA4B,CAAA;AAe/E,MAAM,UAAU,eAAe,CAC7B,IAAY,EACZ,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,GAAwB,EAAE;IAE5D,IAAI,QAAQ,GAAG,aAAa,IAAI,IAAI,CAAA;IACpC,IAAI,aAAa,EAAE,CAAC;QAClB,yJAAyJ;QACzJ,QAAQ,IAAI,gBAAgB,CAAC,aAAa,CAAC;YACzC,CAAC,CAAC,KAAK,aAAa,EAAE;YACtB,CAAC,CAAC,IAAI,aAAa,EAAE,CAAA;IACzB,CAAC;IACD,IAAI,MAAM,EAAE,CAAC;QACX,QAAQ,IAAI,MAAM,MAAM,EAAE,CAAA;IAC5B,CAAC;IACD,iFAAiF;IACjF,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE,OAAO,CAAC,CAAA;IACpD,IAAI,OAAO,EAAE,CAAC;QACZ,QAAQ,IAAI,eAAe,CAAC,OAAO,CAAC,CAAA;IACtC,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAA;IACtB,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAC9B,OAAO,IAAI,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,CAAA;AACjC,CAAC"}
|
package/dist/utils/measure.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const measure: (key: string, details: Record<string, unknown
|
|
1
|
+
export declare const measure: (key: string, details: Record<string, unknown>, type?: "component" | undefined) => (extraDetails?: Record<string, unknown> | undefined) => void;
|
package/dist/utils/measure.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
const globalScope = typeof globalThis !== 'undefined' ? globalThis : window;
|
|
2
|
-
globalScope.__nc_measure_max_depth =
|
|
2
|
+
globalScope.__nc_measure_max_depth =
|
|
3
|
+
typeof sessionStorage !== 'undefined' &&
|
|
4
|
+
sessionStorage.getItem('__nc_measure_max_depth')
|
|
5
|
+
? parseInt(sessionStorage.getItem('__nc_measure_max_depth'))
|
|
6
|
+
: 10;
|
|
3
7
|
globalScope.__nc_measure_enabled =
|
|
4
8
|
typeof sessionStorage !== 'undefined' &&
|
|
5
9
|
sessionStorage.getItem('__nc_measure') === 'true';
|
|
6
10
|
globalScope.__nc_enableMeasure = (enabled = true, maxDepth = 10) => {
|
|
7
11
|
if (enabled) {
|
|
8
12
|
sessionStorage.setItem('__nc_measure', 'true');
|
|
13
|
+
sessionStorage.setItem('__nc_measure_max_depth', String(maxDepth));
|
|
9
14
|
}
|
|
10
15
|
else {
|
|
11
16
|
sessionStorage.removeItem('__nc_measure');
|
|
17
|
+
sessionStorage.removeItem('__nc_measure_max_depth');
|
|
12
18
|
}
|
|
13
19
|
globalScope.__nc_measure_enabled = enabled;
|
|
14
20
|
globalScope.__nc_measure_max_depth = maxDepth;
|
|
@@ -16,15 +22,19 @@ globalScope.__nc_enableMeasure = (enabled = true, maxDepth = 10) => {
|
|
|
16
22
|
let measureCount = 0;
|
|
17
23
|
const STACK = [];
|
|
18
24
|
const NOOP = () => { };
|
|
19
|
-
export const measure = (key, details) => {
|
|
25
|
+
export const measure = (key, details, type) => {
|
|
20
26
|
if (!globalScope.__nc_measure_enabled) {
|
|
21
27
|
return NOOP;
|
|
22
28
|
}
|
|
23
29
|
const selfIndex = measureCount++;
|
|
30
|
+
const selfStackSize = STACK.length;
|
|
31
|
+
if (selfStackSize >= globalScope.__nc_measure_max_depth) {
|
|
32
|
+
return NOOP;
|
|
33
|
+
}
|
|
24
34
|
if (STACK.length >= globalScope.__nc_measure_max_depth) {
|
|
25
35
|
return NOOP;
|
|
26
36
|
}
|
|
27
|
-
const start = performance.now();
|
|
37
|
+
const start = performance.now() + selfStackSize * 0.001;
|
|
28
38
|
STACK.push(key);
|
|
29
39
|
let _stopped = false;
|
|
30
40
|
return (extraDetails) => {
|
|
@@ -32,7 +42,7 @@ export const measure = (key, details) => {
|
|
|
32
42
|
return;
|
|
33
43
|
}
|
|
34
44
|
_stopped = true;
|
|
35
|
-
const end = performance.now();
|
|
45
|
+
const end = performance.now() + selfStackSize * 0.001;
|
|
36
46
|
const mergedDetails = extraDetails
|
|
37
47
|
? { ...details, ...extraDetails }
|
|
38
48
|
: details;
|
|
@@ -43,6 +53,7 @@ export const measure = (key, details) => {
|
|
|
43
53
|
devtools: {
|
|
44
54
|
dataType: 'track-entry',
|
|
45
55
|
track: 'Nordcraft devtools',
|
|
56
|
+
color: COLOR_MAP[type],
|
|
46
57
|
properties: [
|
|
47
58
|
...Object.entries(mergedDetails).map(([k, v]) => [k, String(v)]),
|
|
48
59
|
['Stack', STACK.join(' > ')],
|
|
@@ -56,4 +67,7 @@ export const measure = (key, details) => {
|
|
|
56
67
|
STACK.pop();
|
|
57
68
|
};
|
|
58
69
|
};
|
|
70
|
+
const COLOR_MAP = {
|
|
71
|
+
component: 'secondary',
|
|
72
|
+
};
|
|
59
73
|
//# sourceMappingURL=measure.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"measure.js","sourceRoot":"","sources":["../../src/utils/measure.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,GAAQ,OAAO,UAAU,KAAK,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAA;AAChF,WAAW,CAAC,sBAAsB,
|
|
1
|
+
{"version":3,"file":"measure.js","sourceRoot":"","sources":["../../src/utils/measure.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/api/apiTypes.ts
CHANGED
|
@@ -42,7 +42,11 @@ export enum ApiMethod {
|
|
|
42
42
|
OPTIONS = 'OPTIONS',
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export
|
|
45
|
+
export const REDIRECT_STATUS_CODES = [
|
|
46
|
+
300, 301, 302, 303, 304, 307, 308,
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
export type RedirectStatusCode = (typeof REDIRECT_STATUS_CODES)[number]
|
|
46
50
|
|
|
47
51
|
export type ApiParserMode =
|
|
48
52
|
| 'auto'
|
|
@@ -122,7 +126,7 @@ export interface ApiRequest extends ApiBase {
|
|
|
122
126
|
// A redirect response will be returned if the formula returns a valid url
|
|
123
127
|
formula: Formula
|
|
124
128
|
// The status code used in the redirect response. Only relevant server side
|
|
125
|
-
statusCode?: Nullable<
|
|
129
|
+
statusCode?: Nullable<Formula>
|
|
126
130
|
index: number
|
|
127
131
|
}
|
|
128
132
|
>
|
|
@@ -54,4 +54,13 @@ describe('getNodeSelector', () => {
|
|
|
54
54
|
const selector = getNodeSelector(path)
|
|
55
55
|
expect(selector).toBe('[data-id="0.1\\/2"]')
|
|
56
56
|
})
|
|
57
|
+
|
|
58
|
+
test('should prefix selector with an underscore if it starts with a number', () => {
|
|
59
|
+
const path = '0.1\\/2'
|
|
60
|
+
const selector = getNodeSelector(path, {
|
|
61
|
+
componentName: '404',
|
|
62
|
+
nodeId: 'test-node',
|
|
63
|
+
})
|
|
64
|
+
expect(selector).toBe('[data-id="0.1\\/2"]._404\\:test-node')
|
|
65
|
+
})
|
|
57
66
|
})
|
|
@@ -19,7 +19,10 @@ export function getNodeSelector(
|
|
|
19
19
|
): string {
|
|
20
20
|
let selector = `[data-id="${path}"]`
|
|
21
21
|
if (componentName) {
|
|
22
|
-
selector
|
|
22
|
+
// Do not allow classes to start with a number, for example a page named "404" would result in a selector starting with a number which is invalid in CSS.
|
|
23
|
+
selector += startsWithNumber(componentName)
|
|
24
|
+
? `._${componentName}`
|
|
25
|
+
: `.${componentName}`
|
|
23
26
|
}
|
|
24
27
|
if (nodeId) {
|
|
25
28
|
selector += `\\:${nodeId}`
|
|
@@ -32,3 +35,9 @@ export function getNodeSelector(
|
|
|
32
35
|
|
|
33
36
|
return selector
|
|
34
37
|
}
|
|
38
|
+
|
|
39
|
+
function startsWithNumber(str: string): boolean {
|
|
40
|
+
if (!str) return false
|
|
41
|
+
const code = str.charCodeAt(0)
|
|
42
|
+
return code >= 48 && code <= 57
|
|
43
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
|
|
2
|
+
import { measure } from './measure'
|
|
3
|
+
|
|
4
|
+
const globalScope = globalThis as any
|
|
5
|
+
|
|
6
|
+
describe('measure', () => {
|
|
7
|
+
let originalPerformanceMeasure: any
|
|
8
|
+
let originalPerformanceNow: any
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Reset global state
|
|
12
|
+
globalScope.__nc_measure_enabled = false
|
|
13
|
+
globalScope.__nc_measure_max_depth = 10
|
|
14
|
+
|
|
15
|
+
// Mock performance
|
|
16
|
+
originalPerformanceMeasure = performance.measure
|
|
17
|
+
originalPerformanceNow = performance.now
|
|
18
|
+
performance.measure = () => ({}) as any
|
|
19
|
+
performance.now = () => 0
|
|
20
|
+
|
|
21
|
+
// Mock sessionStorage
|
|
22
|
+
globalScope.sessionStorage = {
|
|
23
|
+
getItem: () => null,
|
|
24
|
+
setItem: () => {},
|
|
25
|
+
removeItem: () => {},
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
performance.measure = originalPerformanceMeasure
|
|
31
|
+
performance.now = originalPerformanceNow
|
|
32
|
+
delete globalScope.sessionStorage
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('should do nothing if NOT enabled', () => {
|
|
36
|
+
const measureSpy = spyOn(performance, 'measure')
|
|
37
|
+
const key = 'test-key'
|
|
38
|
+
const stop = measure(key, { arg: 1 })
|
|
39
|
+
|
|
40
|
+
expect(stop).toBeDefined()
|
|
41
|
+
stop()
|
|
42
|
+
|
|
43
|
+
expect(measureSpy).not.toHaveBeenCalled()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('should call performance.measure when enabled', () => {
|
|
47
|
+
globalScope.__nc_measure_enabled = true
|
|
48
|
+
const measureSpy = spyOn(performance, 'measure')
|
|
49
|
+
|
|
50
|
+
let now = 0
|
|
51
|
+
performance.now = () => now++
|
|
52
|
+
|
|
53
|
+
const key = 'test-performance'
|
|
54
|
+
const stop = measure(key, { arg: 1 })
|
|
55
|
+
|
|
56
|
+
stop({ extra: 2 })
|
|
57
|
+
|
|
58
|
+
expect(measureSpy).toHaveBeenCalledWith(
|
|
59
|
+
key,
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
start: 0,
|
|
62
|
+
end: 1,
|
|
63
|
+
detail: expect.objectContaining({
|
|
64
|
+
devtools: expect.objectContaining({
|
|
65
|
+
track: 'Nordcraft devtools',
|
|
66
|
+
tooltipText: expect.stringContaining(key),
|
|
67
|
+
}),
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('should merge details and extraDetails', () => {
|
|
74
|
+
globalScope.__nc_measure_enabled = true
|
|
75
|
+
const measureSpy = spyOn(performance, 'measure')
|
|
76
|
+
|
|
77
|
+
const stop = measure('key', { original: 'value' })
|
|
78
|
+
stop({ extra: 'value' })
|
|
79
|
+
|
|
80
|
+
const lastCall = measureSpy.mock.calls[0]
|
|
81
|
+
const properties = lastCall[1].detail.devtools.properties
|
|
82
|
+
|
|
83
|
+
const propsMap = new Map(properties)
|
|
84
|
+
expect(propsMap.get('original')).toBe('value')
|
|
85
|
+
expect(propsMap.get('extra')).toBe('value')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('should respect max depth', () => {
|
|
89
|
+
globalScope.__nc_measure_enabled = true
|
|
90
|
+
globalScope.__nc_measure_max_depth = 2
|
|
91
|
+
const measureSpy = spyOn(performance, 'measure')
|
|
92
|
+
|
|
93
|
+
const stop1 = measure('depth-1', {})
|
|
94
|
+
const stop2 = measure('depth-2', {})
|
|
95
|
+
const stop3 = measure('depth-3', {}) // Should be NOOP
|
|
96
|
+
|
|
97
|
+
stop3()
|
|
98
|
+
expect(measureSpy).not.toHaveBeenCalled()
|
|
99
|
+
|
|
100
|
+
stop2()
|
|
101
|
+
expect(measureSpy).toHaveBeenCalledTimes(1)
|
|
102
|
+
|
|
103
|
+
stop1()
|
|
104
|
+
expect(measureSpy).toHaveBeenCalledTimes(2)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('should handle nested measures and display stack', () => {
|
|
108
|
+
globalScope.__nc_measure_enabled = true
|
|
109
|
+
const measureSpy = spyOn(performance, 'measure')
|
|
110
|
+
|
|
111
|
+
const stop1 = measure('parent', {})
|
|
112
|
+
const stop2 = measure('child', {})
|
|
113
|
+
|
|
114
|
+
stop2()
|
|
115
|
+
const childCall = measureSpy.mock.calls[0]
|
|
116
|
+
const childStack = new Map(childCall[1].detail.devtools.properties).get(
|
|
117
|
+
'Stack',
|
|
118
|
+
)
|
|
119
|
+
expect(childStack).toBe('parent > child')
|
|
120
|
+
|
|
121
|
+
stop1()
|
|
122
|
+
const parentCall = measureSpy.mock.calls[1]
|
|
123
|
+
const parentStack = new Map(parentCall[1].detail.devtools.properties).get(
|
|
124
|
+
'Stack',
|
|
125
|
+
)
|
|
126
|
+
expect(parentStack).toBe('parent')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('should only call finish once', () => {
|
|
130
|
+
globalScope.__nc_measure_enabled = true
|
|
131
|
+
const measureSpy = spyOn(performance, 'measure')
|
|
132
|
+
|
|
133
|
+
const stop = measure('once', {})
|
|
134
|
+
stop()
|
|
135
|
+
stop()
|
|
136
|
+
|
|
137
|
+
expect(measureSpy).toHaveBeenCalledTimes(1)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('global __nc_enableMeasure should update state', () => {
|
|
141
|
+
// We need to trigger the globalScope mock logic if we want to test the helper function
|
|
142
|
+
// But since it's defined in the module scope of measure.ts, it might already be there
|
|
143
|
+
if (typeof globalScope.__nc_enableMeasure === 'function') {
|
|
144
|
+
globalScope.__nc_enableMeasure(true, 5)
|
|
145
|
+
expect(globalScope.__nc_measure_enabled).toBe(true)
|
|
146
|
+
expect(globalScope.__nc_measure_max_depth).toBe(5)
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
})
|
package/src/utils/measure.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const globalScope: any = typeof globalThis !== 'undefined' ? globalThis : window
|
|
2
|
-
globalScope.__nc_measure_max_depth =
|
|
2
|
+
globalScope.__nc_measure_max_depth =
|
|
3
|
+
typeof sessionStorage !== 'undefined' &&
|
|
4
|
+
sessionStorage.getItem('__nc_measure_max_depth')
|
|
5
|
+
? parseInt(sessionStorage.getItem('__nc_measure_max_depth')!)
|
|
6
|
+
: 10
|
|
3
7
|
globalScope.__nc_measure_enabled =
|
|
4
8
|
typeof sessionStorage !== 'undefined' &&
|
|
5
9
|
sessionStorage.getItem('__nc_measure') === 'true'
|
|
@@ -7,8 +11,10 @@ globalScope.__nc_measure_enabled =
|
|
|
7
11
|
globalScope.__nc_enableMeasure = (enabled = true, maxDepth = 10) => {
|
|
8
12
|
if (enabled) {
|
|
9
13
|
sessionStorage.setItem('__nc_measure', 'true')
|
|
14
|
+
sessionStorage.setItem('__nc_measure_max_depth', String(maxDepth))
|
|
10
15
|
} else {
|
|
11
16
|
sessionStorage.removeItem('__nc_measure')
|
|
17
|
+
sessionStorage.removeItem('__nc_measure_max_depth')
|
|
12
18
|
}
|
|
13
19
|
globalScope.__nc_measure_enabled = enabled
|
|
14
20
|
globalScope.__nc_measure_max_depth = maxDepth
|
|
@@ -18,17 +24,25 @@ let measureCount = 0
|
|
|
18
24
|
const STACK: string[] = []
|
|
19
25
|
const NOOP = () => {}
|
|
20
26
|
|
|
21
|
-
export const measure = (
|
|
27
|
+
export const measure = (
|
|
28
|
+
key: string,
|
|
29
|
+
details: Record<string, unknown>,
|
|
30
|
+
type?: 'component',
|
|
31
|
+
) => {
|
|
22
32
|
if (!globalScope.__nc_measure_enabled) {
|
|
23
33
|
return NOOP
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
const selfIndex = measureCount++
|
|
37
|
+
const selfStackSize = STACK.length
|
|
38
|
+
if (selfStackSize >= globalScope.__nc_measure_max_depth) {
|
|
39
|
+
return NOOP
|
|
40
|
+
}
|
|
27
41
|
if (STACK.length >= globalScope.__nc_measure_max_depth) {
|
|
28
42
|
return NOOP
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
const start = performance.now()
|
|
45
|
+
const start = performance.now() + selfStackSize * 0.001
|
|
32
46
|
STACK.push(key)
|
|
33
47
|
|
|
34
48
|
let _stopped = false
|
|
@@ -38,7 +52,7 @@ export const measure = (key: string, details: Record<string, unknown>) => {
|
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
_stopped = true
|
|
41
|
-
const end = performance.now()
|
|
55
|
+
const end = performance.now() + selfStackSize * 0.001
|
|
42
56
|
const mergedDetails = extraDetails
|
|
43
57
|
? { ...details, ...extraDetails }
|
|
44
58
|
: details
|
|
@@ -50,6 +64,7 @@ export const measure = (key: string, details: Record<string, unknown>) => {
|
|
|
50
64
|
devtools: {
|
|
51
65
|
dataType: 'track-entry',
|
|
52
66
|
track: 'Nordcraft devtools',
|
|
67
|
+
color: COLOR_MAP[type as keyof typeof COLOR_MAP],
|
|
53
68
|
properties: [
|
|
54
69
|
...Object.entries(mergedDetails).map(([k, v]) => [k, String(v)]),
|
|
55
70
|
['Stack', STACK.join(' > ')],
|
|
@@ -63,3 +78,7 @@ export const measure = (key: string, details: Record<string, unknown>) => {
|
|
|
63
78
|
STACK.pop()
|
|
64
79
|
}
|
|
65
80
|
}
|
|
81
|
+
|
|
82
|
+
const COLOR_MAP = {
|
|
83
|
+
component: 'secondary',
|
|
84
|
+
}
|