@servicenow/sdk-build-core 2.2.9 → 3.0.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 (71) hide show
  1. package/dist/GUID.js +2 -5
  2. package/dist/GUID.js.map +1 -1
  3. package/dist/IncludePaths.d.ts +25 -0
  4. package/dist/IncludePaths.js +97 -0
  5. package/dist/IncludePaths.js.map +1 -0
  6. package/dist/Keys.d.ts +1 -0
  7. package/dist/Keys.js +3 -0
  8. package/dist/Keys.js.map +1 -1
  9. package/dist/XML.d.ts +7 -0
  10. package/dist/XML.js +19 -2
  11. package/dist/XML.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/plugins/Context.d.ts +12 -5
  15. package/dist/plugins/behaviors/Composer.d.ts +2 -1
  16. package/dist/plugins/behaviors/Composer.js.map +1 -1
  17. package/dist/plugins/behaviors/Serializer.d.ts +3 -2
  18. package/dist/plugins/behaviors/extractors/Data.d.ts +5 -1
  19. package/dist/plugins/behaviors/extractors/Data.js +11 -1
  20. package/dist/plugins/behaviors/extractors/Data.js.map +1 -1
  21. package/dist/plugins/index.d.ts +1 -1
  22. package/dist/plugins/util/CallExpression.js +15 -1
  23. package/dist/plugins/util/CallExpression.js.map +1 -1
  24. package/dist/plugins/util/CodeTransformation.d.ts +19 -7
  25. package/dist/plugins/util/CodeTransformation.js +89 -44
  26. package/dist/plugins/util/CodeTransformation.js.map +1 -1
  27. package/dist/plugins/util/index.d.ts +0 -1
  28. package/dist/plugins/util/index.js +0 -1
  29. package/dist/plugins/util/index.js.map +1 -1
  30. package/dist/util/Debug.d.ts +1 -5
  31. package/dist/util/Debug.js +0 -16
  32. package/dist/util/Debug.js.map +1 -1
  33. package/dist/util/RuntimeTableSchema.d.ts +5 -0
  34. package/dist/util/RuntimeTableSchema.js +58 -0
  35. package/dist/util/RuntimeTableSchema.js.map +1 -0
  36. package/dist/util/Util.d.ts +0 -4
  37. package/dist/util/Util.js +0 -14
  38. package/dist/util/Util.js.map +1 -1
  39. package/dist/util/index.d.ts +1 -1
  40. package/dist/util/index.js +1 -1
  41. package/dist/util/index.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/GUID.ts +1 -1
  44. package/src/IncludePaths.ts +122 -0
  45. package/src/Keys.ts +4 -0
  46. package/src/XML.ts +22 -2
  47. package/src/index.ts +1 -1
  48. package/src/plugins/Context.ts +12 -5
  49. package/src/plugins/behaviors/Composer.ts +2 -1
  50. package/src/plugins/behaviors/Serializer.ts +3 -2
  51. package/src/plugins/behaviors/extractors/Data.ts +11 -1
  52. package/src/plugins/index.ts +1 -1
  53. package/src/plugins/util/CallExpression.ts +24 -1
  54. package/src/plugins/util/CodeTransformation.ts +109 -54
  55. package/src/plugins/util/index.ts +0 -1
  56. package/src/util/Debug.ts +1 -20
  57. package/src/util/RuntimeTableSchema.ts +44 -0
  58. package/src/util/Util.ts +0 -21
  59. package/src/util/index.ts +1 -1
  60. package/dist/BuildOptions.d.ts +0 -46
  61. package/dist/BuildOptions.js +0 -45
  62. package/dist/BuildOptions.js.map +0 -1
  63. package/dist/plugins/util/ConfigurationFunction.d.ts +0 -105
  64. package/dist/plugins/util/ConfigurationFunction.js +0 -377
  65. package/dist/plugins/util/ConfigurationFunction.js.map +0 -1
  66. package/dist/util/XMLJsonBuilder.d.ts +0 -18
  67. package/dist/util/XMLJsonBuilder.js +0 -59
  68. package/dist/util/XMLJsonBuilder.js.map +0 -1
  69. package/src/BuildOptions.ts +0 -26
  70. package/src/plugins/util/ConfigurationFunction.ts +0 -468
  71. package/src/util/XMLJsonBuilder.ts +0 -64
