@servicenow/sdk-build-core 4.7.1 → 4.8.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 (77) hide show
  1. package/dist/compiler.d.ts +7 -0
  2. package/dist/compiler.js +22 -6
  3. package/dist/compiler.js.map +1 -1
  4. package/dist/compression.d.ts +1 -1
  5. package/dist/compression.js +28 -48
  6. package/dist/compression.js.map +1 -1
  7. package/dist/diagnostic.js +17 -7
  8. package/dist/diagnostic.js.map +1 -1
  9. package/dist/formatter.js +17 -7
  10. package/dist/formatter.js.map +1 -1
  11. package/dist/fs.js +17 -7
  12. package/dist/fs.js.map +1 -1
  13. package/dist/glob.d.ts +4 -0
  14. package/dist/glob.js +23 -0
  15. package/dist/glob.js.map +1 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/json.js +17 -7
  20. package/dist/json.js.map +1 -1
  21. package/dist/keys-registry.js +17 -7
  22. package/dist/keys-registry.js.map +1 -1
  23. package/dist/now-config-dependencies.js +17 -7
  24. package/dist/now-config-dependencies.js.map +1 -1
  25. package/dist/now-config.d.ts +15 -0
  26. package/dist/now-config.js +20 -7
  27. package/dist/now-config.js.map +1 -1
  28. package/dist/package-inventory.js.map +1 -1
  29. package/dist/plugins/plugin.d.ts +4 -0
  30. package/dist/plugins/plugin.js +17 -1
  31. package/dist/plugins/plugin.js.map +1 -1
  32. package/dist/plugins/shape.d.ts +13 -0
  33. package/dist/plugins/shape.js +43 -7
  34. package/dist/plugins/shape.js.map +1 -1
  35. package/dist/plugins/time.d.ts +5 -0
  36. package/dist/plugins/time.js +8 -0
  37. package/dist/plugins/time.js.map +1 -1
  38. package/dist/taxonomy.d.ts +1106 -6
  39. package/dist/taxonomy.js +17 -8
  40. package/dist/taxonomy.js.map +1 -1
  41. package/dist/telemetry/clients/abstract-client.d.ts +3 -0
  42. package/dist/telemetry/clients/abstract-client.js +2 -0
  43. package/dist/telemetry/clients/abstract-client.js.map +1 -1
  44. package/dist/telemetry/clients/browser-client.d.ts +2 -0
  45. package/dist/telemetry/clients/browser-client.js +1 -0
  46. package/dist/telemetry/clients/browser-client.js.map +1 -1
  47. package/dist/telemetry/clients/node-client.d.ts +1 -0
  48. package/dist/telemetry/clients/node-client.js +25 -10
  49. package/dist/telemetry/clients/node-client.js.map +1 -1
  50. package/dist/telemetry/factory.d.ts +1 -0
  51. package/dist/telemetry/factory.js.map +1 -1
  52. package/dist/typescript.js +17 -7
  53. package/dist/typescript.js.map +1 -1
  54. package/dist/util/conditional-dir.d.ts +13 -0
  55. package/dist/util/conditional-dir.js +22 -0
  56. package/dist/util/conditional-dir.js.map +1 -0
  57. package/dist/util/index.d.ts +1 -0
  58. package/dist/util/index.js +1 -0
  59. package/dist/util/index.js.map +1 -1
  60. package/now.config.schema.json +15 -1
  61. package/package.json +2 -2
  62. package/src/compiler.ts +25 -6
  63. package/src/compression.ts +34 -53
  64. package/src/glob.ts +20 -0
  65. package/src/index.ts +1 -0
  66. package/src/now-config.ts +6 -0
  67. package/src/package-inventory.ts +1 -1
  68. package/src/plugins/plugin.ts +19 -2
  69. package/src/plugins/shape.ts +28 -0
  70. package/src/plugins/time.ts +8 -0
  71. package/src/taxonomy.ts +0 -1
  72. package/src/telemetry/clients/abstract-client.ts +3 -0
  73. package/src/telemetry/clients/browser-client.ts +2 -0
  74. package/src/telemetry/clients/node-client.ts +13 -4
  75. package/src/telemetry/factory.ts +7 -1
  76. package/src/util/conditional-dir.ts +19 -0
  77. package/src/util/index.ts +1 -0
