@servicenow/sdk-build-plugins 4.5.0 → 4.6.1

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 (175) hide show
  1. package/dist/acl-plugin.js +3 -1
  2. package/dist/acl-plugin.js.map +1 -1
  3. package/dist/atf/test-plugin.js +6 -8
  4. package/dist/atf/test-plugin.js.map +1 -1
  5. package/dist/basic-syntax-plugin.js +10 -3
  6. package/dist/basic-syntax-plugin.js.map +1 -1
  7. package/dist/column-plugin.js +99 -49
  8. package/dist/column-plugin.js.map +1 -1
  9. package/dist/flow/flow-logic/flow-logic-diagnostics.js +5 -5
  10. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
  11. package/dist/flow/plugins/flow-action-definition-plugin.js +1229 -54
  12. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  13. package/dist/flow/plugins/flow-data-pill-plugin.js +5 -2
  14. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
  15. package/dist/flow/plugins/flow-definition-plugin.js +16 -42
  16. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  17. package/dist/flow/plugins/flow-diagnostics-plugin.d.ts +2 -2
  18. package/dist/flow/plugins/flow-diagnostics-plugin.js +2 -2
  19. package/dist/flow/plugins/flow-instance-plugin.js +68 -22
  20. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  21. package/dist/flow/plugins/step-definition-plugin.js +2 -1
  22. package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
  23. package/dist/flow/plugins/step-instance-plugin.d.ts +9 -1
  24. package/dist/flow/plugins/step-instance-plugin.js +649 -136
  25. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  26. package/dist/flow/plugins/wfa-datapill-plugin.js +20 -5
  27. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
  28. package/dist/flow/post-install.js +1 -0
  29. package/dist/flow/post-install.js.map +1 -1
  30. package/dist/flow/utils/complex-object-resolver.js +4 -1
  31. package/dist/flow/utils/complex-object-resolver.js.map +1 -1
  32. package/dist/flow/utils/complex-objects.js +1 -1
  33. package/dist/flow/utils/complex-objects.js.map +1 -1
  34. package/dist/flow/utils/flow-constants.d.ts +66 -2
  35. package/dist/flow/utils/flow-constants.js +402 -6
  36. package/dist/flow/utils/flow-constants.js.map +1 -1
  37. package/dist/flow/utils/flow-io-to-record.d.ts +1 -1
  38. package/dist/flow/utils/flow-io-to-record.js +37 -16
  39. package/dist/flow/utils/flow-io-to-record.js.map +1 -1
  40. package/dist/flow/utils/flow-shapes.js +4 -0
  41. package/dist/flow/utils/flow-shapes.js.map +1 -1
  42. package/dist/flow/utils/label-cache-parser.d.ts +9 -2
  43. package/dist/flow/utils/label-cache-parser.js +32 -4
  44. package/dist/flow/utils/label-cache-parser.js.map +1 -1
  45. package/dist/flow/utils/pill-shape-helpers.d.ts +15 -0
  46. package/dist/flow/utils/pill-shape-helpers.js +35 -0
  47. package/dist/flow/utils/pill-shape-helpers.js.map +1 -0
  48. package/dist/flow/utils/pill-string-parser.js +1 -0
  49. package/dist/flow/utils/pill-string-parser.js.map +1 -1
  50. package/dist/flow/utils/schema-to-flow-object.d.ts +6 -1
  51. package/dist/flow/utils/schema-to-flow-object.js +131 -15
  52. package/dist/flow/utils/schema-to-flow-object.js.map +1 -1
  53. package/dist/flow/utils/utils.d.ts +1 -0
  54. package/dist/flow/utils/utils.js +6 -1
  55. package/dist/flow/utils/utils.js.map +1 -1
  56. package/dist/form-plugin.js +7 -9
  57. package/dist/form-plugin.js.map +1 -1
  58. package/dist/inbound-email-action-plugin.d.ts +10 -0
  59. package/dist/inbound-email-action-plugin.js +128 -0
  60. package/dist/inbound-email-action-plugin.js.map +1 -0
  61. package/dist/index.d.ts +4 -0
  62. package/dist/index.js +4 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/instance-scan-plugin.js +0 -5
  65. package/dist/instance-scan-plugin.js.map +1 -1
  66. package/dist/now-config-plugin.js +1 -0
  67. package/dist/now-config-plugin.js.map +1 -1
  68. package/dist/property-plugin.js +1 -1
  69. package/dist/property-plugin.js.map +1 -1
  70. package/dist/record-plugin.d.ts +7 -0
  71. package/dist/record-plugin.js +13 -4
  72. package/dist/record-plugin.js.map +1 -1
  73. package/dist/rest-api-plugin.js +8 -1
  74. package/dist/rest-api-plugin.js.map +1 -1
  75. package/dist/schedule-script/scheduled-script-plugin.js +8 -3
  76. package/dist/schedule-script/scheduled-script-plugin.js.map +1 -1
  77. package/dist/script-include-plugin.js +4 -0
  78. package/dist/script-include-plugin.js.map +1 -1
  79. package/dist/service-catalog/catalog-clientscript-plugin.js +2 -2
  80. package/dist/service-catalog/catalog-clientscript-plugin.js.map +1 -1
  81. package/dist/service-catalog/catalog-ui-policy-plugin.js +2 -2
  82. package/dist/service-catalog/catalog-ui-policy-plugin.js.map +1 -1
  83. package/dist/service-catalog/service-catalog-base.d.ts +20 -20
  84. package/dist/service-catalog/service-catalog-base.js +24 -24
  85. package/dist/service-catalog/service-catalog-base.js.map +1 -1
  86. package/dist/service-catalog/utils.js +1 -1
  87. package/dist/service-catalog/utils.js.map +1 -1
  88. package/dist/service-portal/header-footer-plugin.d.ts +2 -0
  89. package/dist/service-portal/header-footer-plugin.js +50 -0
  90. package/dist/service-portal/header-footer-plugin.js.map +1 -0
  91. package/dist/service-portal/menu-plugin.js +3 -22
  92. package/dist/service-portal/menu-plugin.js.map +1 -1
  93. package/dist/service-portal/page-plugin.js +3 -24
  94. package/dist/service-portal/page-plugin.js.map +1 -1
  95. package/dist/service-portal/page-route-map-plugin.d.ts +2 -0
  96. package/dist/service-portal/page-route-map-plugin.js +114 -0
  97. package/dist/service-portal/page-route-map-plugin.js.map +1 -0
  98. package/dist/service-portal/portal-plugin.js +21 -8
  99. package/dist/service-portal/portal-plugin.js.map +1 -1
  100. package/dist/service-portal/utils.d.ts +40 -2
  101. package/dist/service-portal/utils.js +283 -2
  102. package/dist/service-portal/utils.js.map +1 -1
  103. package/dist/service-portal/widget-plugin.js +9 -218
  104. package/dist/service-portal/widget-plugin.js.map +1 -1
  105. package/dist/static-content-plugin.js +4 -0
  106. package/dist/static-content-plugin.js.map +1 -1
  107. package/dist/table-plugin.js +377 -67
  108. package/dist/table-plugin.js.map +1 -1
  109. package/dist/ui-action-plugin.js +1 -4
  110. package/dist/ui-action-plugin.js.map +1 -1
  111. package/dist/ui-page-plugin.js +68 -13
  112. package/dist/ui-page-plugin.js.map +1 -1
  113. package/dist/ui-policy-plugin.js +28 -96
  114. package/dist/ui-policy-plugin.js.map +1 -1
  115. package/dist/utils.d.ts +5 -1
  116. package/dist/utils.js +41 -0
  117. package/dist/utils.js.map +1 -1
  118. package/dist/view-plugin.js +8 -3
  119. package/dist/view-plugin.js.map +1 -1
  120. package/dist/workspace-plugin.js +39 -36
  121. package/dist/workspace-plugin.js.map +1 -1
  122. package/package.json +5 -4
  123. package/src/acl-plugin.ts +3 -1
  124. package/src/atf/test-plugin.ts +6 -9
  125. package/src/basic-syntax-plugin.ts +11 -3
  126. package/src/column-plugin.ts +137 -75
  127. package/src/flow/flow-logic/flow-logic-diagnostics.ts +5 -6
  128. package/src/flow/plugins/flow-action-definition-plugin.ts +1581 -61
  129. package/src/flow/plugins/flow-data-pill-plugin.ts +5 -2
  130. package/src/flow/plugins/flow-definition-plugin.ts +12 -47
  131. package/src/flow/plugins/flow-diagnostics-plugin.ts +2 -2
  132. package/src/flow/plugins/flow-instance-plugin.ts +98 -22
  133. package/src/flow/plugins/step-definition-plugin.ts +2 -1
  134. package/src/flow/plugins/step-instance-plugin.ts +772 -156
  135. package/src/flow/plugins/wfa-datapill-plugin.ts +25 -5
  136. package/src/flow/post-install.ts +1 -0
  137. package/src/flow/utils/complex-object-resolver.ts +4 -1
  138. package/src/flow/utils/complex-objects.ts +1 -1
  139. package/src/flow/utils/flow-constants.ts +421 -5
  140. package/src/flow/utils/flow-io-to-record.ts +43 -17
  141. package/src/flow/utils/flow-shapes.ts +4 -0
  142. package/src/flow/utils/label-cache-parser.ts +33 -4
  143. package/src/flow/utils/pill-shape-helpers.ts +42 -0
  144. package/src/flow/utils/pill-string-parser.ts +1 -0
  145. package/src/flow/utils/schema-to-flow-object.ts +183 -15
  146. package/src/flow/utils/utils.ts +12 -1
  147. package/src/form-plugin.ts +1 -3
  148. package/src/inbound-email-action-plugin.ts +145 -0
  149. package/src/index.ts +4 -0
  150. package/src/instance-scan-plugin.ts +0 -5
  151. package/src/now-config-plugin.ts +1 -0
  152. package/src/property-plugin.ts +4 -1
  153. package/src/record-plugin.ts +25 -7
  154. package/src/rest-api-plugin.ts +7 -1
  155. package/src/schedule-script/scheduled-script-plugin.ts +14 -3
  156. package/src/script-include-plugin.ts +8 -0
  157. package/src/service-catalog/catalog-clientscript-plugin.ts +2 -2
  158. package/src/service-catalog/catalog-ui-policy-plugin.ts +2 -2
  159. package/src/service-catalog/service-catalog-base.ts +24 -24
  160. package/src/service-catalog/utils.ts +1 -1
  161. package/src/service-portal/header-footer-plugin.ts +57 -0
  162. package/src/service-portal/menu-plugin.ts +1 -23
  163. package/src/service-portal/page-plugin.ts +3 -28
  164. package/src/service-portal/page-route-map-plugin.ts +124 -0
  165. package/src/service-portal/portal-plugin.ts +33 -10
  166. package/src/service-portal/utils.ts +404 -3
  167. package/src/service-portal/widget-plugin.ts +14 -290
  168. package/src/static-content-plugin.ts +3 -0
  169. package/src/table-plugin.ts +466 -99
  170. package/src/ui-action-plugin.ts +1 -8
  171. package/src/ui-page-plugin.ts +76 -13
  172. package/src/ui-policy-plugin.ts +32 -128
  173. package/src/utils.ts +52 -0
  174. package/src/view-plugin.ts +10 -4
  175. package/src/workspace-plugin.ts +43 -43