@@ -0,0 +1,122 @@
1
+ import { FileSystem, path } from '@servicenow/sdk-project'
2
+ import { Context } from './plugins'
3
+
4
+ export type IncludePaths = {
5
+ [table: string]: {
6
+ fields: { [field: string]: string }[]
7
+ }
8
+ }
9
+
10
+ type CustomizedFieldPaths = {
11
+ field: string
12
+ path: string
13
+ }
14
+
15
+ export type CustomizedIncludePathForTable = {
16
+ table: string
17
+ guid: string
18
+ fields: CustomizedFieldPaths[]
19
+ }
20
+
21
+ export type CustomizedIncludePaths = {
22
+ [table: string]: CustomizedIncludePathForTable[]
23
+ }
24
+
25
+ export const DefaultIncludePaths: IncludePaths = {
26
+ // Example of how to define include paths:
27
+ // sys_script_include: {
28
+ // fields: [{ script: '{{name}}.server.js' }],
29
+ // },
30
+ }
31
+
32
+ export const getIncludePaths = (context: Context) => {
33
+ const mappingFileName = path.join(context.app.rootDir, 'now.includes-mapping.json')
34
+ const compilerSourceFile = context.compiler.getSourceFile(mappingFileName)
35
+ let fileText = ''
36
+ if (compilerSourceFile) {
37
+ fileText = compilerSourceFile.getFullText()
38
+ } else {
39
+ // now-sdk fetch doesn't load the file into the compiler, so
40
+ // for now we'll just read the file from the filesystem
41
+ fileText = FileSystem.existsSync(context.fs, mappingFileName)
42
+ ? context.fs.readFileSync(mappingFileName).toString()
43
+ : ''
44
+ }
45
+ return fileText ? (JSON.parse(fileText) as IncludePaths) : (DefaultIncludePaths as IncludePaths)
46
+ }
47
+
48
+ export const calculateCustomPathsFromDocs = (context: Context): CustomizedIncludePaths => {
49
+ const includePaths = getIncludePaths(context)
50
+ const customizedIncludePaths = context
51
+ .getAllDocuments()
52
+ .filter((doc) => doc.kind === 'record')
53
+ .map((doc) => ({ table: doc.data?.['table'], guid: doc.guid, data: doc.data?.['data'] }))
54
+ .filter((record) => includePaths[record.table])
55
+ .map((record) => ({
56
+ table: record.table,
57
+ guid: record.guid,
58
+ fields:
59
+ (includePaths[record.table]?.fields || []).flatMap(Object.entries).map(([field, path]) => ({
60
+ field,
61
+ path: replaceTemplate(path, { sys_id: record.guid, ...record.data }),
62
+ })) || [],
63
+ }))
64
+ .reduce((acc, record) => {
65
+ acc[record.table] = acc[record.table] || []
66
+ acc[record.table].push(record)
67
+ return acc
68
+ }, {})
69
+
70
+ return dedupeFilenames(customizedIncludePaths)
71
+ }
72
+
73
+ export function replaceTemplate(template: string, properties: Record<string, any>) {
74
+ let replacedString = template
75
+ const matches = template.matchAll(/{{([a-z_]+)}}/g)
76
+ for (const m of matches) {
77
+ const [, field] = m
78
+ if (field) {
79
+ const sanitizedValue = sanitizeForFilesystem(properties[field])
80
+ replacedString = replacedString.replaceAll(`{{${field}}}`, sanitizedValue)
81
+ }
82
+ }
83
+ return replacedString
84
+ }
85
+
86
+ const IllegalCharacters = /[/<>?\\:|*"]/g
87
+ const Reserved = /^\.+$/
88
+ const WindowsTrailing = /[. ]+$/
89
+
90
+ function sanitizeForFilesystem(filename: string) {
91
+ return filename.replace(IllegalCharacters, '_').replace(Reserved, '').replace(WindowsTrailing, '')
92
+ }
93
+
94
+ function dedupeFilename(filePath: string, fixedCount: { [path: string]: number }) {
95
+ const parsedPath = path.parse(filePath)
96
+ fixedCount[filePath] = (fixedCount[filePath] || 0) + 1
97
+ const pathParts = parsedPath.base.split('.')
98
+ pathParts[0] = `${pathParts[0]}-${fixedCount[filePath]}`
99
+ return path.join(parsedPath.dir, pathParts.join('.'))
100
+ }
101
+
102
+ function dedupeFilenames(customizedFieldPaths: CustomizedIncludePaths) {
103
+ const pathCount = {}
104
+ for (const table in customizedFieldPaths) {
105
+ const paths = customizedFieldPaths[table]!.flatMap((record) => record.fields.map((f) => f.path))
106
+ paths.forEach((p) => (pathCount[p] = (pathCount[p] || 0) + 1))
107
+ }
108
+
109
+ const fixedCount = {}
110
+ for (const table in customizedFieldPaths) {
111
+ const pathForTable = customizedFieldPaths[table] || []
112
+ for (const includePathForTable of pathForTable) {
113
+ for (let i = 0; i < includePathForTable.fields.length; i++) {
114
+ const p = includePathForTable.fields[i]?.path
115
+ if (p && pathCount[p] > 1) {
116
+ includePathForTable.fields[i]!.path = dedupeFilename(p, fixedCount)
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return customizedFieldPaths
122
+ }
package/src/Keys.ts CHANGED
@@ -141,6 +141,10 @@ export class Keys implements Now.Internal.KeysRegistry {
141
141
  return !!this.explicit[key]
142
142
  }
143
143
 
144
+ getIdUsingExplicitKey(key: string | number) {
145
+ return this.explicit[key]?.id
146
+ }
147
+
144
148
  findExplicitKeyById(id: string) {
145
149
  return Object.entries(this.explicit).find(([, v]) => v.id === id)?.[0]
146
150
  }
package/src/XML.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Action, type Context } from './plugins'
2
2
  import { create } from 'xmlbuilder2'
3
3
  import type { XMLBuilder } from 'xmlbuilder2/lib/interfaces'
4
+ import { maybeGetFieldToTagMap } from './util/RuntimeTableSchema'
4
5
 
5
6
  export type XmlValue = string | number | boolean
6
7
 
@@ -17,13 +18,22 @@ export function unloadBuilder(context: Context) {
17
18
  xml,
18
19
  end,
19
20
  record: (tableName: string, id: string | number, action: Action = 'INSERT_OR_UPDATE') => {
20
- const rec = recordXml(xml, tableName, id, { attr: { action } })
21
+ const rec = recordXml(xml, tableName, id, { attr: { action }, context })
21
22
  rec.addSysScope(context)
22
23
  return rec
23
24
  },
24
25
  }
25
26
  }
26
27
 
28
+ /**
29
+ * If the code contains the character sequence ']]>' it will be parsed as the end of
30
+ * the CDATA section. To avoid this, we need to split the sequence by dividing the
31
+ * content into two CDATA sections at that point.
32
+ */
33
+ export function sanitizeNestedCdataTags(content: string): string {
34
+ return content.replace(/]]>/g, ']]]]><![CDATA[>')
35
+ }
36
+
27
37
  export function recordXml(
28
38
  xml: XMLBuilder,
29
39
  tableName: string,
@@ -31,10 +41,12 @@ export function recordXml(
31
41
  options: {
32
42
  attr?: Record<string, string>
33
43
  excludeScopeElement?: boolean
44
+ context?: Context
34
45
  } = {}
35
46
  ) {
36
47
  const recordXml = xml.ele(tableName, options.attr || { action: 'INSERT_OR_UPDATE' })
37
48
  recordXml.ele('sys_id', undefined).txt(`${id}`)
49
+ const fieldToTagMap = maybeGetFieldToTagMap(tableName, options.context?.getAllDocuments() || [])
38
50
 
39
51
  // Record Builder
40
52
  return {
@@ -49,7 +61,15 @@ export function recordXml(
49
61
  }
50
62
 
51
63
  let fieldXml
52
- if (columnName === 'script' || columnName === 'operation_script') {
64
+ const tag = fieldToTagMap[columnName]
65
+ if (
66
+ columnName === 'script' ||
67
+ columnName === 'operation_script' ||
68
+ tag === 'script' ||
69
+ tag === 'html' ||
70
+ tag === 'xml'
71
+ ) {
72
+ value = tag === 'xml' ? sanitizeNestedCdataTags(value as string) : value
53
73
  fieldXml = recordXml.ele(columnName, attributes).dat(value as string)
54
74
  } else {
55
75
  fieldXml = recordXml.ele(columnName, attributes).txt(`${value}`)
package/src/index.ts CHANGED
@@ -4,4 +4,4 @@ export * from './XML'
4
4
  export * from './plugins'
5
5
  export * from './GUID'
6
6
  export * from './Keys'
7
- export * from './BuildOptions'
7
+ export * from './IncludePaths'
@@ -13,7 +13,7 @@ import {
13
13
  import { Keys } from '../Keys'
14
14
  import { Plugin } from './Plugin'
15
15
  import { FluentDiagnostic } from './Diagnostic'
16
- import { ProjectContext, ts, type Diagnostic } from '@servicenow/sdk-project'
16
+ import { ProjectContext, SupportedNode, ts, type Diagnostic } from '@servicenow/sdk-project'
17
17
 
18
18
  // These are only imported so they can be referenced in JS docs
19
19
  import type {
@@ -99,7 +99,7 @@ export type Context = ProjectContext & {
99
99
  * @param plugins An optional array of {@linkcode Plugin}s to use.
100
100
  * @returns a {@linkcode LinkedDocument} array.
101
101
  */
102
- composeEntities(this: Context, data: EntityData[], plugins?: Plugin[]): LinkedDocument[]
102
+ composeEntities(this: Context, data: EntityData[], plugins?: Plugin[]): Promise<LinkedDocument[]>
103
103
 
104
104
  /**
105
105
  * Accepts an array of XML data and returns an {@linkcode UnlinkedDocument}
@@ -204,10 +204,11 @@ export type Context = ProjectContext & {
204
204
  usageCount: { [key: string]: number }
205
205
 
206
206
  /**
207
- * An object of XML file names that was marked as handled or ignored by code transformation during the build process.
207
+ * Returns an object of XML file names that was marked as handled or ignored by code transformation during the build process.
208
208
  */
209
- handledXmls: Record<string, HandledStates>
209
+ getHandledXmls: () => Record<string, HandledInfo>
210
210
 
211
+ updateHandledXMLs: (xml: string | undefined, status: HandledStates, info?: string, node?: SupportedNode) => void
211
212
  setAllDocuments: (documents: Document[]) => void
212
213
  getAllDocuments: () => Document[]
213
214
  getDocumentMap: () => DocumentMap
@@ -223,10 +224,16 @@ export type Context = ProjectContext & {
223
224
  keys: Keys
224
225
 
225
226
  explicitKeyExists: Keys['explicitKeyExists']
227
+ getIdUsingExplicitKey: Keys['getIdUsingExplicitKey']
226
228
  registerExplicitId: Keys['registerExplicitId']
227
229
  registerCompositeId: Keys['registerCompositeId']
228
230
  getDeletedAndUnusedIds: Keys['getDeletedAndUnusedIds']
229
231
  getKeysSourceFile(): ts.SourceFile
230
232
  }
231
233
 
232
- export type HandledStates = 'handled' | 'ignored' | 'skipped' | 'unchanged'
234
+ export type HandledInfo = {
235
+ state: HandledStates
236
+ info?: string | undefined
237
+ node?: SupportedNode | undefined
238
+ }
239
+ export type HandledStates = 'handled' | 'handled as record' | 'ignored' | 'skipped' | 'unchanged'
@@ -37,6 +37,7 @@ export type Document<DocumentKind extends string = string, Node extends Supporte
37
37
  kind: DocumentKind
38
38
  data: unknown
39
39
  changedData?: unknown
40
+ xmlFilePath?: string
40
41
  xmlData?: unknown
41
42
  node?: Node
42
43
  action?: Action
@@ -80,7 +81,7 @@ export type DocumentMap<D extends Document = Document> = {
80
81
  export type EntityComposerFunction<Data extends Record<string, unknown> = Record<string, unknown>> = (
81
82
  entityData: EntityData<Data>,
82
83
  context: Context
83
- ) => LinkedDocument | LinkedDocument[] | undefined
84
+ ) => Promise<LinkedDocument | LinkedDocument[] | undefined>
84
85
 
85
86
  /**
86
87
  * An XML composer function is a function that accepts a piece
@@ -5,13 +5,14 @@ import { Context } from '../Context'
5
5
  /**
6
6
  * A file is a data structure with a name, a target directory, and content
7
7
  * as a string. The target directory must be the name of one of the magic
8
- * directories that exist within a ServiceNow deployable app package.
8
+ * directories that exist within a ServiceNow installable app package.
9
9
  */
10
10
  export type File = {
11
11
  name: `${string}.xml`
12
- directory: 'dictionary' | 'unload' | 'unload.demo' | 'update' | 'apply_once' | 'scope' | ''
12
+ directory: Directories
13
13
  content: string
14
14
  }
15
+ export type Directories = 'dictionary' | 'unload' | 'unload.demo' | 'update' | 'apply_once' | 'scope' | ''
15
16
 
16
17
  /**
17
18
  * A serializer function is a function which accepts a document of a known
@@ -296,11 +296,17 @@ export class EntityData<const D extends Record<string, unknown> = Record<string,
296
296
  private readonly kind: string,
297
297
  private readonly guid: string,
298
298
  entity: ObjectData<D>,
299
- node: SupportedNode
299
+ node: SupportedNode,
300
+ private installMethod?: string,
301
+ private explicitIds?: string[]
300
302
  ) {
301
303
  super(entity.getProperties(), node)
302
304
  }
303
305
 
306
+ getInstallMethod() {
307
+ return this.installMethod
308
+ }
309
+
304
310
  getKind() {
305
311
  return this.kind
306
312
  }
@@ -308,4 +314,8 @@ export class EntityData<const D extends Record<string, unknown> = Record<string,
308
314
  getGuid() {
309
315
  return this.guid
310
316
  }
317
+
318
+ getExplicitIds() {
319
+ return this.explicitIds ?? []
320
+ }
311
321
  }
@@ -1,5 +1,5 @@
1
1
  export { Plugin } from './Plugin'
2
- export { type Context, type HandledStates } from './Context'
2
+ export { type Context, type HandledStates, type HandledInfo } from './Context'
3
3
  export * from './behaviors'
4
4
  export * from './util'
5
5
  export * from './Diagnostic'
@@ -82,6 +82,29 @@ export function extractCallExpression<
82
82
  return {
83
83
  handled: true,
84
84
  diagnostics,
85
- data: new EntityData(kind, guid(entityValue), entity, node),
85
+ data: new EntityData(
86
+ kind,
87
+ guid(entityValue),
88
+ entity,
89
+ node,
90
+ extractInstallMethod(entity),
91
+ extractExplicitId(entity)
92
+ ),
86
93
  }
87
94
  }
95
+
96
+ function extractInstallMethod<const D extends Record<string, unknown>>(entity: ObjectData<D>) {
97
+ const $meta = entity.getProperty('$meta')
98
+ if (!$meta) {
99
+ return
100
+ }
101
+ return $meta.getValue()['installMethod']
102
+ }
103
+
104
+ function extractExplicitId<const D extends Record<string, unknown>>(entity: ObjectData<D>) {
105
+ const $id = entity.getProperty('$id')
106
+ if (!$id) {
107
+ return []
108
+ }
109
+ return [`${$id.getValue()}`]
110
+ }
@@ -1,4 +1,4 @@
1
- import { NOW_FILE_EXTENSION, SupportedNode, path, ts, tsc } from '@servicenow/sdk-project'
1
+ import { Logger, NOW_FILE_EXTENSION, SupportedNode, path, ts, tsc } from '@servicenow/sdk-project'
2
2
  const { format, resolve } = path
3
3
  import { Context } from '../Context'
4
4
  import { getCallExpressionName } from './CallExpression'
@@ -6,10 +6,13 @@ import { isArray, isEmpty, isObject, isPlainObject } from 'lodash'
6
6
  import * as z from 'zod'
7
7
  import { Action, Document } from '../behaviors'
8
8
  import { isGUID } from '../../GUID'
9
- import { getPropertyAssignment } from './ObjectLiteral'
10
9
  import { noThrow } from '../../util'
10
+ import { getPropertyAssignment } from './ObjectLiteral'
11
11
 
12
12
  export function stringify(val: unknown) {
13
+ if (typeof val === 'function') {
14
+ return val.toString()
15
+ }
13
16
  return JSON.stringify(val)
14
17
  }
15
18
 
@@ -28,8 +31,8 @@ export function formatSourceFileName(name: string) {
28
31
  }
29
32
 
30
33
  export function getSysUpdateName(document: Document & { xml?: string }, table: string) {
31
- if (document.xml) {
32
- return path.basename(document.xml, path.extname(document.xml))
34
+ if (document.xmlFilePath) {
35
+ return path.basename(document.xmlFilePath, path.extname(document.xmlFilePath))
33
36
  }
34
37
  return `${table}_${document.guid}`
35
38
  }
@@ -184,7 +187,7 @@ export function getOrCreatePropertyAssignment(
184
187
  obj: ts.ObjectLiteralExpression,
185
188
  name: string,
186
189
  initializer: string = 'undefined'
187
- ) {
190
+ ): ts.PropertyAssignment {
188
191
  const stringifiedName = stringify(name)
189
192
  const prop =
190
193
  obj.getProperty(name)?.asKind(ts.SyntaxKind.PropertyAssignment) ??
@@ -208,49 +211,6 @@ export function getOrCreatePropertyAssignment(
208
211
  return propOrErr
209
212
  }
210
213
 
211
- export function createOrUpdateScriptProperty(obj: ts.ObjectLiteralExpression, name: string, value: string) {
212
- if (!value) {
213
- return
214
- }
215
-
216
- const property = obj.getProperty(name)?.asKind(ts.SyntaxKind.PropertyAssignment)
217
- if (property) {
218
- property.remove()
219
- }
220
- value = value.replaceAll('`', '\\`').replaceAll('${', '\\${').trim()
221
-
222
- return createOrUpdateStringLiteralProperty(obj, name, value, '`')
223
- }
224
-
225
- export function createOrUpdateStringLiteralProperty(
226
- obj: ts.ObjectLiteralExpression,
227
- name: string,
228
- value: string | undefined,
229
- valueQuoteStyle: "'" | '"' | '`' = "'"
230
- ) {
231
- if (value === undefined) {
232
- return
233
- }
234
-
235
- let property = obj.getProperty(name)?.asKind(ts.SyntaxKind.PropertyAssignment)
236
-
237
- if (!property) {
238
- property = obj.addPropertyAssignment({ name, initializer: `${valueQuoteStyle}${value}${valueQuoteStyle}` })
239
- } else {
240
- const valueNode = property.getChildAtIndexIfKind(2, ts.SyntaxKind.StringLiteral)
241
- if (valueNode) {
242
- valueNode.setLiteralValue(value || 'undefined')
243
- } else {
244
- const valueNode = property.getChildAtIndexIfKind(2, ts.SyntaxKind.NoSubstitutionTemplateLiteral)
245
- if (valueNode) {
246
- valueNode.setLiteralValue(value || 'undefined')
247
- }
248
- }
249
- }
250
-
251
- return property
252
- }
253
-
254
214
  export function isEscapedPropertyAssignment(args: ts.ObjectLiteralExpression, name: string) {
255
215
  const prop = args.getProperty(name)
256
216
  if (!prop) {
@@ -289,6 +249,7 @@ function isResolvablePropertyAssignment(args: ts.ObjectLiteralExpression, proper
289
249
  /** utility function to get the $id value in a object literal node */
290
250
  export const getNodeId = (node: ts.ObjectLiteralExpression) => {
291
251
  const prop = node.getPropertyOrThrow('$id').asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
252
+
292
253
  if (prop.getInitializerIfKind(ts.SyntaxKind.ElementAccessExpression)) {
293
254
  const expression = prop.getInitializerIfKind(ts.SyntaxKind.ElementAccessExpression)?.getArgumentExpression()
294
255
  return (
@@ -357,7 +318,20 @@ export const recordSchema = z.object({
357
318
  table: z.string(),
358
319
  })
359
320
 
360
- export function writePropertyAsReference(arg: ts.ObjectLiteralExpression, name: string, def: string, value: unknown) {
321
+ export function addCallExpressionToProperty(obj: ts.ObjectLiteralExpression, field: string, functionCall) {
322
+ const property = getOrCreatePropertyAssignment(obj, field, functionCall)
323
+ property.setInitializer((writer) => {
324
+ writer.write(functionCall)
325
+ })
326
+ }
327
+
328
+ export function writePropertyAsReference(
329
+ logger: Logger,
330
+ arg: ts.ObjectLiteralExpression,
331
+ name: string,
332
+ def: string,
333
+ value: unknown
334
+ ) {
361
335
  if (!value || isArray(value)) {
362
336
  return
363
337
  }
@@ -381,7 +355,7 @@ export function writePropertyAsReference(arg: ts.ObjectLiteralExpression, name:
381
355
  if (isArray(value[property])) {
382
356
  writeArrayPropertyAsReference(nestedArgs, property, '[]', value[property])
383
357
  } else {
384
- writePropertyAsReference(nestedArgs, property, '{}', value[property])
358
+ writePropertyAsReference(logger, nestedArgs, property, '{}', value[property])
385
359
  }
386
360
  }
387
361
 
@@ -399,7 +373,7 @@ export function writePropertyAsReference(arg: ts.ObjectLiteralExpression, name:
399
373
 
400
374
  // This first write is to check whether the value is a resolvable idenfifier
401
375
  // We don't want to throw here if the property assignment would be malformed
402
- noThrow(() => {
376
+ const maybeError = noThrow(() => {
403
377
  propertyAssignment.setInitializer((writer) => {
404
378
  if (value === 'default') {
405
379
  writer.write(stringify(value))
@@ -408,6 +382,11 @@ export function writePropertyAsReference(arg: ts.ObjectLiteralExpression, name:
408
382
  }
409
383
  })
410
384
  })
385
+ if (maybeError instanceof Error) {
386
+ logger.warn(
387
+ `Invalid reference value '${value}' for property '${name}' in file ${arg.getSourceFile().getFilePath()}`
388
+ )
389
+ }
411
390
  if (isResolvablePropertyAssignment(arg, name)) {
412
391
  return
413
392
  }
@@ -437,7 +416,7 @@ export function writeCustomProperty(arg: ts.ObjectLiteralExpression, name: strin
437
416
  export function mergeDataIntoObjectLiteral(node: ts.ObjectLiteralExpression, data: object) {
438
417
  const properties = node
439
418
  .getProperties()
440
- .reduce((a, v) => ({ ...a, [(v as ts.PropertyAssignment).getName()]: v }), {})
419
+ .reduce((a, v) => ({ ...a, [getObjectPropertyAssignmentName(v as ts.PropertyAssignment)]: v }), {})
441
420
  for (const [key, value] of Object.entries(data)) {
442
421
  const array = isArray(value)
443
422
  if (!isEmpty(properties) && (value === '' || (array && value.length === 0))) {
@@ -455,6 +434,10 @@ export function mergeDataIntoObjectLiteral(node: ts.ObjectLiteralExpression, dat
455
434
  if (value !== '' && value !== undefined && !(array && value.length === 0)) {
456
435
  const propertyAssignment = properties[key]
457
436
  if (propertyAssignment) {
437
+ if (isNowIncludeCall(propertyAssignment.getInitializerIfKind(ts.SyntaxKind.CallExpression))) {
438
+ // Don't transform any updates to Now.includes calls
439
+ continue
440
+ }
458
441
  setPropertyAssignmentValue(propertyAssignment, value)
459
442
  } else {
460
443
  const newPropertyAssignment = getOrCreatePropertyAssignment(node, key, array ? '[]' : '{}')
@@ -526,6 +509,21 @@ export function filterEmpty(data: any) {
526
509
  }, {})
527
510
  }
528
511
 
512
+ export function escapeStringLiteralValues(str: string) {
513
+ return str.replace(/[`\\]|\${/g, (char) => {
514
+ switch (char) {
515
+ case '`':
516
+ return '\\`'
517
+ case '\\':
518
+ return '\\' + char
519
+ case '${':
520
+ return '\\${'
521
+ default:
522
+ return char
523
+ }
524
+ })
525
+ }
526
+
529
527
  export function setPropertyAssignmentValue(
530
528
  property: ts.PropertyAssignment,
531
529
  value: string | ts.PropertyAssignmentStructure | ts.ImportSpecifierStructure | object
@@ -550,9 +548,14 @@ export function setPropertyAssignmentValue(
550
548
  const taggedInitializer = property.getInitializerIfKind(ts.SyntaxKind.TaggedTemplateExpression)
551
549
  if (taggedInitializer) {
552
550
  const template = taggedInitializer.getTemplate()
553
- template.setLiteralValue(value)
551
+
552
+ template.setLiteralValue(escapeStringLiteralValues(value))
554
553
  } else {
555
- property.setInitializer(stringify(value))
554
+ const stringified =
555
+ typeof value !== 'string' || value.indexOf('\n') < 0
556
+ ? stringify(value)
557
+ : `\`${escapeStringLiteralValues(value)}\``
558
+ property.setInitializer(stringified)
556
559
  }
557
560
  }
558
561
 
@@ -674,3 +677,55 @@ export function removeNode(node: SupportedNode) {
674
677
  // behavior configurable.
675
678
  node.replaceWithText('undefined')
676
679
  }
680
+
681
+ /**
682
+ * Re-extracts CallExpression property names for transform to check for customized properties on Plugin APIs.
683
+ * If CallExpression properties don't match don't match schema expected by fluent this function will tranform updates
684
+ * only to those properties (not new properties)
685
+ * @param fn type of CallExpression to extract
686
+ * @param node Node
687
+ * @param dataForTransform Document Data passed to a transformer function
688
+ * @param cleanedData Document data cleaned/parsed by a Zod step that has filtered out properties not handled by fluent
689
+ */
690
+ export function transformCustomizedProperties<
691
+ const A extends unknown[],
692
+ const E extends A extends [infer T extends Record<string, unknown>] ? T : never,
693
+ >(fn: (...args: A) => E, node, dataForTransform, cleanedData) {
694
+ if (!dataForTransform || !cleanedData) {
695
+ return
696
+ }
697
+
698
+ const [arg] = node.getArguments()
699
+ const properties = arg?.asKind(ts.SyntaxKind.ObjectLiteralExpression)?.getProperties()
700
+
701
+ if (!properties) {
702
+ return
703
+ }
704
+
705
+ const customizedProperties = {}
706
+ Object.values(properties).forEach((prop) => {
707
+ const propName = getObjectPropertyAssignmentName(prop as ts.PropertyAssignment)
708
+ if (dataForTransform[propName] !== undefined && cleanedData[propName] === undefined) {
709
+ customizedProperties[propName] = dataForTransform[propName]
710
+ }
711
+ })
712
+
713
+ if (!isEmpty(customizedProperties)) {
714
+ transformFunctionArguments(node, fn, ...([customizedProperties] as PartialElements<A>))
715
+ }
716
+ }
717
+
718
+ export function isNowIncludeCall(callExpression?: ts.CallExpression) {
719
+ if (!callExpression) {
720
+ return false
721
+ }
722
+ const propertyAccessExp = callExpression.getExpressionIfKind(ts.SyntaxKind.PropertyAccessExpression)
723
+ const nowNamespace = propertyAccessExp?.getExpressionIfKind(ts.SyntaxKind.Identifier)?.getText()
724
+ const functionName = propertyAccessExp?.getName()
725
+ return nowNamespace === 'Now' && functionName === 'include'
726
+ }
727
+
728
+ export function getObjectPropertyAssignmentName(objectProperty: ts.PropertyAssignment) {
729
+ const nameNode = objectProperty.getNameNode()
730
+ return nameNode.isKind(ts.SyntaxKind.StringLiteral) ? nameNode.getLiteralValue() : objectProperty.getName()
731
+ }
@@ -1,4 +1,3 @@
1
1
  export * from './CallExpression'
2
2
  export * from './CodeTransformation'
3
3
  export * from './ObjectLiteral'
4
- export * from './ConfigurationFunction'
package/src/util/Debug.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { inspect as nodeInspect } from 'util'
2
- import { Data, Document, File } from '../plugins/behaviors'
2
+ import { Document } from '../plugins/behaviors'
3
3
  import { Logger } from '@servicenow/sdk-project'
4
4
 
5
5
  export function inspect(val: unknown, depth = 2) {
@@ -10,21 +10,6 @@ export function inspect(val: unknown, depth = 2) {
10
10
  })
11
11
  }
12
12
 
13
- export function debugData(data: ({ data: unknown } | Data)[], logger: Logger, message = 'DATA:') {
14
- logger.debug(
15
- `${message} ${inspect(
16
- data.map((d) => {
17
- if (d instanceof Data) {
18
- return d.getValue()
19
- } else {
20
- return d
21
- }
22
- }),
23
- 5
24
- )}`
25
- )
26
- }
27
-
28
13
  export function debugDocuments(documents: Document[], logger: Logger, message = 'DOCUMENTS:') {
29
14
  logger.debug(
30
15
  `${message} ${inspect(
@@ -37,7 +22,3 @@ export function debugDocuments(documents: Document[], logger: Logger, message =
37
22
  )}`
38
23
  )
39
24
  }
40
-
41
- export function debugFiles(files: File[], logger: Logger, message = 'FILES:') {
42
- logger.debug(`${message}: ${inspect(files, 5)}`)
43
- }