@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.
@@ -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<import("./apiTypes").RedirectStatusCode>;
66
+ statusCode?: Nullable<Formula>;
67
67
  index: number;
68
68
  }>>;
69
69
  get isError(): Nullable<{
@@ -42,7 +42,8 @@ export declare enum ApiMethod {
42
42
  HEAD = "HEAD",
43
43
  OPTIONS = "OPTIONS"
44
44
  }
45
- export type RedirectStatusCode = 300 | 301 | 302 | 303 | 304 | 307 | 308;
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<RedirectStatusCode>;
105
+ statusCode?: Nullable<Formula>;
105
106
  index: number;
106
107
  }>>;
107
108
  isError?: Nullable<{
@@ -8,4 +8,7 @@ export var ApiMethod;
8
8
  ApiMethod["HEAD"] = "HEAD";
9
9
  ApiMethod["OPTIONS"] = "OPTIONS";
10
10
  })(ApiMethod || (ApiMethod = {}));
11
+ export const REDIRECT_STATUS_CODES = [
12
+ 300, 301, 302, 303, 304, 307, 308,
13
+ ];
11
14
  //# sourceMappingURL=apiTypes.js.map
@@ -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 += `.${componentName}`;
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;IACjC,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"}
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"}
@@ -1 +1 @@
1
- export declare const measure: (key: string, details: Record<string, unknown>) => (extraDetails?: Record<string, unknown> | undefined) => void;
1
+ export declare const measure: (key: string, details: Record<string, unknown>, type?: "component" | undefined) => (extraDetails?: Record<string, unknown> | undefined) => void;
@@ -1,14 +1,20 @@
1
1
  const globalScope = typeof globalThis !== 'undefined' ? globalThis : window;
2
- globalScope.__nc_measure_max_depth = 10;
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,GAAG,EAAE,CAAA;AACvC,WAAW,CAAC,oBAAoB;IAC9B,OAAO,cAAc,KAAK,WAAW;QACrC,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,KAAK,MAAM,CAAA;AAEnD,WAAW,CAAC,kBAAkB,GAAG,CAAC,OAAO,GAAG,IAAI,EAAE,QAAQ,GAAG,EAAE,EAAE,EAAE;IACjE,IAAI,OAAO,EAAE,CAAC;QACZ,cAAc,CAAC,OAAO,CAAC,cAAc,EAAE,MAAM,CAAC,CAAA;IAChD,CAAC;SAAM,CAAC;QACN,cAAc,CAAC,UAAU,CAAC,cAAc,CAAC,CAAA;IAC3C,CAAC;IACD,WAAW,CAAC,oBAAoB,GAAG,OAAO,CAAA;IAC1C,WAAW,CAAC,sBAAsB,GAAG,QAAQ,CAAA;AAC/C,CAAC,CAAA;AAED,IAAI,YAAY,GAAG,CAAC,CAAA;AACpB,MAAM,KAAK,GAAa,EAAE,CAAA;AAC1B,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAA;AAErB,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAW,EAAE,OAAgC,EAAE,EAAE;IACvE,IAAI,CAAC,WAAW,CAAC,oBAAoB,EAAE,CAAC;QACtC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;IAChC,IAAI,KAAK,CAAC,MAAM,IAAI,WAAW,CAAC,sBAAsB,EAAE,CAAC;QACvD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;IAC/B,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAEf,IAAI,QAAQ,GAAG,KAAK,CAAA;IACpB,OAAO,CAAC,YAAsC,EAAE,EAAE;QAChD,IAAI,QAAQ,EAAE,CAAC;YACb,OAAM;QACR,CAAC;QAED,QAAQ,GAAG,IAAI,CAAA;QACf,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;QAC7B,MAAM,aAAa,GAAG,YAAY;YAChC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,GAAG,YAAY,EAAE;YACjC,CAAC,CAAC,OAAO,CAAA;QAEX,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE;YACvB,KAAK;YACL,GAAG;YACH,MAAM,EAAE;gBACN,QAAQ,EAAE;oBACR,QAAQ,EAAE,aAAa;oBACvB,KAAK,EAAE,oBAAoB;oBAC3B,UAAU,EAAE;wBACV,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;wBAChE,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;wBAC5B,CAAC,eAAe,EAAE,SAAS,CAAC;wBAC5B,CAAC,cAAc,EAAE,YAAY,GAAG,SAAS,CAAC;qBAC3C;oBACD,WAAW,EAAE,GAAG,SAAS,KAAK,GAAG,EAAE;iBACpC;aACF;SACF,CAAC,CAAA;QACF,KAAK,CAAC,GAAG,EAAE,CAAA;IACb,CAAC,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"measure.js","sourceRoot":"","sources":["../../src/utils/measure.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -19,5 +19,5 @@
19
19
  "zod": "4.2.1"
20
20
  },
21
21
  "main": "dist/index.js",
22
- "version": "2.0.2"
22
+ "version": "2.0.4"
23
23
  }
@@ -42,7 +42,11 @@ export enum ApiMethod {
42
42
  OPTIONS = 'OPTIONS',
43
43
  }
44
44
 
45
- export type RedirectStatusCode = 300 | 301 | 302 | 303 | 304 | 307 | 308
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<RedirectStatusCode>
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 += `.${componentName}`
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
+ })
@@ -1,5 +1,9 @@
1
1
  const globalScope: any = typeof globalThis !== 'undefined' ? globalThis : window
2
- globalScope.__nc_measure_max_depth = 10
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 = (key: string, details: Record<string, unknown>) => {
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
+ }