@nordcraft/core 2.0.3 → 2.0.5

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.
@@ -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.3"
22
+ "version": "2.0.5"
23
23
  }
@@ -279,6 +279,11 @@ export class ToddleApiV2<Handler> implements ApiRequest {
279
279
  globalFormulas: this.globalFormulas,
280
280
  path: ['apis', apiKey, 'redirectRules', rule, 'formula'],
281
281
  })
282
+ yield* getFormulasInFormula({
283
+ formula: value.statusCode,
284
+ globalFormulas: this.globalFormulas,
285
+ path: ['apis', apiKey, 'redirectRules', rule, 'statusCode'],
286
+ })
282
287
  }
283
288
  yield* getFormulasInFormula({
284
289
  formula: api.isError?.formula,
@@ -398,27 +398,31 @@ export class ToddleComponent<Handler> {
398
398
  })
399
399
  }
400
400
  for (const [formulaKey, formula] of Object.entries(this.formulas ?? {})) {
401
- yield* getFormulasInFormula({
402
- formula: formula.formula,
403
- globalFormulas,
404
- path: ['formulas', formulaKey, 'formula'],
405
- packageName,
406
- })
401
+ if (isDefined(formula)) {
402
+ yield* getFormulasInFormula({
403
+ formula: formula.formula,
404
+ globalFormulas,
405
+ path: ['formulas', formulaKey, 'formula'],
406
+ packageName,
407
+ })
408
+ }
407
409
  }
408
410
  for (const [variableKey, variable] of Object.entries(
409
411
  this.variables ?? {},
410
412
  )) {
411
- yield* getFormulasInFormula({
412
- formula: variable.initialValue,
413
- globalFormulas,
414
- path: ['variables', variableKey, 'initialValue'],
415
- packageName,
416
- })
413
+ if (isDefined(variable)) {
414
+ yield* getFormulasInFormula({
415
+ formula: variable.initialValue,
416
+ globalFormulas,
417
+ path: ['variables', variableKey, 'initialValue'],
418
+ packageName,
419
+ })
420
+ }
417
421
  }
