@servicenow/sdk-build-core 4.4.1 → 4.6.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 (56) hide show
  1. package/dist/compression.d.ts +2 -1
  2. package/dist/compression.js +5 -2
  3. package/dist/compression.js.map +1 -1
  4. package/dist/keys-registry.d.ts +2 -1
  5. package/dist/keys-registry.js +15 -15
  6. package/dist/keys-registry.js.map +1 -1
  7. package/dist/now-config.d.ts +476 -5
  8. package/dist/now-config.js +165 -1
  9. package/dist/now-config.js.map +1 -1
  10. package/dist/plugins/index.d.ts +1 -0
  11. package/dist/plugins/index.js +1 -0
  12. package/dist/plugins/index.js.map +1 -1
  13. package/dist/plugins/plugin.d.ts +17 -18
  14. package/dist/plugins/plugin.js +52 -31
  15. package/dist/plugins/plugin.js.map +1 -1
  16. package/dist/plugins/post-install.d.ts +31 -0
  17. package/dist/plugins/post-install.js +3 -0
  18. package/dist/plugins/post-install.js.map +1 -0
  19. package/dist/plugins/shape.js +4 -1
  20. package/dist/plugins/shape.js.map +1 -1
  21. package/dist/taxonomy.js +50 -0
  22. package/dist/taxonomy.js.map +1 -1
  23. package/dist/telemetry/clients/abstract-client.d.ts +23 -2
  24. package/dist/telemetry/clients/abstract-client.js +80 -0
  25. package/dist/telemetry/clients/abstract-client.js.map +1 -1
  26. package/dist/telemetry/clients/browser-client.d.ts +15 -9
  27. package/dist/telemetry/clients/browser-client.js +8 -82
  28. package/dist/telemetry/clients/browser-client.js.map +1 -1
  29. package/dist/telemetry/clients/node-client.d.ts +28 -6
  30. package/dist/telemetry/clients/node-client.js +37 -101
  31. package/dist/telemetry/clients/node-client.js.map +1 -1
  32. package/dist/telemetry/factory.js +4 -6
  33. package/dist/telemetry/factory.js.map +1 -1
  34. package/dist/telemetry/types.d.ts +1 -1
  35. package/dist/typescript.d.ts +8 -0
  36. package/dist/typescript.js +12 -1
  37. package/dist/typescript.js.map +1 -1
  38. package/dist/xml.js +3 -1
  39. package/dist/xml.js.map +1 -1
  40. package/now.config.schema.json +386 -2
  41. package/package.json +5 -6
  42. package/src/compression.ts +7 -2
  43. package/src/keys-registry.ts +14 -12
  44. package/src/now-config.ts +204 -1
  45. package/src/plugins/index.ts +1 -0
  46. package/src/plugins/plugin.ts +86 -63
  47. package/src/plugins/post-install.ts +34 -0
  48. package/src/plugins/shape.ts +4 -1
  49. package/src/taxonomy.ts +53 -0
  50. package/src/telemetry/clients/abstract-client.ts +104 -3
  51. package/src/telemetry/clients/browser-client.ts +12 -95
  52. package/src/telemetry/clients/node-client.ts +39 -114
  53. package/src/telemetry/factory.ts +5 -8
  54. package/src/telemetry/types.ts +2 -1
  55. package/src/typescript.ts +12 -1
  56. package/src/xml.ts +4 -1
@@ -1,4 +1,14 @@
1
- import type { Telemetry, TelemetryEvent, TelemetryEventData, TimerMetric, InstanceSettings, Session } from '../types'
1
+ import type {
2
+ Telemetry,
3
+ TelemetryEvent,
4
+ TelemetryEventData,
5
+ TimerMetric,
6
+ InstanceSettings,
7
+ Session,
8
+ Datapoint,
9
+ HeartbeatBody,
10
+ } from '../types'
11
+ import { NETWORK_TIMEOUT_MS, DatapointType } from '../types'
2
12
  import { AppSeeTimerMetric } from './util'
3
13
 