@@ -247,13 +247,6 @@ export const UiActionPlugin = Plugin.create({
247
247
  ),
248
248
  })),
249
249
  })
250
- const roles = arg.get('roles').ifArray()?.getElements() ?? []
251
- if (!arg.get('condition').toString().getValue().trim() && roles.length === 0) {
252
- diagnostics.warn(
253
- arg.get('roles').ifDefined() ?? arg.get('condition').ifDefined() ?? arg,
254
- 'UI Actions with an empty condition and no roles defined can be called by any logged-in users. Please restrict UI actions. The condition field should be specified to restrict execution of this UI Action to certain users. For example, current.canWrite() condition restricts the UI Action to the users who can modify the current record, gs.hasRole("admin") condition restricts the UI Action to the users with admin role.'
255
- )
256
- }
257
250
 
258
251
  if (arg.get('script').is(ModuleFunctionShape) && isClient.ifBoolean()?.getValue()) {
259
252
  diagnostics.error(isClient, 'Module scripts (sys_module) cannot be used on client-side UI Actions')
@@ -309,7 +302,7 @@ export const UiActionPlugin = Plugin.create({
309
302
  })
310
303
  )
311
304
  }
312
-
305
+ const roles = arg.get('roles').ifArray()?.getElements() ?? []
313
306
  const roleRecords: Record[] = []