418
422
  for (const [workflowKey, workflow] of Object.entries(
419
423
  this.workflows ?? {},
420
424
  )) {
421
- for (const [actionKey, action] of workflow.actions.entries()) {
425
+ for (const [actionKey, action] of workflow?.actions.entries() ?? []) {
422
426
  yield* getFormulasInAction({
423
427
  action,
424
428
  globalFormulas,
@@ -93,15 +93,17 @@ export interface ElementNodeModel {
93
93
  repeat?: Nullable<Formula>
94
94
  repeatKey?: Nullable<Formula>
95
95
  tag: string
96
- attrs: Partial<Record<string, Formula>>
96
+ attrs?: Nullable<Partial<Record<string, Formula>>>
97
97
  style?: Nullable<NodeStyleModel>
98
98
  variants?: Nullable<StyleVariant[]>
99
99
  animations?: Nullable<Record<string, Record<string, AnimationKeyframe>>>
100
- children: string[]
101
- events: Partial<Record<string, Nullable<EventModel>>>
100
+ children?: Nullable<string[]>
101
+ events?: Nullable<Partial<Record<string, Nullable<EventModel>>>>
102
102
  classes?: Nullable<Record<string, { formula?: Nullable<Formula> }>>
103
103
  'style-variables'?: Nullable<Array<StyleVariable>>
104
104
  customProperties?: Nullable<Record<CustomPropertyName, CustomProperty>>
105
+ // Legacy implementations added an invalid property. We'll keep it here to know we can safely remove it
106
+ styleVariables?: never
105
107
  }
106
108
 
107
109
  export interface ComponentNodeModel {
@@ -117,9 +119,9 @@ export interface ComponentNodeModel {
117
119
  style?: Nullable<NodeStyleModel>
118
120
  variants?: Nullable<StyleVariant[]>
119
121
  animations?: Nullable<Record<string, Record<string, AnimationKeyframe>>>
120
- attrs: Record<string, Formula>
121
- children: string[]
122
- events: Record<string, EventModel>
122
+ attrs?: Nullable<Record<string, Formula>>
123
+ children?: Nullable<string[]>
124
+ events?: Nullable<Partial<Record<string, Nullable<EventModel>>>>
123
125
  customProperties?: Nullable<Record<CustomPropertyName, CustomProperty>>
124
126
  }
125
127
 
@@ -130,7 +132,9 @@ export interface SlotNodeModel {
130
132
  condition?: Nullable<Formula>
131
133
  repeat?: Nullable<never>
132
134
  repeatKey?: Nullable<never>
133
- children: string[]
135
+ children?: Nullable<string[]>
136
+ // slots cannot have events, but an empty object might have been declared regardless
137
+ events?: never
134
138
  }
135
139
  export type NodeModel =
136
140
  | TextNodeModel
@@ -180,9 +184,9 @@ export interface Component {
180
184
  // @deprecated - use route->path instead
181
185
  page?: Nullable<string> // page url /projects/:id - only for pages
182
186
  route?: Nullable<PageRoute>
183
- attributes?: Nullable<Record<string, ComponentAttribute>>
184
- variables?: Nullable<Record<string, ComponentVariable>>
185
- formulas?: Nullable<Record<string, ComponentFormula>>
187
+ attributes?: Nullable<Record<string, Nullable<ComponentAttribute>>>
188
+ variables?: Nullable<Record<string, Nullable<ComponentVariable>>>
189
+ formulas?: Nullable<Record<string, Nullable<ComponentFormula>>>
186
190
  contexts?: Nullable<
187
191
  Record<
188
192
  // `componentName` or `packageName/componentName` if the context comes from a different package than the component itself
@@ -190,10 +194,10 @@ export interface Component {
190
194
  ComponentContext
191
195
  >
192
196
  >
193
- workflows?: Nullable<Record<string, ComponentWorkflow>>
197
+ workflows?: Nullable<Record<string, Nullable<ComponentWorkflow>>>
194
198
  apis?: Nullable<Record<string, Nullable<ComponentAPI>>>
195
199
  nodes?: Nullable<Record<string, NodeModel>>
196
- events?: Nullable<ComponentEvent[]>
200
+ events?: Nullable<Nullable<ComponentEvent>[]>
197
201
  onLoad?: Nullable<EventModel>
198
202
  onAttributeChange?: Nullable<EventModel>
199
203
  // exported indicates that a component is exported in a package
@@ -310,7 +314,7 @@ export interface CustomActionModel {
310
314
  package?: Nullable<string>
311
315
  name: string
312
316
  description?: Nullable<string>
313
- group?: Nullable<string>
317
+ group?: Nullable<unknown>
314
318
  data?: Nullable<string | number | boolean | Formula>
315
319
  arguments?: Nullable<Partial<CustomActionArgument[]>>
316
320
  events?: Nullable<Record<string, ActionModelActions>>
@@ -328,12 +332,16 @@ export interface SwitchActionModel {
328
332
  }>
329
333
  >
330
334
  default?: Nullable<ActionModelActions>
335
+ // Should never be set
336
+ arguments?: never
331
337
  }
332
338
 
333
339
  export interface VariableActionModel {
334
340
  type: 'SetVariable'
335
341
  variable: string
336
342
  data: Nullable<Formula>
343
+ // Arguments for this action should never be provided, but have been set sometimes
344
+ arguments?: never
337
345
  }
338
346
 
339
347
  export interface FetchActionModel {
@@ -355,6 +363,8 @@ export interface SetURLParameterAction {
355
363
  parameter: string
356
364
  data?: Nullable<Formula>
357
365
  historyMode?: Nullable<'replace' | 'push'>
366
+ // Should never be set
367
+ arguments?: never
358
368
  }
359
369
 
360
370
  export interface SetMultiUrlParameterAction {
@@ -367,12 +377,14 @@ export interface EventActionModel {
367
377
  type: 'TriggerEvent'
368
378
  event: string
369
379
  data?: Nullable<Formula>
380
+ // Should never be set
381
+ arguments?: never
370
382
  }
371
383
 
372
384
  export interface WorkflowActionModel {
373
385
  type: 'TriggerWorkflow'
374
386
  workflow: string
375
- parameters: Record<string, { formula?: Nullable<Formula> }>
387
+ parameters?: Nullable<Record<string, { formula?: Nullable<Formula> }>>
376
388
  callbacks?: Nullable<
377
389
  Partial<Record<string, { actions?: Nullable<Partial<ActionModel[]>> }>>
378
390
  >
@@ -383,6 +395,8 @@ export interface WorkflowCallbackActionModel {
383
395
  type: 'TriggerWorkflowCallback'
384
396
  event: string
385
397
  data?: Nullable<Formula>
398
+ // Should never be set
399
+ arguments?: never
386
400
  }
387
401
 
388
402
  export type ActionModel =
@@ -209,13 +209,55 @@ export const isToddleFormula = <Handler>(
209
209
 
210
210
  export function applyFormula(
211
211
  formula: Formula | string | number | undefined | null | boolean,
212
- _ctx: FormulaContext,
212
+ ctx: FormulaContext,
213
213
  extendedPath?: Array<string | number> | undefined,
214
214
  ): any {
215
- const path = [...(_ctx?.jsonPath ?? []), ...(extendedPath ?? [])]
216
- const ctx = { ..._ctx, jsonPath: path }
217
- const report = (value: any, p: Array<string | number> = path) => {
218
- ctx?.reportFormulaEvaluation?.(p, value, ctx)
215
+ // Short-circuit when not reporting to avoid unnecessary overhead of creating new objects and function
216
+ if (!ctx.reportFormulaEvaluation) {
217
+ if (!isFormula(formula)) {
218
+ return formula
219
+ }
220
+ try {
221
+ switch (formula.type) {
222
+ case 'value':
223
+ return formula.value
224
+ case 'path':
225
+ return applyPathFormula(formula, ctx.data)
226
+ case 'switch':
227
+ return applySwitchFormula(formula, ctx)
228
+ case 'or':
229
+ return applyOrFormula(formula, ctx)
230
+ case 'and':
231
+ return applyAndFormula(formula, ctx)
232
+ case 'object':
233
+ return applyObjectFormula(formula, ctx)
234
+ case 'record':
235
+ return applyRecordFormula(formula, ctx)
236
+ case 'array':
237
+ return applyArrayFormula(formula, ctx)
238
+ case 'function':
239
+ return applyFunctionFormula(formula, ctx)
240
+ case 'apply':
241
+ return applyApplyFormula(formula, ctx)
242
+ default:
243
+ if (ctx.env?.logErrors) {
244
+ console.error('Could not recognize formula', formula)
245
+ }
246
+ }
247
+ } catch (e) {
248
+ if (ctx.env?.logErrors) {
249
+ console.error(e)
250
+ }
251
+ return null
252
+ }
253
+
254
+ return undefined
255
+ }
256
+
257
+ const jsonPath = [...(ctx.jsonPath ?? []), ...(extendedPath ?? [])]
258
+ const _ctx = { ...ctx, jsonPath }
259
+ const report = (value: any, p: Array<string | number> = jsonPath) => {
260
+ ctx.reportFormulaEvaluation?.(p, value, _ctx)
219
261
  return value
220
262
  }
221
263
 
@@ -228,58 +270,58 @@ export function applyFormula(
228
270
  return report(formula.value)
229
271
  }
230
272
  case 'path': {
231
- return report(applyPathFormula(formula, ctx.data))
273
+ return report(applyPathFormula(formula, _ctx.data))
232
274
  }
233
275
  case 'switch': {
234
276
  if (
235
- ctx.reportFormulaEvaluation &&
236
- ctx.jsonPath.length < MAX_REPORT_DEPTH
277
+ _ctx.reportFormulaEvaluation &&
278
+ _ctx.jsonPath.length < MAX_REPORT_DEPTH
237
279
  ) {
238
- return report(applyEvaluateAllSwitchFormula(formula, ctx))
280
+ return report(applyEvaluateAllSwitchFormula(formula, _ctx))
239
281
  }
240
- return applySwitchFormula(formula, ctx)
282
+ return applySwitchFormula(formula, _ctx)
241
283
  }
242
284
  case 'or': {
243
285
  if (
244
- ctx.reportFormulaEvaluation &&
245
- ctx.jsonPath.length < MAX_REPORT_DEPTH
286
+ _ctx.reportFormulaEvaluation &&
287
+ _ctx.jsonPath.length < MAX_REPORT_DEPTH
246
288
  ) {
247
- return report(applyEvaluateAllOrFormula(formula, ctx))
289
+ return report(applyEvaluateAllOrFormula(formula, _ctx))
248
290
  }
249
- return applyOrFormula(formula, ctx)
291
+ return applyOrFormula(formula, _ctx)
250
292
  }
251
293
  case 'and': {
252
294
  if (
253
- ctx.reportFormulaEvaluation &&
254
- ctx.jsonPath.length < MAX_REPORT_DEPTH
295
+ _ctx.reportFormulaEvaluation &&
296
+ _ctx.jsonPath.length < MAX_REPORT_DEPTH
255
297
  ) {
256
- return report(applyEvaluateAllAndFormula(formula, ctx))
298
+ return report(applyEvaluateAllAndFormula(formula, _ctx))
257
299
  }
258
- return applyAndFormula(formula, ctx)
300
+ return applyAndFormula(formula, _ctx)
259
301
  }
260
302
  case 'object': {
261
- return report(applyObjectFormula(formula, ctx))
303
+ return report(applyObjectFormula(formula, _ctx))
262
304
  }
263
305
  case 'record': {
264
306
  // object used to be called record, there are still examples in the wild.
265
- return report(applyRecordFormula(formula, ctx))
307
+ return report(applyRecordFormula(formula, _ctx))
266
308
  }
267
309
  case 'array': {
268
- return report(applyArrayFormula(formula, ctx))
310
+ return report(applyArrayFormula(formula, _ctx))
269
311
  }
270
312
  case 'function': {
271
- return report(applyFunctionFormula(formula, ctx))
313
+ return report(applyFunctionFormula(formula, _ctx))
272
314
  }
273
315
  case 'apply': {
274
- return report(applyApplyFormula(formula, ctx))
316
+ return report(applyApplyFormula(formula, _ctx))
275
317
  }
276
318
  default:
277
- if (ctx.env?.logErrors) {
319
+ if (_ctx.env?.logErrors) {
278
320
  console.error('Could not recognize formula', formula)
279
321
  }
280
322
  }
281
323
  } catch (e) {
282
- if (ctx.env?.logErrors) {
324
+ if (_ctx.env?.logErrors) {
283
325
  console.error(e)
284
326
  }
285
327
  return report(null)
package/src/types.ts CHANGED
@@ -37,6 +37,12 @@ export type ActionHandler<Args = unknown[]> = (
37
37
  ) => void
38
38
  env: ToddleEnv
39
39
  abortSignal: AbortSignal
40
+ stores?: {
41
+ theme: {
42
+ set: (value: string | null) => void
43
+ get: () => string | null
44
+ }
45
+ }
40
46
  },
41
47
  event?: Nullable<Event>,
42
48
  ) => void
@@ -75,10 +75,13 @@ export const groupBy = <T>(items: T[], f: (t: T) => string) =>
75
75
  return acc
76
76
  }, {})
77
77
 
78
- export const filterObject = <T>(
78
+ export const filterObject = <T, T2 extends T = T>(
79
79
  object: Record<string, T>,
80
80
  f: (kv: [string, T]) => boolean,
81
- ): Record<string, T> => Object.fromEntries(Object.entries(object).filter(f))
81
+ ): Record<string, T2> =>
82
+ Object.fromEntries(
83
+ Object.entries(object).filter((o): o is [string, T2] => f(o)),
84
+ )
82
85
 
83
86
  export function get<T = any>(
84
87
  collection: T,
@@ -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
+ })