@@ -1,4 +1,5 @@
1
- import { ZipDeflate, unzipSync, zipSync, gunzip as callbackGunzip, Zip, gzipSync, gunzipSync } from 'fflate'
1
+ import { unzipSync, zipSync, gunzip as callbackGunzip, gzipSync, gunzipSync } from 'fflate'
2
+ import type { Zippable } from 'fflate'
2
3
  import { FileSystem, TsMorphFileSystemWrapper } from './fs'
3
4
  import type { Logger } from './logger'
4
5
  import { path } from './path'
@@ -9,7 +10,8 @@ export const unzipAppPackage = async (
9
10
  fs: FileSystem,
10
11
  zipPath: string,
11
12
  targetPath: string,
12
- logger?: Logger
13
+ logger?: Logger,
14
+ ignore?: (filename: string) => boolean
13
15
  ): Promise<{ files: string[] }> => {
14
16
  const zipArrBuf = new Uint8Array(fs.readFileSync(zipPath))
15
17
  const unzipResult = unzipSync(zipArrBuf)
@@ -31,72 +33,51 @@ export const unzipAppPackage = async (
31
33
  delete unzipResult[ZIP_STATUS_FILE]
32
34
  }
33
35
 
34
- const files = Object.entries(unzipResult).map(([filePath, data]) => {
36
+ const files: string[] = []
37
+ for (const [filePath, data] of Object.entries(unzipResult)) {
38
+ const filename = path.basename(filePath)
39
+
40
+ if (ignore?.(filename)) {
41
+ logger?.info(`Skipping file ${filename} due to excludeFilePatterns`)
42
+ continue
43
+ }
44
+
35
45
  const fullPath = path.join(targetPath, filePath)
36
46
 
37
47
  // Create directories if necessary
38
- const dir = path.dirname(fullPath)
39
- FileSystem.ensureDirSync(fs, dir)
48
+ FileSystem.ensureDirSync(fs, path.dirname(fullPath))
40
49
 
41
50
  // Write file content to the filesystem
42
51
  fs.writeFileSync(fullPath, data)
43
52
 
44
- return fullPath
45
- })
53
+ files.push(fullPath)
54
+ }
46
55
 
47
56
  return { files }
48
57
  }
49
58
 
50
59
  export const zipDirectory = async (fs: FileSystem, source: string, destination: string): Promise<string> => {
51
- return new Promise<string>((resolve, reject) => {
52
- try {
53
- const z = new Zip()
54
-
55
- const outputStream = FileSystem.createWriteStream(fs, destination)
56
-
57
- z.ondata = (err, data, final) => {
58
- if (err) {
59
- outputStream.destroy()
60
- reject(err)
61
- } else {
62
- outputStream.write(data)
63
- if (final) {
64
- outputStream.end()
65
- }
66
- }
67
- }
68
-
69
- outputStream.on('finish', () => {
70
- resolve(destination)
71
- })
72
-
73
- outputStream.on('error', (error) => {
74
- reject(error)
75
- })
76
-
77
- // Using our own glob for browser compatibility
78
- const wrappedFs = new TsMorphFileSystemWrapper(fs)
79
- const files = wrappedFs.globSync(['**'], {
80
- cwd: source,
81
- })
60
+ // Using our own glob for browser compatibility
61
+ const wrappedFs = new TsMorphFileSystemWrapper(fs)
62
+ const files = wrappedFs.globSync(['**'], {
63
+ cwd: source,
64
+ })
82
65
 
83
- if (files.length === 0) {
84
- reject(`No files found in ${source} directory to create zip.`)
85
- }
66
+ if (files.length === 0) {
67
+ throw new Error(`No files found in ${source} directory to create zip.`)
68
+ }
86
69
 
87
- for (const file of files) {
88
- const fp = path.normalize(file)
89
- const data = fs.readFileSync(fp)
90
- const zf = new ZipDeflate(fp.replace(source, ''), { level: 6 })
91
- z.add(zf)
92
- zf.push(data, true)
93
- }
70
+ const fileMap: Zippable = {}
71
+ for (const file of files) {
72
+ const fp = path.normalize(file)
73
+ const data = new Uint8Array(fs.readFileSync(fp))
74
+ const entryName = fp.replace(source, '')
75
+ fileMap[entryName] = [data, { level: 6 }]
76
+ }
94
77
 
95
- z.end()
96
- } catch (err) {
97
- reject(err)
98
- }
99
- })
78
+ const zipped = zipSync(fileMap)
79
+ fs.writeFileSync(destination, zipped)
80
+ return destination
100
81
  }