314
307
  for (const role of roles) {
315
308
  const roleReference = role.isString()
@@ -17,6 +17,7 @@ import {
17
17
  type Factory,
18
18
  type Transform,
19
19
  } from '@servicenow/sdk-build-core'
20
+ import { parseDocument, DomUtils } from 'htmlparser2'
20
21
  import { XMLParser, XMLBuilder, type X2jOptions, type XmlBuilderOptions } from 'fast-xml-parser'
21
22
  import { create } from 'xmlbuilder2'
22
23
  import { NowIdShape } from './now-id-plugin'
@@ -51,6 +52,62 @@ const builderOptions: XmlBuilderOptions = {
51
52
  ],
52
53
  }
53
54
 
55
+ const LT_PLACEHOLDER = '\0__SDK_LT__\0'
56
+ const AMP_PLACEHOLDER = '\0__SDK_AMP__\0'
57
+ const RAW_CONTENT_TAGS = ['script', 'style', 'textarea'] as const
58
+
59
+ /**
60
+ * Uses htmlparser2 to find script/style/textarea content and replaces `<` and
61
+ * `&` with placeholders. This prevents fast-xml-parser from misinterpreting
62
+ * JavaScript comparison operators (e.g. `a < b`) as XML tag openers, and
63
+ * prevents the XMLBuilder's entity escaping from converting `&` to `$[AMP]`
64
+ * inside script content.
65
+ *
66
+ * htmlparser2 is used instead of regex because it correctly handles edge cases
67
+ * like `>` inside attribute values and script tags inside HTML comments.
68
+ */
69
+ function escapeRawContent(html: string): string {
70
+ const doc = parseDocument(html, { withStartIndices: true, withEndIndices: true })
71
+
72
+ const regions: { start: number; end: number }[] = []
73
+ for (const tag of RAW_CONTENT_TAGS) {
74
+ for (const el of DomUtils.getElementsByTagName(tag, doc, true)) {
75
+ for (const child of el.children) {
76
+ if (child.type === 'text' && child.startIndex != null && child.endIndex != null) {
77
+ const text = child.data
78
+ if (text.includes('<') || text.includes('&')) {
79
+ regions.push({ start: child.startIndex, end: child.endIndex + 1 })
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ if (regions.length === 0) {
87
+ return html
88
+ }
89
+
90
+ // Sort by position descending so replacements don't shift earlier indices
91
+ regions.sort((a, b) => b.start - a.start)
92
+
93
+ let result = html
94
+ for (const { start, end } of regions) {
95
+ const content = result.slice(start, end)
96
+ const escaped = content.replace(/&/g, AMP_PLACEHOLDER).replace(/</g, LT_PLACEHOLDER)
97
+ result = result.slice(0, start) + escaped + result.slice(end)
98
+ }
99
+
100
+ return result
101
+ }
102
+
103
+ /**
104
+ * Restores `<` and `&` characters that were replaced by `escapeRawContent`
105
+ * after fast-xml-parser has finished processing.
106
+ */
107
+ function restoreRawContent(html: string): string {
108
+ return html.replaceAll(LT_PLACEHOLDER, '<').replaceAll(AMP_PLACEHOLDER, '&')
109
+ }
110
+
54
111
  const POLARIS_APPSHELL_THEME_ID = 'c86a62e2c7022010099a308dc7c26022'
55
112
  const BYOUI_ARTIFACT_NAME_SUFFIX = 'BYOUI Files'
56
113
  // Matches the prefix HtmlImportPlugin prepends when it resolves an `import x from '*.html'`
@@ -388,9 +445,11 @@ export const UiPagePlugin = Plugin.create({
388
445
 
389
446
  if (html) {
390
447
  try {
448
+ html = escapeRawContent(html)
391
449
  const nodes = parser.parse(html)
392
450
  const transformed = nodeTransformer(nodes)
393
451
  html = new XMLBuilder(builderOptions).build(transformed)
452
+ html = restoreRawContent(html)
394
453
  } catch (error: unknown) {
395
454
  if (error instanceof Error) {
396
455
  diagnostics.error(arg.get('html'), error.message)
@@ -485,12 +544,14 @@ const getUIPageSourceFilePaths = (
485
544
  ): { files: string[]; assetNames: string[] } => {
486
545
  const empty = { files: [], assetNames: [] }
487
546
  try {
488
- // Derive manifest path from HTML path
489
- // The manifest is in the build output directory (staticContentDir), not the source directory
490
- // e.g., src/client/index.html -> dist/static/index.ui-source-manifest.json
491
- const htmlBasename = path.basename(htmlFilePath, '.html')
547
+ // Derive manifest path from HTML path by mirroring the directory structure
548
+ // from clientDir into staticContentDir and swapping the extension.
549
+ // e.g., src/client/index.html -> dist/static/index.ui-source-manifest.json
550
+ // src/client/admin/settings.html -> dist/static/admin/settings.ui-source-manifest.json
551
+ const clientAbsDir = path.join(rootDir, config.clientDir)
492
552
  const staticContentAbsDir = path.join(rootDir, config.staticContentDir)
493
- const manifestPath = path.join(staticContentAbsDir, `${htmlBasename}.ui-source-manifest.json`)
553
+ const htmlRelPath = path.relative(clientAbsDir, htmlFilePath)
554
+ const manifestPath = path.join(staticContentAbsDir, htmlRelPath).replace(/\.html$/, '.ui-source-manifest.json')
494
555
 
495
556
  // Check if manifest file exists
496
557
  try {
@@ -510,24 +571,26 @@ const getUIPageSourceFilePaths = (
510
571
 
511
572
  // Derive the JS asset name from the manifest's entry field, matching
512
573
  // static-content-plugin's formula: path.join(scope, relativePath_without_ext).
513
- // The manifest file is named after the HTML entry (e.g. index.ui-source-manifest.json)
514
- // but the JS bundle is named after the JS entry (e.g. main.tsx -> main.jsdbx).
515
- // Using manifest.entry's basename ensures the names align.
574
+ // The entry path is relative to the client directory and preserves subdirectories.
575
+ // e.g., src/client/main.tsx -> scope/main
576
+ // src/client/admin/settings.tsx -> scope/admin/settings
516
577
  if (!manifest.entry || typeof manifest.entry !== 'string') {
517
578
  logger.warn(`No entry field in manifest at ${manifestPath}`)
518
579
  return empty
519
580
  }
520
- const entryBasename = path.basename(manifest.entry, path.extname(manifest.entry))
521
- const entryAssetName = path.join(config.scope, entryBasename).replace(/\\/g, '/')
581
+ const entryRelativePath = path.relative(config.clientDir, manifest.entry).replace(/\\/g, '/')
582
+ const entryRelativeWithoutExt = entryRelativePath.replace(/\.[^.]+$/, '')
583
+ const entryAssetName = path.join(config.scope, entryRelativeWithoutExt).replace(/\\/g, '/')
522
584
 
523
585
  // Check if a source map bundle also exists in staticContentDir.
524
586
  // static-content-plugin names source map assets as: path.join(scope, relativePath.replace('dbx', ''))
525
- // e.g. main.jsdbx.map -> main.js.map -> scope/main.js.map
587
+ // e.g. main.jsdbx.map -> scope/main.js.map
588
+ // admin/settings.jsdbx.map -> scope/admin/settings.js.map
526
589
  const assetNames = [entryAssetName]
527
- const sourceMapFilePath = path.join(staticContentAbsDir, `${entryBasename}.jsdbx.map`)
590
+ const sourceMapFilePath = path.join(staticContentAbsDir, `${entryRelativeWithoutExt}.jsdbx.map`)
528
591
  try {
529
592
  fs.accessSync(sourceMapFilePath)
530
- const sourceMapAssetName = path.join(config.scope, `${entryBasename}.js.map`).replace(/\\/g, '/')
593
+ const sourceMapAssetName = path.join(config.scope, `${entryRelativeWithoutExt}.js.map`).replace(/\\/g, '/')
531
594
  assetNames.push(sourceMapAssetName)
532
595
  } catch {
533
596
  // no source map in this build output — skip
@@ -108,128 +108,35 @@ export const UiPolicyPlugin = Plugin.create({
108
108
  },
109
109
  },
110
110
  toShape(record, { descendants }) {
111
- const actions = descendants
112
- .query(SYS_UI_POLICY_ACTION)
113
- .map((action) => {
114
- const fieldValue = action.get('field')?.ifString()?.getValue()
115
- if (!fieldValue) {
116
- return null // Skip actions without a field
117
- }
118
-
119
- const actionObj: {
120
- field: string
121
- visible?: boolean | 'ignore'
122
- readOnly?: boolean | 'ignore'
123
- mandatory?: boolean | 'ignore'
124
- cleared?: boolean
125
- table?: string
126
- value?: string
127
- fieldMessage?: string
128
- fieldMessageType?: string
129
- valueAction?: string
130
- } = {
131
- field: fieldValue,
132
- }
133
-
134
- // Convert ServiceNow string format to boolean | 'ignore'
135
- const visible = stringToBoolean(action.get('visible')?.ifString()?.getValue())
136
- const disabled = stringToBoolean(action.get('disabled')?.ifString()?.getValue())
137
- const mandatory = stringToBoolean(action.get('mandatory')?.ifString()?.getValue())
138
-
139
- // Only include visible if it's not 'ignore'
140
- if (visible !== undefined && visible !== 'ignore') {
141
- actionObj.visible = visible
142
- }
143
-
144
- // Convert 'disabled' to 'readOnly'
145
- // Only include 'readOnly' if it's not 'ignore'
146
- if (disabled !== undefined && disabled !== 'ignore') {
147
- actionObj.readOnly = disabled
148
- }
149
-
150
- // Only include mandatory if it's not 'ignore'
151
- if (mandatory !== undefined && mandatory !== 'ignore') {
152
- actionObj.mandatory = mandatory
153
- }
154
-
155
- const cleared = action.get('cleared')?.ifBoolean()?.getValue()
156
- if (cleared) {
157
- actionObj.cleared = cleared
158
- }
159
-
160
- // Add new optional fields - only if they have meaningful values
161
- const table = action.get('table')?.ifString()?.getValue()
162
- const parentTable = record.get('table')?.ifString()?.getValue()
163
- // Only include table if it's different from the parent policy's table
164
- if (table && table !== parentTable) {
165
- actionObj.table = table
166
- }
167
-
168
- const value = action.get('value')?.ifString()?.getValue()
169
- if (value) {
170
- actionObj.value = value
171
- }
172
-
173
- const fieldMessage = action.get('field_message')?.ifString()?.getValue()
174
- if (fieldMessage) {
175
- actionObj.fieldMessage = fieldMessage
176
- }
177
-
178
- const fieldMessageType = action.get('field_message_type')?.ifString()?.getValue()
179
- // Only include if it's not the default value 'none'
180
- if (fieldMessageType && fieldMessageType !== 'none') {
181
- actionObj.fieldMessageType = fieldMessageType
182
- }
183
-
184
- const valueAction = action.get('value_action')?.ifString()?.getValue()
185
- // Only include if it's not the default value 'ignore'
186
- if (valueAction && valueAction !== 'ignore') {
187
- actionObj.valueAction = valueAction
188
- }
189
-
190
- // Skip actions that have no meaningful properties (all are 'ignore')
191
- if (Object.keys(actionObj).length === 1) {
192
- return null // Only 'field' property exists, skip this action
193
- }
111
+ const actions = descendants.query(SYS_UI_POLICY_ACTION).map((action) => {
112
+ return action.transform(({ $ }) => ({
113
+ field: $,
114
+ visible: $.map((v) => stringToBoolean(v?.asString()?.getValue())).def('ignore'),
115
+ readOnly: $.from('disabled')
116
+ .map((v) => stringToBoolean(v?.asString()?.getValue()))
117
+ .def('ignore'),
118
+ mandatory: $.map((v) => stringToBoolean(v?.asString()?.getValue())).def('ignore'),
119
+ cleared: $.toBoolean().def(false),
120
+ table: $.def(''),
121
+ value: $.def(''),
122
+ fieldMessage: $.from('field_message').def(''),
123
+ fieldMessageType: $.from('field_message_type').def('none'),
124
+ valueAction: $.from('value_action').def('ignore'),
125
+ }))
126
+ })
194
127
 
195
- return actionObj
196
- })
197
- .filter((action) => action !== null) // Remove null actions
198
-
199
- // Process related list actions
200
- const relatedListActions = descendants
201
- .query(SYS_UI_POLICY_RL_ACTION)
202
- .map((rlAction) => {
203
- const rlActionObj: {
204
- list?: string
205
- visible?: boolean | 'ignore'
206
- } = {}
207
-
208
- const listValue = rlAction.get('list')?.ifString()?.getValue()
209
- if (listValue) {
210
- // Strip REL: prefix if present (transform ServiceNow → Fluent)
211
- // Users write plain GUIDs or table.field format in Fluent code
212
- if (listValue.startsWith('REL:')) {
213
- rlActionObj.list = listValue.substring(4) // Remove 'REL:' prefix
214
- } else {
215
- rlActionObj.list = listValue
128
+ const relatedListActions = descendants.query(SYS_UI_POLICY_RL_ACTION).map((rlAction) =>
129
+ rlAction.transform(({ $ }) => ({
130
+ list: $.map((v) => {
131
+ if (v?.ifString()?.ifDefined()) {
132
+ const listVal = v.asString().getValue()
133
+ return listVal.startsWith('REL:') ? listVal.substring(4) : listVal
216
134
  }
217
- }
218
-
219
- // Convert ServiceNow string format to boolean | 'ignore'
220
- const visible = stringToBoolean(rlAction.get('visible')?.ifString()?.getValue())
221
- if (visible !== undefined && visible !== 'ignore') {
222
- rlActionObj.visible = visible
223
- }
224
-
225
- // Skip if no meaningful properties (only $id exists)
226
- if (Object.keys(rlActionObj).length === 0) {
227
- return null
228
- }
229
-
230
- return rlActionObj
231
- })
232
- .filter((rlAction) => rlAction !== null) // Remove null actions
135
+ return ''
136
+ }).def(''),
137
+ visible: $.map((v) => stringToBoolean(v?.asString()?.getValue())).def('ignore'),
138
+ }))
139
+ )
233
140
 
234
141
  return {
235
142
  success: true,
@@ -278,7 +185,7 @@ export const UiPolicyPlugin = Plugin.create({
278
185
  order: $.from('order').toNumber().def(100),
279
186
  setValues: $.from('set_values').def(''),
280
187
  view: $.from('view').def(''),
281
- actions: $.val(actions.length > 0 ? actions : undefined),
188
+ actions: $.val(actions).def([]),
282
189
  relatedListActions: $.val(
283
190
  relatedListActions.length > 0 ? relatedListActions : undefined
284
191
  ),
@@ -401,11 +308,10 @@ export const UiPolicyPlugin = Plugin.create({
401
308
  const hasClearedProp = action.get('cleared')?.isBoolean() || false
402
309
 
403
310
  if (!hasVisibleProp && !hasReadOnlyProp && !hasMandatoryProp && !hasClearedProp) {
404
- diagnostics.error(
311
+ diagnostics.hint(
405
312
  action,
406
- `Action at index ${i} must specify at least one of: visible, readOnly, mandatory, or cleared`
313
+ `Action at index ${i} has no effect — consider specifying at least one of: visible, readOnly, mandatory, or cleared`
407
314
  )
408
- continue
409
315
  }
410
316
 
411
317
  const actionRecord = await factory.createRecord({
@@ -475,14 +381,12 @@ export const UiPolicyPlugin = Plugin.create({
475
381
  }
476
382
  }
477
383
 
478
- // At least one property must be specified (visible or list)
479
384
  const hasVisibleProp = isValidActionValue(rlAction.get('visible'))
480
385
  if (!hasVisibleProp && !listValue) {
481
- diagnostics.error(
386
+ diagnostics.hint(
482
387
  rlAction,
483
- `Related list action at index ${i} must specify at least one of: list or visible`
388
+ `Related list action at index ${i} has no effect — consider specifying at least one of: list or visible`
484
389
  )
485
- continue
486
390
  }
487
391
 
488
392
  const rlActionRecord = await factory.createRecord({
package/src/utils.ts CHANGED
@@ -6,7 +6,10 @@ import {
6
6
  type Compiler,
7
7
  type PluginApiDoc,
8
8
  type Record as FluentRecord,
9
+ type OutputFile,
10
+ type Transform,
9
11
  } from '@servicenow/sdk-build-core'
12
+ import { create } from 'xmlbuilder2'
10
13
 
11
14
  export function toReference(shape: Shape) {
12
15
  return shape.ifRecord()?.getId() ?? shape.ifString()?.getValue() ?? ''
@@ -130,6 +133,55 @@ export const showGuidFieldDiagnostic = (
130
133
  }
131
134
  }
132
135
 
136
+ export async function generateChoiceSetFile(
137
+ choiceSet: FluentRecord,
138
+ choices: FluentRecord[],
139
+ config: { scope: string; scopeId: string },
140
+ transform: Transform
141
+ ): Promise<OutputFile> {
142
+ const tableName = choiceSet.get('name').asString().getValue()
143
+ const elementName = choiceSet.get('element').asString().getValue()
144
+ const xml = create().ele('record_update')
145
+ const root = xml.ele('sys_choice_set', { table: tableName, field: elementName })
146
+
147
+ choices.forEach((choice) => {
148
+ const child = root.ele('sys_choice', { action: choice.getAction() })
149
+ child.ele('sys_id').txt(choice.getId().getValue())
150
+ child.ele('name').txt(tableName)
151
+ child.ele('element').txt(elementName)
152
+
153
+ for (const prop of [
154
+ 'label',
155
+ 'value',
156
+ 'sequence',
157
+ 'dependent_value',
158
+ 'hint',
159
+ 'inactive',
160
+ 'inactive_on_update',
161
+ 'language',
162
+ ]) {
163
+ choice
164
+ .get(prop)
165
+ .ifDefined()
166
+ ?.toString()
167
+ .pipe((p) => child.ele(prop).txt(p.getValue()))
168
+ }
169
+ })
170
+
171
+ const child = root.ele('sys_choice_set', { action: choiceSet.getAction() })
172
+ child.ele('sys_id').txt(choiceSet.getId().getValue())
173
+ child.ele('sys_scope', { display_value: config.scope }).txt(config.scopeId)
174
+ child.ele('name').txt(tableName)
175
+ child.ele('element').txt(elementName)
176
+
177
+ return {
178
+ source: choiceSet,
179
+ name: `${await transform.getUpdateName(choiceSet)}.xml`,
180
+ category: choiceSet.getInstallCategory(),
181
+ content: xml.end({ prettyPrint: true }),
182
+ }
183
+ }
184
+
133
185
  /**
134
186
  * Creates a documentation entry for first-party SDK plugins.
135
187
  *
@@ -1,4 +1,4 @@
1
- import { Plugin, Record, RecordId, Shape } from '@servicenow/sdk-build-core'
1
+ import { Plugin, Record, RecordId, Shape, ts } from '@servicenow/sdk-build-core'
2
2
 
3
3
  export const ViewPlugin = Plugin.create({
4
4
  name: 'ViewPlugin',
@@ -11,7 +11,7 @@ export const ViewPlugin = Plugin.create({
11
11
  shapes: [
12
12
  {
13
13
  shape: Record,
14
- inspect(record, { diagnostics }) {
14
+ inspect(record, { diagnostics, logger }) {
15
15
  if (record.getTable() !== 'sys_ui_view') {
16
16
  return
17
17
  }
@@ -22,8 +22,14 @@ export const ViewPlugin = Plugin.create({
22
22
  }
23
23
 
24
24
  const viewName = record.get('name')
25
- if (!viewName.isString() || !/^[a-zA-Z0-9_]+$/.test(viewName.getValue())) {
26
- diagnostics.error(viewName, `View name can only contain alphanumeric characters`)
25
+ if (!viewName.isString() || !/^[a-zA-Z0-9_,]+$/.test(viewName.getValue())) {
26
+ if (ts.Node.isNode(viewName.getOriginalSource())) {
27
+ diagnostics.error(viewName, `View name can only contain alphanumeric characters`)
28
+ } else {
29
+ logger.warn(
30
+ `[ViewPlugin] View name '${viewName.isString() ? viewName.getValue() : ''}' in ${record.getOriginalFilePath()} can only contain alphanumeric characters`
31
+ )
32
+ }
27
33
  }
28
34
  },
29
35
  },
@@ -5,6 +5,7 @@ import {
5
5
  type Record,
6
6
  type Shape,
7
7
  type ObjectShape,
8
+ Database,
8
9
  } from '@servicenow/sdk-build-core'
9
10
  import { NowIdShape } from './now-id-plugin'
10
11
  import { createSdkDocEntry } from './utils'
@@ -47,68 +48,36 @@ export const WorkspacePlugin = Plugin.create({
47
48
  sys_ux_page_registry: {
48
49
  relationships: {
49
50
  sys_ux_registry_m2m_category: {
50
- via: (parent, child) =>
51
- child.getCreator()?.getName() === 'WorkspacePlugin' &&
52
- parent.getId().getValue() ===
53
- (child.get('page_registry').isRecord()
54
- ? child.get('page_registry').asRecord().getId().getValue()
55
- : child.get('page_registry').asString().getValue()),
51
+ via: 'page_registry',
56
52
  descendant: true,
57
53
  },
58
54
  sys_ux_page_property: {
59
- via: (parent, child) =>
60
- child.getCreator()?.getName() === 'WorkspacePlugin' &&
61
- parent.getId().getValue() ===
62
- (child.get('page').isRecord()
63
- ? child.get('page').asRecord().getId().getValue()
64
- : child.get('page').asString().getValue()),
55
+ via: 'page',
65
56
  descendant: true,
66
57
  },
67
58
  sys_ux_app_config: {
68
- via: (parent, child) =>
69
- child.getCreator()?.getName() === 'WorkspacePlugin' &&
70
- child.getId().getValue() ===
71
- (parent.get('admin_panel').isRecord()
72
- ? parent.get('admin_panel').asRecord().getId().getValue()
73
- : parent.get('admin_panel').asString().getValue()),
59
+ via: 'admin_panel',
60
+ inverse: true,
74
61
  descendant: true,
75
62
  relationships: {
76
63
  sys_ux_screen: {
77
- via: (parent, child) =>
78
- child.getCreator()?.getName() === 'WorkspacePlugin' &&
79
- parent.getId().getValue() ===
80
- (child.get('app_config').isRecord()
81
- ? child.get('app_config').asRecord().getId().getValue()
82
- : child.get('app_config').asString().getValue()),
64
+ via: 'app_config',
83
65
  descendant: true,
84
66
  relationships: {
85
67
  sys_ux_macroponent: {
86
- via: (parent, child) =>
87
- child.getCreator()?.getName() === 'WorkspacePlugin' &&
88
- child.getId().getValue() ===
89
- (parent.get('macroponent').isRecord()
90
- ? parent.get('macroponent').asRecord().getId().getValue()
91
- : parent.get('macroponent').asString().getValue()),
68
+ via: 'macroponent',
69
+ inverse: true,
92
70
  descendant: true,
93
71
  },
94
72
  },
95
73
  },
96
74
  sys_ux_app_route: {
97
- via: (parent, child) =>
98
- child.getCreator()?.getName() === 'WorkspacePlugin' &&
99
- parent.getId().getValue() ===
100
- (child.get('app_config').isRecord()
101
- ? child.get('app_config').asRecord().getId().getValue()
102
- : child.get('app_config').asString().getValue()),
75
+ via: 'app_config',
103
76
  descendant: true,
104
77
  relationships: {
105
78
  sys_ux_screen_type: {
106
- via: (parent, child) =>
107
- child.getCreator()?.getName() === 'WorkspacePlugin' &&
108
- child.getId().getValue() ===
109
- (parent.get('screen_type').isRecord()
110
- ? parent.get('screen_type').asRecord().getId().getValue()
111
- : parent.get('screen_type').asString().getValue()),
79
+ via: 'screen_type',
80
+ inverse: true,
112
81
  descendant: true,
113
82
  },
114
83
  },
@@ -116,10 +85,33 @@ export const WorkspacePlugin = Plugin.create({
116
85
  },
117
86
  },
118
87
  },
119
- async toShape(pageRegRecord, { factory, descendants }) {
88
+ async toShape(pageRegRecord, { factory, descendants: descendantsDB }) {
120
89
  if (pageRegRecord.getCreator()?.getName() !== 'WorkspacePlugin') {
121
90
  return { success: false }
122
91
  }
92
+
93
+ const excludedRecords: Record[] = []
94
+ const supportedRecords: Record[] = []
95
+ const partiallySupportedTables = new Set<string>([
96
+ 'sys_ux_registry_m2m_category',
97
+ 'sys_ux_page_property',
98
+ 'sys_ux_app_config',
99
+ 'sys_ux_screen',
100
+ 'sys_ux_macroponent',
101
+ 'sys_ux_app_route',
102
+ 'sys_ux_screen_type',
103
+ ])
104
+ descendantsDB.query().forEach((descendant) => {
105
+ if (
106
+ partiallySupportedTables.has(descendant.getTable()) &&
107
+ descendant.getCreator()?.getName() !== 'WorkspacePlugin'
108
+ ) {
109
+ excludedRecords.push(descendant)
110
+ } else {
111
+ supportedRecords.push(descendant)
112
+ }
113
+ })
114
+ const descendants = new Database(supportedRecords)
123
115
  // Get 'path' and 'title'
124
116
  // TODO 'active'
125
117
  const path: string = pageRegRecord.get('path').asString().getValue() ?? ''
@@ -226,6 +218,14 @@ export const WorkspacePlugin = Plugin.create({
226
218
  defaultRecordOverrides,
227
219
  })
228
220
 
221
+ if (excludedRecords.length > 0) {
222
+ return {
223
+ success: 'partial',
224
+ value: callExpressionShape,
225
+ unhandledRecords: excludedRecords,
226
+ }
227
+ }
228
+
229
229
  return {
230
230
  success: true,
231
231
  value: callExpressionShape,