@servicenow/sdk-build-plugins 4.5.0 → 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 (144) hide show
  1. package/dist/column-plugin.js +3 -7
  2. package/dist/column-plugin.js.map +1 -1
  3. package/dist/flow/flow-logic/flow-logic-diagnostics.js +5 -5
  4. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
  5. package/dist/flow/plugins/flow-action-definition-plugin.js +1229 -54
  6. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  7. package/dist/flow/plugins/flow-data-pill-plugin.js +5 -2
  8. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
  9. package/dist/flow/plugins/flow-definition-plugin.js +16 -42
  10. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  11. package/dist/flow/plugins/flow-diagnostics-plugin.d.ts +2 -2
  12. package/dist/flow/plugins/flow-diagnostics-plugin.js +2 -2
  13. package/dist/flow/plugins/flow-instance-plugin.js +68 -22
  14. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  15. package/dist/flow/plugins/step-definition-plugin.js +2 -1
  16. package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
  17. package/dist/flow/plugins/step-instance-plugin.d.ts +9 -1
  18. package/dist/flow/plugins/step-instance-plugin.js +649 -136
  19. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  20. package/dist/flow/plugins/wfa-datapill-plugin.js +20 -5
  21. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
  22. package/dist/flow/post-install.js +1 -0
  23. package/dist/flow/post-install.js.map +1 -1
  24. package/dist/flow/utils/complex-object-resolver.js +4 -1
  25. package/dist/flow/utils/complex-object-resolver.js.map +1 -1
  26. package/dist/flow/utils/complex-objects.js +1 -1
  27. package/dist/flow/utils/complex-objects.js.map +1 -1
  28. package/dist/flow/utils/flow-constants.d.ts +66 -2
  29. package/dist/flow/utils/flow-constants.js +402 -6
  30. package/dist/flow/utils/flow-constants.js.map +1 -1
  31. package/dist/flow/utils/flow-io-to-record.d.ts +1 -1
  32. package/dist/flow/utils/flow-io-to-record.js +37 -16
  33. package/dist/flow/utils/flow-io-to-record.js.map +1 -1
  34. package/dist/flow/utils/flow-shapes.js +4 -0
  35. package/dist/flow/utils/flow-shapes.js.map +1 -1
  36. package/dist/flow/utils/label-cache-parser.d.ts +9 -2
  37. package/dist/flow/utils/label-cache-parser.js +32 -4
  38. package/dist/flow/utils/label-cache-parser.js.map +1 -1
  39. package/dist/flow/utils/pill-shape-helpers.d.ts +15 -0
  40. package/dist/flow/utils/pill-shape-helpers.js +35 -0
  41. package/dist/flow/utils/pill-shape-helpers.js.map +1 -0
  42. package/dist/flow/utils/pill-string-parser.js +1 -0
  43. package/dist/flow/utils/pill-string-parser.js.map +1 -1
  44. package/dist/flow/utils/schema-to-flow-object.d.ts +6 -1
  45. package/dist/flow/utils/schema-to-flow-object.js +131 -15
  46. package/dist/flow/utils/schema-to-flow-object.js.map +1 -1
  47. package/dist/flow/utils/utils.d.ts +1 -0
  48. package/dist/flow/utils/utils.js +6 -1
  49. package/dist/flow/utils/utils.js.map +1 -1
  50. package/dist/form-plugin.js +7 -9
  51. package/dist/form-plugin.js.map +1 -1
  52. package/dist/inbound-email-action-plugin.d.ts +10 -0
  53. package/dist/inbound-email-action-plugin.js +128 -0
  54. package/dist/inbound-email-action-plugin.js.map +1 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +4 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/instance-scan-plugin.js +0 -5
  59. package/dist/instance-scan-plugin.js.map +1 -1
  60. package/dist/property-plugin.js +1 -1
  61. package/dist/property-plugin.js.map +1 -1
  62. package/dist/record-plugin.d.ts +7 -0
  63. package/dist/record-plugin.js +10 -2
  64. package/dist/record-plugin.js.map +1 -1
  65. package/dist/rest-api-plugin.js +8 -1
  66. package/dist/rest-api-plugin.js.map +1 -1
  67. package/dist/schedule-script/scheduled-script-plugin.js +8 -3
  68. package/dist/schedule-script/scheduled-script-plugin.js.map +1 -1
  69. package/dist/service-catalog/service-catalog-base.d.ts +18 -18
  70. package/dist/service-catalog/service-catalog-base.js +22 -22
  71. package/dist/service-catalog/service-catalog-base.js.map +1 -1
  72. package/dist/service-portal/header-footer-plugin.d.ts +2 -0
  73. package/dist/service-portal/header-footer-plugin.js +50 -0
  74. package/dist/service-portal/header-footer-plugin.js.map +1 -0
  75. package/dist/service-portal/menu-plugin.js +3 -22
  76. package/dist/service-portal/menu-plugin.js.map +1 -1
  77. package/dist/service-portal/page-plugin.js +3 -24
  78. package/dist/service-portal/page-plugin.js.map +1 -1
  79. package/dist/service-portal/page-route-map-plugin.d.ts +2 -0
  80. package/dist/service-portal/page-route-map-plugin.js +114 -0
  81. package/dist/service-portal/page-route-map-plugin.js.map +1 -0
  82. package/dist/service-portal/portal-plugin.js +21 -8
  83. package/dist/service-portal/portal-plugin.js.map +1 -1
  84. package/dist/service-portal/utils.d.ts +40 -2
  85. package/dist/service-portal/utils.js +283 -2
  86. package/dist/service-portal/utils.js.map +1 -1
  87. package/dist/service-portal/widget-plugin.js +9 -218
  88. package/dist/service-portal/widget-plugin.js.map +1 -1
  89. package/dist/static-content-plugin.js +4 -0
  90. package/dist/static-content-plugin.js.map +1 -1
  91. package/dist/table-plugin.js +190 -26
  92. package/dist/table-plugin.js.map +1 -1
  93. package/dist/ui-action-plugin.js +1 -4
  94. package/dist/ui-action-plugin.js.map +1 -1
  95. package/dist/ui-page-plugin.js +68 -13
  96. package/dist/ui-page-plugin.js.map +1 -1
  97. package/dist/view-plugin.js +8 -3
  98. package/dist/view-plugin.js.map +1 -1
  99. package/dist/workspace-plugin.js +39 -36
  100. package/dist/workspace-plugin.js.map +1 -1
  101. package/package.json +5 -4
  102. package/src/column-plugin.ts +3 -8
  103. package/src/flow/flow-logic/flow-logic-diagnostics.ts +5 -6
  104. package/src/flow/plugins/flow-action-definition-plugin.ts +1581 -61
  105. package/src/flow/plugins/flow-data-pill-plugin.ts +5 -2
  106. package/src/flow/plugins/flow-definition-plugin.ts +12 -47
  107. package/src/flow/plugins/flow-diagnostics-plugin.ts +2 -2
  108. package/src/flow/plugins/flow-instance-plugin.ts +98 -22
  109. package/src/flow/plugins/step-definition-plugin.ts +2 -1
  110. package/src/flow/plugins/step-instance-plugin.ts +772 -156
  111. package/src/flow/plugins/wfa-datapill-plugin.ts +25 -5
  112. package/src/flow/post-install.ts +1 -0
  113. package/src/flow/utils/complex-object-resolver.ts +4 -1
  114. package/src/flow/utils/complex-objects.ts +1 -1
  115. package/src/flow/utils/flow-constants.ts +421 -5
  116. package/src/flow/utils/flow-io-to-record.ts +43 -17
  117. package/src/flow/utils/flow-shapes.ts +4 -0
  118. package/src/flow/utils/label-cache-parser.ts +33 -4
  119. package/src/flow/utils/pill-shape-helpers.ts +42 -0
  120. package/src/flow/utils/pill-string-parser.ts +1 -0
  121. package/src/flow/utils/schema-to-flow-object.ts +183 -15
  122. package/src/flow/utils/utils.ts +12 -1
  123. package/src/form-plugin.ts +1 -3
  124. package/src/inbound-email-action-plugin.ts +145 -0
  125. package/src/index.ts +4 -0
  126. package/src/instance-scan-plugin.ts +0 -5
  127. package/src/property-plugin.ts +4 -1
  128. package/src/record-plugin.ts +14 -4
  129. package/src/rest-api-plugin.ts +7 -1
  130. package/src/schedule-script/scheduled-script-plugin.ts +14 -3
  131. package/src/service-catalog/service-catalog-base.ts +22 -22
  132. package/src/service-portal/header-footer-plugin.ts +57 -0
  133. package/src/service-portal/menu-plugin.ts +1 -23
  134. package/src/service-portal/page-plugin.ts +3 -28
  135. package/src/service-portal/page-route-map-plugin.ts +124 -0
  136. package/src/service-portal/portal-plugin.ts +33 -10
  137. package/src/service-portal/utils.ts +404 -3
  138. package/src/service-portal/widget-plugin.ts +14 -290
  139. package/src/static-content-plugin.ts +3 -0
  140. package/src/table-plugin.ts +226 -36
  141. package/src/ui-action-plugin.ts +1 -8
  142. package/src/ui-page-plugin.ts +76 -13
  143. package/src/view-plugin.ts +10 -4
  144. package/src/workspace-plugin.ts +43 -43