101
82
 
102
83
  const gunzip = (data: Uint8Array) =>
package/src/glob.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Converts a glob pattern to a RegExp.
3
+ * Supports `*` (match within a path segment), `**` (match across segments), and `?` (single character).
4
+ */
5
+ function globToRegex(pattern: string): RegExp {
6
+ const escaped = pattern
7
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars (not * or ?)
8
+ .replace(/\*\*/g, '\0') // placeholder for **
9
+ .replace(/\*/g, '[^/]*') // * → match within segment
10
+ .replace(/\0/g, '.*') // ** → match across segments
11
+ .replace(/\?/g, '[^/]') // ? → single char within segment
12
+ return new RegExp(`^${escaped}$`)
13
+ }
14
+
15
+ /**
16
+ * Returns true if `str` matches any of the provided glob patterns.
17
+ */
18
+ export function matchesGlob(str: string, patterns: string[]): boolean {
19
+ return patterns.some((pattern) => globToRegex(pattern).test(str))
20
+ }
package/src/index.ts CHANGED
@@ -22,3 +22,4 @@ export * from './formatter'
22
22
  export * from './telemetry'
23
23
  export * from './claims'
24
24
  export * from './taxonomy'
25
+ export * from './glob'
package/src/now-config.ts CHANGED
@@ -254,11 +254,13 @@ const NowConfigSchema = z
254
254
  npmUpdateCheck: z.literal(false).or(z.number()).default(10),
255
255
  dependencies: dependenciesSchema,
256
256
  ignoreTransformTableList: z.array(z.string()).default([]),
257
+ excludeFilePatterns: z.array(z.string()).default([]),
257
258
  trustedModules: z.array(z.string()).default([]),
258
259
  tsconfigPath: z.string().optional(),
259
260
  scripts: z.record(z.string(), z.string()).default({}),
260
261
  linter: LinterSchema,
261
262
  tableOutputFormat: z.enum(['bootstrap', 'component']).default('bootstrap').optional(),
263
+ emitDictionary: z.boolean().default(true),
262
264
  defaultLanguage: z
263
265
  .string()
264
266
  .regex(/^[a-z]{2,3}(-[a-zA-Z0-9]{2,8})*$/)
@@ -308,6 +310,7 @@ const NowConfigSchema = z
308
310
  type: z.enum(['configuration', 'package']).default('package'),
309
311
  taxonomy,
310
312
  hostedPlugins: z.record(z.string(), z.string()).optional(),
313
+ sysCode: z.string().optional(),
311
314
 
312
315
  // Application Runtime Policy (ARP) fields
313
316
  applicationRuntimePolicy: z.enum(['none', 'tracking', 'enforcing']).default('none'),
@@ -328,6 +331,7 @@ const NowConfigSchema = z
328
331
  | 'npmUpdateCheck'
329
332
  | 'dependencies'
330
333
  | 'ignoreTransformTableList'
334
+ | 'excludeFilePatterns'
331
335
  | 'trustedModules'
332
336
  | 'modulePaths'
333
337
  | 'serverModulesIncludePatterns'
@@ -346,12 +350,14 @@ const NowConfigSchema = z
346
350
  | 'scripts'
347
351
  | 'linter'
348
352
  | 'tableOutputFormat'
353
+ | 'emitDictionary'
349
354
  | 'defaultLanguage'
350
355
  | 'tableDefaultLanguage'