4
14
  export abstract class AbstractAppSeeClient implements Telemetry {
@@ -7,6 +17,7 @@ export abstract class AbstractAppSeeClient implements Telemetry {
7
17
  protected readonly hostname?: string | undefined
8
18
  protected session?: Session
9
19
  protected startPromise?: Promise<void>
20
+ protected userId?: string
10
21
 
11
22
  constructor(
12
23
  protected readonly config: InstanceSettings,
@@ -58,6 +69,96 @@ export abstract class AbstractAppSeeClient implements Telemetry {
58
69
  ])
59
70
  }
60
71
 
61
- protected abstract doStart(): Promise<void>
62
- protected abstract doSendEvent(events: TelemetryEvent | TelemetryEvent[]): void
72
+ protected doSendEvent(events: TelemetryEvent | TelemetryEvent[]): void {
73
+ try {
74
+ if (!this.session) {
75
+ return
76
+ }
77
+
78
+ const defaultValues = this.getDefaultDataValues()
79
+ const eventData = Array.isArray(events) ? events : [events]
80
+ const now = Date.now()
81
+ const hostname = this.hostname ? { hostname: this.hostname } : {}
82
+
83
+ const dataPoints = eventData.map<Datapoint>((d) => ({
84
+ t: DatapointType.Event,
85
+ d: now,
86
+ n: d.name,
87
+ p: { scopeId: d.appInfo?.scopeId, ...hostname, ...defaultValues, ...d.data },
88
+ }))
89
+
90
+ dataPoints.push({
91
+ t: DatapointType.User,
92
+ d: now,
93
+ n: this.userId ?? 'unknown',
94
+ p: this.getUserProps(hostname),
95
+ })
96
+
97
+ const body: HeartbeatBody = {
98
+ SessionId: this.session.SessionId,
99
+ DataPoints: dataPoints,
100
+ TabId: this.session.TabId || '0',
101
+ ClientTime: new Date().toISOString(),
102
+ ConfigReceivedTime: new Date().toISOString(),
103
+ }
104
+
105
+ fetch(new URL('/web/heartbeat', this.config.BaseUrl), {
106
+ method: 'POST',
107
+ headers: this.getHeaders(),
108
+ body: JSON.stringify(body),
109
+ signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
110
+ }).catch(() => {})
111
+ } catch (_error) {}
112
+ }
113
+
114
+ protected getHeaders() {
115
+ return {
116
+ APIKey: this.config.APIKey,
117
+ APIAuth: this.config.APIAuth,
118
+ BrowserId: '0',
119
+ ClientId: this.session?.ClientId || '0',
120
+ Version: this.sdkVersion || 'unknown',
121
+ 'Content-Type': 'application/json',
122
+ }
123
+ }
124
+
125
+ protected async doStart(): Promise<void> {
126
+ try {
127
+ if (this.session) {
128
+ return
129
+ }
130
+
131
+ this.userId = await this.getUserHash()
132
+
133
+ const tabId = this.generateHexString()
134
+ const body = {
135
+ RequestId: this.generateHexString(),
136
+ TabId: tabId,
137
+ AppUserId: this.userId,
138
+ ClientTime: new Date().toISOString(),
139
+ TrackingLevel: 'Full',
140
+ ...this.getEnvironmentInfo(),
141
+ }
142
+
143
+ const response = await fetch(new URL('/web/config', this.config.BaseUrl), {
144
+ method: 'POST',
145
+ headers: this.getHeaders(),
146
+ body: JSON.stringify(body),
147
+ signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
148
+ })
149
+
150
+ const bodyResponse = await response.json()
151
+ if (response.ok) {
152
+ this.session = { ...bodyResponse, TabId: tabId }
153
+ } else {
154
+ throw new Error(`Failed to start session with status ${response.status}: ${bodyResponse.Error}`)
155
+ }
156
+ } catch (_error) {}
157
+ }
158
+
159
+ abstract getUserHash(): Promise<string>
160
+ abstract generateHexString(): string
161
+ abstract getEnvironmentInfo(): { SystemLocale: string; ScreenHeight: number; ScreenWidth: number }
162
+ abstract getDefaultDataValues(): TelemetryEventData<{ version: string; clientName: string }>
163
+ abstract getUserProps(hostname: Record<string, string>): TelemetryEventData
63
164
  }