@@ -321,6 +321,20 @@ type SysDocumentationProperties = {
321
321
  help: string | undefined
322
322
  }
323
323
 
324
+ type SysDictionaryOverrideProperties = {
325
+ name: string
326
+ element: string
327
+ base_table: string | undefined
328
+ default: string | undefined
329
+ calculation: string | undefined
330
+ reference_qual: string | undefined
331
+ read_only_option: string | undefined
332
+ dependent: string | undefined
333
+ mandatory: boolean | undefined
334
+ display: boolean | undefined
335
+ attributes: string | undefined
336
+ }
337
+
324
338
  /**
325
339
  * The access levels are mapped to the following values on the platform
326
340
  *
@@ -390,7 +404,7 @@ export const TablePlugin = Plugin.create({
390
404
  return { success: false }
391
405
  }
392
406
 
393
- const recordDefs = tableDefToRecordProperties(tableDef, config.tableDefaultLanguage)
407
+ const recordDefs = tableDefToRecordProperties(tableDef, config.defaultLanguage)
394
408
  const records: Record[] = []
395
409
  for (const [key, table] of [
396
410
  ['sysDbObject', 'sys_db_object'],
@@ -398,6 +412,7 @@ export const TablePlugin = Plugin.create({
398
412
  ['sysChoice', 'sys_choice'],
399
413
  ['sysIndex', 'sys_index'],
400
414
  ['sysDocumentation', 'sys_documentation'],
415
+ ['sysDictionaryOverride', 'sys_dictionary_override'],
401
416
  ] as const) {
402
417
  for (const rec of [recordDefs[key]].flat()) {
403
418
  records.push(
@@ -449,6 +464,10 @@ export const TablePlugin = Plugin.create({
449
464
  },
450
465
  },
451
466
  },
467
+ sys_dictionary_override: {
468
+ via: { name: 'name' },
469
+ descendant: true,
470
+ },
452
471
  ua_table_licensing_config: {
453
472
  descendant: true,
454
473
  via: { name: 'name' },
@@ -474,9 +493,12 @@ export const TablePlugin = Plugin.create({
474
493
  const schema: { [key: string]: CallExpressionShape } = {}
475
494
  let displayColumn: string | undefined
476
495
  const columns = descendants.query('sys_dictionary')
496
+ const overrides = descendants.query('sys_dictionary_override')
477
497
  let collectionRecord: Record
478
498
  const choices = descendants.query('sys_choice')
479
499
  const documentation = descendants.query('sys_documentation')
500
+
501
+ // Process regular columns
480
502
  for (const column of columns) {
481
503
  if (column.get('internal_type').getValue() === 'collection') {
482
504
  // 'collection' sys_dictionary record only has table properties
@@ -489,13 +511,89 @@ export const TablePlugin = Plugin.create({
489
511
  documentation: documentation.filter(
490
512
  (d) => d.get('element').ifString()?.getValue() === columnName
491
513
  ),
492
- tableDefaultLanguage: config.tableDefaultLanguage,
514
+ tableDefaultLanguage: config.defaultLanguage,
493
515
  })
494
516
  if (column.get('display').ifDefined()?.toBoolean().getValue()) {
495
517
  displayColumn = columnName
496
518
  }
497
519
  }
498
520
 
521
+ // Process dictionary overrides as OverrideColumn
522
+ for (const override of overrides) {
523
+ const columnName = override.get('element').asString().getValue()
524
+ schema[columnName] = new CallExpressionShape({
525
+ source: override,
526
+ callee: 'OverrideColumn',
527
+ args: [
528
+ override.transform(({ $ }) => ({
529
+ baseTable: $.from('base_table'),
530
+ default: $.from('default_value_override', 'default_value').map((flag, value) => {
531
+ return flag.toBoolean()?.getValue() ? value.ifString()?.getValue() : undefined
532
+ }),
533
+ calculation: $.from('calculation_override', 'calculation').map((flag, value) => {
534
+ return flag.toBoolean()?.getValue() ? value.ifString()?.getValue() : undefined
535
+ }),
536
+ referenceQualifier: $.from('reference_qual_override', 'reference_qual').map(
537
+ (flag, value) => {
538
+ return flag.toBoolean()?.getValue() ? value.ifString()?.getValue() : undefined
539
+ }
540
+ ),
541
+ readOnlyOption: $.from('read_only_option_override', 'read_only_option').map(
542
+ (flag, value) => {
543
+ return flag.toBoolean()?.getValue() ? value.ifString()?.getValue() : undefined
544
+ }
545
+ ),
546
+ dependent: $.from('dependent_override', 'dependent').map((flag, value) => {
547
+ return flag.toBoolean()?.getValue() ? value.ifString()?.getValue() : undefined
548
+ }),
549
+ mandatory: $.from('mandatory_override', 'mandatory').map((flag, value) => {
550
+ return flag.toBoolean()?.getValue() ? value.toBoolean()?.getValue() : undefined
551
+ }),
552
+ attributes: $.from('attributes_override', 'attributes').map((flag, attrs) => {
553
+ if (!flag.toBoolean()?.getValue() || !attrs.isString()) {
554
+ return undefined
555
+ }
556
+ const result: { [key: string]: string | number | boolean } = {}
557
+ attrs
558
+ .toString()
559
+ .getValue()
560
+ .split(',')
561
+ .forEach((attr) => {
562
+ if (attr === '') {
563
+ return
564
+ }
565
+ const [key, value] = attr.split('=').map((s) => s.trim())
566
+ if (!key || value === undefined) {
567
+ return
568
+ }
569
+ if (value === 'true') {
570
+ result[key] = true
571
+ } else if (value === 'false') {
572
+ result[key] = false
573
+ } else {
574
+ // Try to parse as number
575
+ const numValue = Number(value)
576
+ if (!isNaN(numValue) && value !== '') {
577
+ result[key] = numValue
578
+ } else {
579
+ result[key] = value
580
+ }
581
+ }
582
+ })
583
+ return result
584
+ }),
585
+ display: $.from('display_override').map((v) => {
586
+ if (!v.ifDefined()) {
587
+ return undefined
588
+ }
589
+ const boolValue = v.toBoolean().getValue()
590
+ return boolValue === true ? true : undefined
591
+ }),
592
+ })),
593
+ ],
594
+ })
595
+ }
596
+
499
597
  const tableDocumentation = documentation.filter((d) => !d.get('element').getValue())
500
598
  const licensing = descendants.query('ua_table_licensing_config')
501
599
  const autoNumber = descendants.query('sys_number')
@@ -629,14 +727,14 @@ export const TablePlugin = Plugin.create({
629
727
  (label) =>
630
728
  label.ifString() ??
631
729
  (tableDocumentation.length &&
632
- !isDefaultDocumentation('', tableDocumentation, config.tableDefaultLanguage)
730
+ !isDefaultDocumentation('', tableDocumentation, config.defaultLanguage)
633
731
  ? tableDocumentation.map((doc) =>
634
732
  doc
635
733
  .transform(({ $ }) => ({
636
734
  label: $.def(''),
637
735
  help: $.def(''),
638
736
  hint: $.def(''),
639
- language: $.def(config.tableDefaultLanguage),
737
+ language: $.def(config.defaultLanguage),
640
738
  plural: $.def(''),
641
739
  url: $.def(''),
642
740
  urlTarget: $.from('url_target').def(''),
@@ -718,8 +816,9 @@ export const TablePlugin = Plugin.create({
718
816
  const documentation = descendants.query('sys_documentation')
719
817
  const licensing = descendants.query('ua_table_licensing_config')
720
818
  const autoNumber = descendants.query('sys_number')
721
- let collectionRecord: Record | undefined
819
+ const overrides = descendants.query('sys_dictionary_override')
722
820
  let displayColumn: string | undefined
821
+ let collectionRecord: Record | undefined
723
822
  const elements: XMLBuilder[] = []
724
823
  for (const column of columns) {
725
824
  const displayValue = column.get('display').ifBoolean()?.getValue()
@@ -889,7 +988,7 @@ export const TablePlugin = Plugin.create({
889
988
  !isDefaultDocumentation(
890
989
  doc.get('element')?.toString().getValue(),
891
990
  [doc],
892
- config.tableDefaultLanguage
991
+ config.defaultLanguage
893
992
  )
894
993
  ),
895
994
  config,
@@ -903,6 +1002,7 @@ export const TablePlugin = Plugin.create({
903
1002
  )
904
1003
 
905
1004
  const autoNumberFiles = await generateRecordXml(autoNumber, config, transform)
1005
+ const overrideFiles = await generateRecordXml(overrides, config, transform)
906
1006
 
907
1007
  return {
908
1008
  success: true,
@@ -916,6 +1016,7 @@ export const TablePlugin = Plugin.create({
916
1016
  ...documentationFiles,
917
1017
  ...licensingFiles,
918
1018
  ...autoNumberFiles,
1019
+ ...overrideFiles,
919
1020
  ],
920
1021
  }
921
1022
  },
@@ -927,6 +1028,13 @@ export const TablePlugin = Plugin.create({
927
1028
  value: `sys_dictionary_${record.get('name').getValue()}_${record.get('element').getValue() || 'null'}`,
928
1029
  }),
929
1030
  },
1031
+ sys_dictionary_override: {
1032
+ coalesce: ['name', 'element'],
1033
+ getUpdateName: (record) => ({
1034
+ success: true,
1035
+ value: `sys_dictionary_override_${record.get('name').getValue()}_${record.get('element').getValue() || 'null'}`,
1036
+ }),
1037
+ },
930
1038
  sys_documentation: {
931
1039
  coalesce: ['name', 'element', 'language'],
932
1040
  getUpdateName: (record) => ({
@@ -1021,7 +1129,7 @@ export const TablePlugin = Plugin.create({
1021
1129
  ignoreColumnNameCheck = true
1022
1130
  }
1023
1131
 
1024
- // sys_dictionary
1132
+ // sys_dictionary and sys_dictionary_override
1025
1133
  const schema = table.get('schema').asObject()
1026
1134
  const columnIdsMap = new Map<string, string>()
1027
1135
  for (const [name, column] of schema.entries()) {
@@ -1031,37 +1139,116 @@ export const TablePlugin = Plugin.create({
1031
1139
  'Column name must only contain lowercase letters, numbers, and underscores'
1032
1140
  )
1033
1141
  }
1142
+
1143
+ // Check if this is an OverrideColumn
1034
1144
  if (
1035
- !ignoreColumnNameCheck &&
1036
- !tableNameMatch &&
1037
- !isSNScope(scopeName) &&
1038
- scopeName !== 'global' &&
1039
- !name.match(scopeRegex)
1145
+ column.is(CallExpressionShape) &&
1146
+ column.as(CallExpressionShape).getCallee() === 'OverrideColumn'
1040
1147
  ) {
1041
- // 'sn' and 'now' scoped apps ignore this validation
1042
- diagnostics.error(
1043
- column.getOriginalNode().getParentIfKind(ts.SyntaxKind.PropertyAssignment) ?? column,
1044
- `Column name should be prefixed with scope '${scopeName}_' if table name does not contain prefix`
1045
- )
1046
- } else if (scopeName === 'global' && !globalTableNameMatch && !name.match(globalRegex)) {
1047
- diagnostics.error(
1048
- column.getOriginalNode().getParentIfKind(ts.SyntaxKind.PropertyAssignment) ?? column,
1049
- `Column name should be prefixed with 'u_' custom prefix if table name does not contain this prefix, such as when adding columns to an existing global table`
1050
- )
1051
- }
1052
- const display = table.get('display').ifString()?.getValue() === name
1053
- const result = await transform.toRecord(
1054
- addFieldsToColumn(
1055
- { name, table: tableName.getValue(), display },
1056
- column.as(CallExpressionShape)
1148
+ // This is an OverrideColumn() call - get the object from its first argument
1149
+ const columnObj = column.as(CallExpressionShape).getArgument(0).asObject()
1150
+
1151
+ // Handle OverrideColumn - create sys_dictionary_override record
1152
+ // Validate that the table extends another table
1153
+ const extendsTable = table.get('extends').ifString()?.getValue()
1154
+ if (!extendsTable) {
1155
+ diagnostics.error(
1156
+ column,
1157
+ `Cannot use OverrideColumn in table '${tableName.getValue()}' because it does not extend another table`
1158
+ )
1159
+ return { success: false }
1160
+ }
1161
+
1162
+ // Use baseTable if provided, otherwise default to extends
1163
+ const baseTableValue = columnObj.get('baseTable')
1164
+ let baseTable: string
1165
+
1166
+ if (baseTableValue.ifString()) {
1167
+ baseTable = baseTableValue.asString().getValue()
1168
+ } else {
1169
+ // Default to extends if baseTable not provided
1170
+ baseTable = extendsTable
1171
+ }
1172
+
1173
+ // Create sys_dictionary_override record
1174
+ const overrideRecord = await factory.createRecord({
1175
+ source: statement ?? callExpression,
1176
+ table: 'sys_dictionary_override',
1177
+ properties: columnObj.transform(({ $ }) => ({
1178
+ name: $.val(tableName),
1179
+ element: $.val(name),
1180
+ base_table: $.val(baseTable),
1181
+ default_value: $.from('default'),
1182
+ default_value_override: $.from('default').map((v) => !!v.ifDefined()),
1183
+ calculation: $.from('calculation'),
1184
+ calculation_override: $.from('calculation').map((v) => !!v.ifDefined()),
1185
+ reference_qual: $.from('referenceQualifier'),
1186
+ reference_qual_override: $.from('referenceQualifier').map((v) => !!v.ifDefined()),
1187
+ read_only_option: $.from('readOnlyOption'),
1188
+ read_only_option_override: $.from('readOnlyOption').map((v) => !!v.ifDefined()),
1189
+ read_only: $.from('readOnlyOption').map((readOnlyOption) => {
1190
+ // read_only should be true if readOnlyOption has a value
1191
+ return !!readOnlyOption.ifDefined()
1192
+ }),
1193
+ read_only_override: $.from('readOnlyOption').map((v) => !!v.ifDefined()),
1194
+ dependent: $.from('dependent'),
1195
+ dependent_override: $.from('dependent').map((v) => !!v.ifDefined()),
1196
+ mandatory: $.from('mandatory').toBoolean().def(false),
1197
+ mandatory_override: $.from('mandatory').map((v) => !!v.ifDefined()),
1198
+ attributes: $.from('attributes').map((attrs) => {
1199
+ if (!attrs.isObject()) {
1200
+ return
1201
+ }
1202
+ const attrsObj = attrs.asObject().getValue()
1203
+ return Object.entries(attrsObj)
1204
+ .map(([key, value]) => `${key}=${value}`)
1205
+ .join(',')
1206
+ }),
1207
+ attributes_override: $.from('attributes').map((v) => !!v.ifDefined()),
1208
+ display_override: $.from('display').map((v) => {
1209
+ const boolShape = v.ifBoolean()
1210
+ if (!boolShape) {
1211
+ return false
1212
+ }
1213
+ return boolShape.getValue() === true
1214
+ }),
1215
+ })),
1216
+ })
1217
+ relatedRecords.push(overrideRecord)
1218
+ } else {
1219
+ // Handle regular column - create sys_dictionary record
1220
+ if (
1221
+ !ignoreColumnNameCheck &&
1222
+ !tableNameMatch &&
1223
+ !isSNScope(scopeName) &&
1224
+ scopeName !== 'global' &&
1225
+ !name.match(scopeRegex)
1226
+ ) {
1227
+ // 'sn' and 'now' scoped apps ignore this validation
1228
+ diagnostics.error(
1229
+ column.getOriginalNode().getParentIfKind(ts.SyntaxKind.PropertyAssignment) ?? column,
1230
+ `Column name should be prefixed with scope '${scopeName}_' if table name does not contain prefix`
1231
+ )
1232
+ } else if (scopeName === 'global' && !globalTableNameMatch && !name.match(globalRegex)) {
1233
+ diagnostics.error(
1234
+ column.getOriginalNode().getParentIfKind(ts.SyntaxKind.PropertyAssignment) ?? column,
1235
+ `Column name should be prefixed with 'u_' custom prefix if table name does not contain this prefix, such as when adding columns to an existing global table`
1236
+ )
1237
+ }
1238
+ const display = table.get('display').ifString()?.getValue() === name
1239
+ const result = await transform.toRecord(
1240
+ addFieldsToColumn(
1241
+ { name, table: tableName.getValue(), display },
1242
+ column.as(CallExpressionShape)
1243
+ )
1057
1244
  )
1058
- )
1059
- if (!result.success) {
1060
- diagnostics.error(column, 'Invalid column in table schema')
1061
- return { success: false }
1245
+ if (!result.success) {
1246
+ diagnostics.error(column, 'Invalid column in table schema')
1247
+ return { success: false }
1248
+ }
1249
+ relatedRecords.push(result.value)
1250
+ columnIdsMap.set(name, result.value.getId().getValue())
1062
1251
  }
1063
- relatedRecords.push(result.value)
1064
- columnIdsMap.set(name, result.value.getId().getValue())
1065
1252
  }
1066
1253
 
1067
1254
  // sys_index
@@ -1193,7 +1380,7 @@ export const TablePlugin = Plugin.create({
1193
1380
  properties: {
1194
1381
  name: tableName,
1195
1382
  element: undefined,
1196
- language: config.tableDefaultLanguage,
1383
+ language: config.defaultLanguage,
1197
1384
  label: table.get('label').ifString() ?? tableName,
1198
1385
  plural: generatePlural(
1199
1386
  table.get('label')?.ifString()?.getValue() ?? tableName.getValue()
@@ -1292,7 +1479,7 @@ export const TablePlugin = Plugin.create({
1292
1479
  .map((actions) => hasAction('create', actions))
1293
1480
  .def(false),
1294
1481
  is_extendable: $.from('extensible').toBoolean().def(false),
1295
- label: $.map((label) => getLabelForDefaultLanguage(label, config.tableDefaultLanguage)),
1482
+ label: $.map((label) => getLabelForDefaultLanguage(label, config.defaultLanguage)),
1296
1483
  live_feed_enabled: $.from('liveFeed').toBoolean().def(false),
1297
1484
  name: $,
1298
1485
  scriptable_table: $.from('scriptableTable').toBoolean().def(false),
@@ -1469,6 +1656,7 @@ function tableDefToRecordProperties(
1469
1656
  sysChoice: SysChoiceProperties[]
1470
1657
  sysIndex: SysIndexProperties[]
1471
1658
  sysDocumentation: SysDocumentationProperties[]
1659
+ sysDictionaryOverride: SysDictionaryOverrideProperties[]
1472
1660
  } {
1473
1661
  const sysDbObject: SysDbObjectProperties = {
1474
1662
  name: tableDef.name,
@@ -1490,6 +1678,7 @@ function tableDefToRecordProperties(
1490
1678
  const sysDictionary: SysDictionaryProperties[] = []
1491
1679
  const sysChoice: SysChoiceProperties[] = []
1492
1680
  const sysDocumentation: SysDocumentationProperties[] = []
1681
+ const sysDictionaryOverride: SysDictionaryOverrideProperties[] = []
1493
1682
 
1494
1683
  // table documentation
1495
1684
  if (tableDef.label && tableDef.plural) {
@@ -1647,6 +1836,7 @@ function tableDefToRecordProperties(
1647
1836
  sysChoice,
1648
1837
  sysIndex,
1649
1838
  sysDocumentation,
1839
+ sysDictionaryOverride,
1650
1840
  }
1651
1841
  }
1652
1842
 
@@ -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
@@ -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
  },