351
356
  | 'packageResolverVersion'
352
357
  | 'type'
353
358
  | 'taxonomy'
354
359
  | 'hostedPlugins'
360
+ | 'sysCode'
355
361
  | 'applicationRuntimePolicy'
356
362
  | 'networkPolicies'
357
363
  | 'wildcardPolicy'
@@ -14,7 +14,7 @@ export interface PackageInventoryConfig {
14
14
  */
15
15
  export async function sha256(content: string | Uint8Array): Promise<string> {
16
16
  const data = typeof content === 'string' ? new TextEncoder().encode(content) : content
17
- const hashBuffer = await crypto.subtle.digest('SHA-256', data)
17
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data as BufferSource)
18
18
  const hashArray = Array.from(new Uint8Array(hashBuffer))
19
19
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
20
20
  }
@@ -19,7 +19,7 @@ import type { File, OutputFile } from './file'
19
19
  import type { Product } from './product'
20
20
  import { Database, DiffDatabase } from './database'
21
21
  import type { Context as BaseContext, Diagnostics } from './context'
22
- import { getFileType, isSNScope } from '../util'
22
+ import { getConditionalPluginDir, getFileType, isSNScope } from '../util'
23
23
  import { NOW_FILE_EXTENSION, NowConfig } from '..'
24
24
  import { path } from '@servicenow/sdk-build-core'
25
25
 
@@ -839,6 +839,23 @@ export class Plugin {
839
839
  return this.traverseDescendants(parent, database)
840
840
  }
841
841
 