@@ -1,5 +1,4 @@
1
- import type { TelemetryEvent, TelemetryEventData, Datapoint, HeartbeatBody } from '../types'
2
- import { NETWORK_TIMEOUT_MS, DatapointType, type InstanceSettings } from '../types'
1
+ import type { TelemetryEventData, InstanceSettings } from '../types'
3
2
  import { AbstractAppSeeClient } from './abstract-client'
4
3
 
5
4
  export class BrowserTelemetryClient extends AbstractAppSeeClient {
@@ -22,9 +21,8 @@ export class BrowserTelemetryClient extends AbstractAppSeeClient {
22
21
  }
23
22
 
24
23
  private readonly ideVersion: string | undefined
25
- private userId?: string
26
24
 
27
- private async getUserHash() {
25
+ override async getUserHash(): Promise<string> {
28
26
  const userUniqueId = await this.getUserIdentifier()
29
27
  const bytes = new TextEncoder().encode(userUniqueId)
30
28
  const byteHash = await crypto.subtle.digest('SHA-256', bytes)
@@ -52,109 +50,28 @@ export class BrowserTelemetryClient extends AbstractAppSeeClient {
52
50
  }
53
51
  }
54
52
 
55
- private generateHexString(): string {
53
+ override generateHexString(): string {
56
54
  return crypto.randomUUID()
57
55
  }
58
56
 
59
- override async doStart(): Promise<void> {
60
- try {
61
- if (this.session) {
62
- return
63
- }
64
-
65
- this.userId = await this.getUserHash()
66
-
67
- const tabId = this.generateHexString()
68
- const body = {
69
- RequestId: this.generateHexString(),
70
- TabId: tabId,
71
- SystemLocale: navigator.language ?? 'unknown',
72
- AppUserId: this.userId,
73
- ScreenHeight: typeof screen !== 'undefined' ? screen.height : 0,
74
- ScreenWidth: typeof screen !== 'undefined' ? screen.width : 0,
75
- ClientTime: new Date().toISOString(),
76
- TrackingLevel: 'Full',
77
- }
78
-
79
- const response = await fetch(new URL('/web/config', this.config.BaseUrl), {
80
- method: 'POST',
81
- headers: this.getHeaders(),
82
- body: JSON.stringify(body),
83
- signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
84
- })
85
-
86
- const bodyResponse = await response.json()
87
- if (response.ok) {
88
- this.session = { ...bodyResponse, TabId: tabId }
89
- } else {
90
- throw new Error(`Failed to start session with status ${response.status}: ${bodyResponse.Error}`)
91
- }
92
- } catch (_error) {}
93
- }
94
-
95
- override doSendEvent(events: TelemetryEvent | TelemetryEvent[]): void {
96
- try {
97
- if (!this.session) {
98
- return
99
- }
100
-
101
- const defaultValues = this.getDefaultDataValues()
102
- const eventData = Array.isArray(events) ? events : [events]
103
- const now = Date.now()
104
- const hostname = this.hostname ? { hostname: this.hostname } : {}
105
-
106
- const dataPoints = eventData.map<Datapoint>((d) => ({
107
- t: DatapointType.Event,
108
- d: now,
109
- n: d.name,
110
- p: { scopeId: d.appInfo?.scopeId, ...hostname, ...defaultValues, ...d.data },
111
- }))
112
-
113
- const userProps = { hostname } //does CI make sense for browser environment ??
114
-
115
- dataPoints.push({
116
- t: DatapointType.User,
117
- d: now,
118
- n: this.userId!,
119
- p: userProps,
120
- })
121
-
122
- const body: HeartbeatBody = {
123
- SessionId: this.session.SessionId,
124
- DataPoints: dataPoints,
125
- TabId: this.session.TabId || '0',
126
- ClientTime: new Date().toISOString(),
127
- ConfigReceivedTime: new Date().toISOString(),
128
- }
129
-
130
- fetch(new URL('/web/heartbeat', this.config.BaseUrl), {
131
- method: 'POST',
132
- headers: this.getHeaders(),
133
- body: JSON.stringify(body),
134
- signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
135
- }).catch(() => {})
136
- } catch (_error) {}
137
- }
138
-
139
- private getHeaders() {
57
+ override getEnvironmentInfo() {
140
58
  return {
141
- APIKey: this.config.APIKey,
142
- APIAuth: this.config.APIAuth,
143
- BrowserId: '0',
144
- ClientId: this.session?.ClientId || '0',
145
- Version: this.sdkVersion || 'unknown',
146
- 'Content-Type': 'application/json',
59
+ SystemLocale: navigator.language ?? 'unknown',
60
+ ScreenHeight: typeof screen !== 'undefined' ? screen.height : 0,
61
+ ScreenWidth: typeof screen !== 'undefined' ? screen.width : 0,
147
62
  }
148
63
  }
149
64
 
150
- private getDefaultDataValues() {
151
- const defaultValues: TelemetryEventData = {
65
+ override getDefaultDataValues() {
66
+ return {
152
67
  browser: navigator.userAgent || 'unknown',
153
68
  version: this.sdkVersion || 'unknown',
154
69
  clientName: this.clientName,
155
70
  ideVersion: this.ideVersion || 'unknown',
156
71
  }
72
+ }
157
73
 
158
- return defaultValues
74
+ override getUserProps(hostname: Record<string, string>): TelemetryEventData {
75
+ return { ...hostname }
159
76
  }
160
77
  }
@@ -1,26 +1,8 @@
1
1
  import * as os from 'os'
2
2
  import ciInfo from 'ci-info'
3
- import { NETWORK_TIMEOUT_MS, DatapointType } from '../types'
4
- import type { TelemetryEvent, TelemetryEventData, Datapoint, HeartbeatBody, InstanceSettings } from '../types'
3
+ import type { TelemetryEventData, InstanceSettings } from '../types'
5
4
  import { AbstractAppSeeClient } from './abstract-client'
6
5
 
7
- function generateHexString() {
8
- // biome-ignore lint/style/noRestrictedImports: <explanation>
9
- const crypto = require('node:crypto')
10
- return crypto.randomBytes(16).toString('hex')
11
- }
12
-
13
- async function getUserHash() {
14
- const user = `${os.userInfo().username}:${os.hostname()}`
15
- if (!user) {
16
- return 'unknown'
17
- }
18
-
19
- const bytes = new TextEncoder().encode(user)
20
- const byteHash = await crypto.subtle.digest('SHA-256', bytes)
21
- return Buffer.from(byteHash).toString('hex')
22
- }
23
-
24
6
  export class NodeTelemetryClient extends AbstractAppSeeClient {
25
7
  constructor(
26
8
  config: InstanceSettings,
@@ -32,120 +14,63 @@ export class NodeTelemetryClient extends AbstractAppSeeClient {
32
14
  })
33
15
  }
34
16
 
35
- private userId?: string
36
-
37
- override async doStart(): Promise<void> {
38
- try {
39
- if (this.session) {
40
- return
41
- }
42
-
43
- this.userId = await getUserHash()
44
-
45
- const tabId = generateHexString()
46
- const body = {
47
- RequestId: generateHexString(),
48
- TabId: tabId,
49
- SystemLocale: (process.env['LANG'] || 'unknown').split('.')[0],
50
- AppUserId: this.userId,
51
- ScreenHeight: 0,
52
- ScreenWidth: 0,
53
- ClientTime: new Date().toISOString(),
54
- TrackingLevel: 'Full',
55
- }
56
-
57
- const response = await fetch(new URL('/web/config', this.config.BaseUrl), {
58
- method: 'POST',
59
- headers: this.getHeaders(),
60
- body: JSON.stringify(body),
61
- signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
62
- })
63
-
64
- const bodyResponse = await response.json()
65
- if (response.ok) {
66
- this.session = { ...bodyResponse, TabId: tabId }
67
- } else {
68
- throw new Error(`Failed to start session with status ${response.status}: ${bodyResponse.Error}`)
69
- }
70
- } catch (_error) {
71
- //ignore
17
+ override async getUserHash(): Promise<string> {
18
+ const user = `${os.userInfo().username}:${os.hostname()}`
19
+ if (!user) {
20
+ return 'unknown'
72
21
  }
73
- }
74
-
75
- override doSendEvent(events: TelemetryEvent | TelemetryEvent[]): void {
76
- try {
77
- //Check if we have a session
78
- if (!this.session) {
79
- return
80
- }
81
22
 
82
- const defaultValues = this.getDefaultDataValues()
83
- const eventData = Array.isArray(events) ? events : [events]
84
- const now = Date.now()
85
- const hostname = this.hostname ? { hostname: this.hostname } : {}
86
-
87
- const dataPoints = eventData.map<Datapoint>((d) => ({
88
- t: DatapointType.Event,
89
- d: now,
90
- n: d.name,
91
- p: { scopeId: d.appInfo?.scopeId, ...hostname, ...defaultValues, ...d.data },
92
- }))
93
-
94
- const userProps = this.applyCITelemetry({ ...hostname })
95
-
96
- dataPoints.push({
97
- t: DatapointType.User,
98
- d: now,
99
- n: this.userId!,
100
- p: userProps,
101
- })
102
-
103
- const body: HeartbeatBody = {
104
- SessionId: this.session.SessionId,
105
- DataPoints: dataPoints,
106
- TabId: this.session.TabId || '0',
107
- ClientTime: new Date().toISOString(),
108
- ConfigReceivedTime: new Date().toISOString(),
109
- }
23
+ const bytes = new TextEncoder().encode(user)
24
+ const byteHash = await crypto.subtle.digest('SHA-256', bytes)
25
+ return Buffer.from(byteHash).toString('hex')
26
+ }
110
27
 
111
- fetch(new URL('/web/heartbeat', this.config.BaseUrl), {
112
- method: 'POST',
113
- headers: this.getHeaders(),
114
- body: JSON.stringify(body),
115
- signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
116
- }).catch(() => {})
117
- } catch (_error) {
118
- //ignore errors from telemetry
119
- }
28
+ override generateHexString(): string {
29
+ // biome-ignore lint/style/noRestrictedImports: node:crypto is not available via sdk-build-core
30
+ const nodeCrypto = require('node:crypto')
31
+ return nodeCrypto.randomBytes(16).toString('hex')
120
32
  }
121
33
 
122
- private getHeaders() {
34
+ override getEnvironmentInfo() {
123
35
  return {
124
- APIKey: this.config.APIKey,
125
- APIAuth: this.config.APIAuth,
126
- BrowserId: '0',
127
- ClientId: this.session?.ClientId || '0',
128
- Version: this.sdkVersion || 'unknown',
129
- 'Content-Type': 'application/json',
36
+ SystemLocale: (process.env['LANG'] || 'unknown').split('.')[0] ?? 'unknown',
37
+ ScreenHeight: 0,
38
+ ScreenWidth: 0,
130
39
  }
131
40
  }
132
41
 
133
- private getDefaultDataValues() {
134
- const defaultValues: TelemetryEventData = {
42
+ override getDefaultDataValues() {
43
+ const codingAgentData: TelemetryEventData<{ codingAgent?: string }> = {}
44
+ if (process.env['GEMINI_CLI']) {
45
+ codingAgentData.codingAgent = 'gemini'
46
+ } else if (process.env['CLAUDECODE']) {
47
+ codingAgentData.codingAgent = 'claudecode'
48
+ } else if (process.env['CODEX_CI']) {
49
+ codingAgentData.codingAgent = 'codex'
50
+ }
51
+
52
+ return {
135
53
  os: os.type(),
136
54
  version: this.sdkVersion || 'unknown',
137
55
  nodeVersion: process.version,
138
56
  clientName: this.clientName,
57
+ ...codingAgentData,
58
+ ...this.applyCITelemetry(),
139
59
  }
60
+ }
140
61
 
141
- return this.applyCITelemetry(defaultValues)
62
+ override getUserProps(hostname: Record<string, string>): TelemetryEventData {
63
+ return {
64
+ hostname,
65
+ ...this.applyCITelemetry(),
66
+ }
142
67
  }
143
68
 
144
- private applyCITelemetry(data: TelemetryEventData) {
69
+ private applyCITelemetry() {
145
70
  if (ciInfo.isCI) {
146
- return { ...data, ci: true, ciName: ciInfo.name, ciPR: ciInfo.isPR }
71
+ return { ci: true, ciName: ciInfo.name, ciPR: ciInfo.isPR }
147
72
  }
148
73
 
149
- return data
74
+ return {}
150
75
  }
151
76
  }
@@ -10,16 +10,13 @@ export class TelemetryFactory {
10
10
  type?: 'node' | 'browser'
11
11
  attributes?: { sdkVersion?: string; clientName?: string; hostname?: string; ideVersion?: string }
12
12
  }): Telemetry {
13
+ if (process.env['NODE_ENV'] === 'test' || !options || !options.type) {
14
+ return new NoOpTelemetry()
15
+ }
16
+
13
17
  const config = getAppSeeConfig()
14
18
 
15
- if (
16
- process.env['NODE_ENV'] === 'test' ||
17
- !options ||
18
- !options.type ||
19
- !config.APIKey ||
20
- !config.APIAuth ||
21
- !config.BaseUrl
22
- ) {
19
+ if (!config.APIKey || !config.APIAuth || !config.BaseUrl) {
23
20
  return new NoOpTelemetry()
24
21
  }
25
22
 
@@ -56,6 +56,7 @@ export interface TimerMetric {
56
56
  end(eventData?: Pick<TelemetryEvent, 'appInfo' | 'data'>): TelemetryEvent
57
57
  }
58
58
 
59
- export type TelemetryEventData = Record<string, unknown>
59
+ export type TelemetryEventData<T extends Record<string, unknown> = Record<string, unknown>> = T &
60
+ Record<string, unknown>
60
61
 
61
62
  export type TelemetryEvent = { name: string; appInfo?: { scopeId?: string }; data?: TelemetryEventData }
package/src/typescript.ts CHANGED
@@ -159,6 +159,10 @@ export const SupportedKinds = {
159
159
  name: 'FunctionExpression',
160
160
  node: {} as ts.FunctionExpression,
161
161
  },
162
+ [ts.SyntaxKind.ParenthesizedExpression]: {
163
+ name: 'ParenthesizedExpression',
164
+ node: {} as ts.ParenthesizedExpression,
165
+ },
162
166
  [ts.SyntaxKind.FunctionDeclaration]: {
163
167
  name: 'FunctionDeclaration',
164
168
  node: {} as ts.FunctionDeclaration,
@@ -351,6 +355,10 @@ export const SupportedKinds = {
351
355
  name: 'PrefixUnaryExpression',
352
356
  node: {} as ts.PrefixUnaryExpression,
353
357
  },
358
+ [ts.SyntaxKind.ReturnStatement]: {
359
+ name: 'ReturnStatement',
360
+ node: {} as ts.ReturnStatement,
361
+ },
354
362
  } satisfies {
355
363
  [K in ts.SyntaxKind]?: {
356
364
  name: keyof typeof ts.SyntaxKind
@@ -475,7 +483,10 @@ export function remove(target: ts.Node): void {
475
483
  }
476
484
  }
477
485
 
478
- throw new Error(`Unable to remove "${getUnsupportedKindName(target)}" node: ${target.getText()}`)
486
+ // It is possible that multiple plugins could attempt to remove the same node. To support this, only error if target is unsupported
487
+ if (!isSupportedNode(target)) {
488
+ throw new Error(`Unable to remove "${getUnsupportedKindName(target)}" node: ${target.getText()}`)
489
+ }
479
490
  }
480
491
 
481
492
  /**
package/src/xml.ts CHANGED
@@ -104,8 +104,11 @@ export function recordXml(
104
104
  But when fluent generates the XML from source it creates XML with &nbsp; which makes the XML invalid since `nbsp` is not defined.
105
105
 
106
106
  So we are replacing &nbsp; with &amp;nbsp; to ensure the XML is valid.
107
+ Note: This only applies to plain text content. CDATA sections do not parse
108
+ entity references, so &nbsp; is valid as-is inside CDATA.
107
109
  */
108
- const sanitizedValue = stringValue.replaceAll('&nbsp;', '&amp;nbsp;')
110
+ const sanitizedValue =
111
+ contentType === 'plain' ? stringValue.replaceAll('&nbsp;', '&amp;nbsp;') : stringValue
109
112
 
110
113
  const columnNameElement: Element = {
111
114
  type: 'element',