842
+ /**
843
+ * Determines whether the conditional directory of a (if/hosted) child matches that of it's parent.
844
+ */
845
+ private isMixedConditionalChild(parent: Record, child: Record): boolean {
846
+ const parentPlugin = getConditionalPluginDir(parent.getOriginalFilePath())
847
+ const childPlugin = getConditionalPluginDir(child.getOriginalFilePath())
848
+ if (!parentPlugin && !childPlugin) {
849
+ return false
850
+ } else if (!parentPlugin || !childPlugin) {
851
+ return true
852
+ }
853
+
854
+ // both conditional - the child belongs to this parent only if they
855
+ // target the same conditional plugin (the if/<plugin> or hosted_plugins/<plugin> segment)
856
+ return parentPlugin !== childPlugin
857
+ }
858
+
842
859
  private traverseDescendants(parent: Record, database: Database, visited: Set<string> = new Set()): Record[] {
843
860
  visited.add(parent.getId().getValue())
844
861
 
@@ -848,7 +865,7 @@ export class Plugin {
848
865
  const descendants: Record[] = []
849
866
  const push = (children: Record | Record[] | undefined) => {
850
867
  for (const child of [children].flat()) {
851
- if (child && !visited.has(child.getId().getValue())) {
868
+ if (child && !visited.has(child.getId().getValue()) && !this.isMixedConditionalChild(parent, child)) {
852
869
  visited.add(child.getId().getValue())
853
870
  descendants.push(child)
854
871
  descendants.push(...this.traverseDescendants(child, database, visited))
@@ -658,6 +658,20 @@ export class TemplateSpanShape extends StringShape {
658
658
  return `${substitution instanceof Shape ? substitution.getValue() : typeof substitution === 'function' ? substitution(this.expression) : (substitution ?? this.expression.toString().getValue())}${this.literalText}`
659
659
  }
660
660
 
661
+ /**
662
+ * Override equals to compare literal text and expression code instead of
663
+ * rendered values. The base getValue() resolves expressions via
664
+ * toString().getValue() which produces identical strings for structurally
665
+ * different CallExpressionShape instances, causing the commit system to
666
+ * incorrectly skip updates.
667
+ */
668
+ override equals(other: unknown): boolean {
669
+ if (other instanceof TemplateSpanShape) {
670
+ return this.literalText === other.literalText && this.expression.getCode() === other.expression.getCode()
671
+ }
672
+ return super.equals(other)
673
+ }
674
+
661
675
  override getCode(): string {
662
676
  return `$\{${this.expression.getCode()}}${StringShape.escapeBackticks(this.literalText)}`
663
677
  }
@@ -697,6 +711,20 @@ export class TemplateExpressionShape extends StringShape {
697
711
  return `${this.literalText}${this.spans.map((s, i) => s.getValue(typeof substitutions === 'function' ? substitutions : substitutions?.[i])).join('')}`
698
712
  }
699
713
 
714
+ /**
715
+ * Override equals to compare literal text and spans structurally instead of
716
+ * using rendered getValue() strings. See TemplateSpanShape.equals() for details.
717
+ */
718
+ override equals(other: unknown): boolean {
719
+ if (other instanceof TemplateExpressionShape) {
720
+ if (this.literalText !== other.literalText || this.spans.length !== other.spans.length) {
721
+ return false
722
+ }
723
+ return this.spans.every((span, i) => span.equals(other.spans[i]))
724
+ }
725
+ return super.equals(other)
726
+ }
727
+
700
728
  override getCode(): string {
701
729
  return `\`${StringShape.escapeBackticks(this.literalText)}${this.spans.map((s) => s.getCode()).join('')}\``
702
730
  }
@@ -175,6 +175,14 @@ export function parseDateTime(dateTimeStr: string) {
175
175
  return undefined
176
176
  }
177
177
 
178
+ /**
179
+ * Convert a Duration object to the platform's epoch-relative datetime string.
180
+ * E.g. `{ days: 4, hours: 3, minutes: 2, seconds: 1 }` → `'1970-01-05 03:02:01'`
181
+ */
182
+ export function formatDuration(duration: Duration): string {
183
+ return formatDateToPlatformFormat(durationFieldToXML(duration))
184
+ }
185
+
178
186
  /**
179
187
  * Convert a Duration object to a Date for glide_duration fields.
180
188
  * Duration fields are timezone-agnostic (they represent a span of time),
package/src/taxonomy.ts CHANGED
@@ -250,7 +250,6 @@ const taxonomyMapping = z
250
250
  [TableNames.SYS_FLOW_TRIGGER]: taxonomyItem.default('automation/trigger'),
251
251
  [TableNames.SYS_HUB_FLOW]: flowTaxonomyItem,
252
252
  [TableNames.SYS_HUB_ACTION_TYPE_DEFINITION]: flowTaxonomyItem,
253
- [TableNames.SYS_ELEMENT_MAPPING]: flowTaxonomyItem,
254
253
  [TableNames.SYS_HUB_FLOW_INPUT]: flowTaxonomyItem,
255
254
  [TableNames.SYS_HUB_FLOW_OUTPUT]: flowTaxonomyItem,
256
255
  [TableNames.SYS_HUB_ACTION_INPUT_ACTION_INSTANCE]: flowTaxonomyItem,
@@ -15,6 +15,7 @@ export abstract class AbstractAppSeeClient implements Telemetry {
15
15
  protected readonly sdkVersion?: string | undefined
16
16
  protected readonly clientName: 'cli' | 'ide' | string
17
17
  protected readonly hostname?: string | undefined
18
+ protected readonly codingAgent?: string | undefined
18
19
  protected session?: Session
19
20
  protected startPromise?: Promise<void>
20
21
  protected userId?: string
@@ -25,11 +26,13 @@ export abstract class AbstractAppSeeClient implements Telemetry {
25
26
  sdkVersion?: string
26
27
  clientName: 'cli' | 'ide' | string
27
28
  hostname?: string
29
+ codingAgent?: string
28
30
  }
29
31
  ) {
30
32
  this.sdkVersion = telemetryAttributes.sdkVersion
31
33
  this.clientName = telemetryAttributes.clientName
32
34
  this.hostname = telemetryAttributes.hostname
35
+ this.codingAgent = telemetryAttributes.codingAgent
33
36
  }
34
37
 
35
38
  startTimerEvent(name: TelemetryEvent['name']): TimerMetric {
@@ -9,6 +9,7 @@ export class BrowserTelemetryClient extends AbstractAppSeeClient {
9
9
  clientName?: 'ide' | string
10
10
  hostname?: string
11
11
  ideVersion?: string
12
+ codingAgent?: string
12
13
  }
13
14
  ) {
14
15
  const { ideVersion, ...baseAttributes } = telemetryAttributes ?? {}
@@ -68,6 +69,7 @@ export class BrowserTelemetryClient extends AbstractAppSeeClient {
68
69
  version: this.sdkVersion || 'unknown',
69
70
  clientName: this.clientName,
70
71
  ideVersion: this.ideVersion || 'unknown',
72
+ ...(this.codingAgent ? { codingAgent: this.codingAgent } : {}),
71
73
  }
72
74
  }
73
75
 
@@ -7,7 +7,12 @@ import { detectCodingAgent, detectIde } from './detect-agent'
7
7
  export class NodeTelemetryClient extends AbstractAppSeeClient {
8
8
  constructor(
9
9
  config: InstanceSettings,
10
- telemetryAttributes?: { sdkVersion?: string; clientName?: 'cli' | string; hostname?: string }
10
+ telemetryAttributes?: {
11
+ sdkVersion?: string
12
+ clientName?: 'cli' | string
13
+ hostname?: string
14
+ codingAgent?: string
15
+ }
11
16
  ) {
12
17
  super(config, {
13
18
  ...(telemetryAttributes ?? {}),
@@ -44,9 +49,13 @@ export class NodeTelemetryClient extends AbstractAppSeeClient {
44
49
  const env = process.env
45
50
  const agentData: TelemetryEventData<{ codingAgent?: string; ide?: string }> = {}
46
51
 
47
- const codingAgent = detectCodingAgent(env)
48
- if (codingAgent) {
49
- agentData.codingAgent = codingAgent
52
+ if (this.codingAgent) {
53
+ agentData.codingAgent = this.codingAgent
54
+ } else {
55
+ const codingAgent = detectCodingAgent(env)
56
+ if (codingAgent) {
57
+ agentData.codingAgent = codingAgent
58
+ }
50
59
  }
51
60
  const ide = detectIde(env, process.platform)
52
61
  if (ide) {
@@ -8,7 +8,13 @@ import { getAppSeeConfig } from './config'
8
8
  export class TelemetryFactory {
9
9
  static create(options?: {
10
10
  type?: 'node' | 'browser'
11
- attributes?: { sdkVersion?: string; clientName?: string; hostname?: string; ideVersion?: string }
11
+ attributes?: {
12
+ sdkVersion?: string
13
+ clientName?: string
14
+ hostname?: string
15
+ ideVersion?: string
16
+ codingAgent?: string
17
+ }
12
18
  }): Telemetry {
13
19
  if (process.env['NODE_ENV'] === 'test' || !options || !options.type) {
14
20
  return new NoOpTelemetry()
@@ -0,0 +1,19 @@
1
+ const CONDITIONAL_MARKERS = ['if', 'hosted_plugins'] as const
2
+
3
+ /**
4
+ * Extracts the conditional plugin directory from a file path — the `if` or
5
+ * `hosted_plugins` marker plus the plugin segment that follows it (e.g.
6
+ * `if/com.plugin` from `.../if/com.plugin/update/x.xml`, or
7
+ * `hosted_plugins/com.plugin` from `.../hosted_plugins/com.plugin/...`).
8
+ *
9
+ * Two conditional records belong to the same app only when this value matches,
10
+ * so it is used both for grouping descendants and for detecting conditional
11
+ * conflicts. Returns undefined when the path is not under a conditional directory.
12
+ *
13
+ * Matches either path separator so it behaves the same on POSIX and Windows.
14
+ */
15
+ export function getConditionalPluginDir(filePath: string): string | undefined {
16
+ const marker = `(${CONDITIONAL_MARKERS.join('|')})`
17
+ const match = filePath.match(new RegExp(`[/\\\\]${marker}[/\\\\]([^/\\\\]+)`))
18
+ return match ? `${match[1]}/${match[2]}` : undefined
19
+ }
package/src/util/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './is-sn-scope'
2
2
  export * from './get-file-type'
3
3
  export * from './delete-multiple'
4
+ export * from './conditional-dir'