@servicenow/sdk-build-core 4.6.0 → 4.7.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 (59) hide show
  1. package/dist/compiler.d.ts +2 -0
  2. package/dist/compiler.js +13 -7
  3. package/dist/compiler.js.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/now-config.d.ts +51 -3
  8. package/dist/now-config.js +44 -1
  9. package/dist/now-config.js.map +1 -1
  10. package/dist/package-inventory.d.ts +15 -0
  11. package/dist/package-inventory.js +59 -0
  12. package/dist/package-inventory.js.map +1 -0
  13. package/dist/plugins/context.d.ts +2 -2
  14. package/dist/plugins/file.d.ts +1 -0
  15. package/dist/plugins/index.d.ts +0 -1
  16. package/dist/plugins/index.js +0 -1
  17. package/dist/plugins/index.js.map +1 -1
  18. package/dist/plugins/plugin.d.ts +41 -53
  19. package/dist/plugins/plugin.js +517 -162
  20. package/dist/plugins/plugin.js.map +1 -1
  21. package/dist/plugins/shape.d.ts +13 -2
  22. package/dist/plugins/shape.js +96 -15
  23. package/dist/plugins/shape.js.map +1 -1
  24. package/dist/taxonomy.js +7 -2
  25. package/dist/taxonomy.js.map +1 -1
  26. package/dist/telemetry/clients/detect-agent.d.ts +4 -0
  27. package/dist/telemetry/clients/detect-agent.js +84 -0
  28. package/dist/telemetry/clients/detect-agent.js.map +1 -0
  29. package/dist/telemetry/clients/node-client.d.ts +2 -0
  30. package/dist/telemetry/clients/node-client.js +10 -9
  31. package/dist/telemetry/clients/node-client.js.map +1 -1
  32. package/dist/telemetry/index.d.ts +1 -1
  33. package/dist/xml.d.ts +2 -2
  34. package/dist/xml.js +2 -2
  35. package/dist/xml.js.map +1 -1
  36. package/now.config.schema.json +54 -0
  37. package/package.json +9 -5
  38. package/src/compiler.ts +14 -7
  39. package/src/index.ts +1 -0
  40. package/src/now-config.ts +56 -1
  41. package/src/package-inventory.ts +75 -0
  42. package/src/plugins/context.ts +2 -2
  43. package/src/plugins/file.ts +1 -0
  44. package/src/plugins/index.ts +0 -1
  45. package/src/plugins/plugin.ts +704 -231
  46. package/src/plugins/shape.ts +115 -24
  47. package/src/taxonomy.ts +8 -2
  48. package/src/telemetry/clients/detect-agent.ts +88 -0
  49. package/src/telemetry/clients/node-client.ts +12 -8
  50. package/src/telemetry/index.ts +1 -1
  51. package/src/xml.ts +11 -2
  52. package/dist/plugins/cache.d.ts +0 -15
  53. package/dist/plugins/cache.js +0 -22
  54. package/dist/plugins/cache.js.map +0 -1
  55. package/dist/plugins/usage.d.ts +0 -11
  56. package/dist/plugins/usage.js +0 -26
  57. package/dist/plugins/usage.js.map +0 -1
  58. package/src/plugins/cache.ts +0 -23
  59. package/src/plugins/usage.ts +0 -26
@@ -5,17 +5,26 @@ import {
5
5
  type SupportedNodeByKindName,
6
6
  ts,
7
7
  } from '../typescript'
8
- import { DeletedShape, type ObjectShape, type Record, type Shape, type ShapeClass, type StringShape } from './shape'
8
+ import {
9
+ CallExpressionShape,
10
+ DeletedShape,
11
+ type ObjectShape,
12
+ type Record,
13
+ type Shape,
14
+ type ShapeClass,
15
+ type StringShape,
16
+ VariableStatementShape,
17
+ } from './shape'
9
18
  import type { File, OutputFile } from './file'
10
19
  import type { Product } from './product'
11
20
  import { Database, DiffDatabase } from './database'
12
- import type { Context as BaseContext } from './context'
21
+ import type { Context as BaseContext, Diagnostics } from './context'
13
22
  import { getFileType, isSNScope } from '../util'
14
- import { Cache } from './cache'
15
- import { NOW_FILE_EXTENSION } from '..'
23
+ import { NOW_FILE_EXTENSION, NowConfig } from '..'
16
24
  import { path } from '@servicenow/sdk-build-core'
17
25
 
18
26
  type Context = Omit<BaseContext, 'keys'>
27
+ const FAILURE = { success: false } as const
19
28
 
20
29
  export type Result<Value = unknown> =
21
30
  | {
@@ -93,29 +102,6 @@ export type CoalesceStrategy =
93
102
 
94
103
  export type FileType = 'fluent' | 'module' | 'json' | 'unknown'
95
104
 
96
- export type PluginApiDoc = {
97
- /**
98
- * The name of the API (e.g., 'BusinessRule', 'Acl', 'Table')
99
- * This is the callee name used in fluent code.
100
- */
101
- apiName: string
102
-
103
- /**
104
- * Tags for categorizing and organizing the API documentation
105
- */
106
- tags: string[]
107
- }
108
-
109
- /**
110
- * Manifest structure for API documentation.
111
- */
112
- export type DocsManifest = {
113
- [apiName: string]: {
114
- docPath: string
115
- tags: string[]
116
- }
117
- }
118
-
119
105
  export type PluginConfig<
120
106
  Nodes extends SupportedKindName[] = SupportedKindName[],
121
107
  Shapes extends ShapeClass[] = ShapeClass[],
@@ -127,12 +113,6 @@ export type PluginConfig<
127
113
  */
128
114
  name: `${string}Plugin`
129
115
 
130
- /**
131
- * Documentation for the APIs provided by this plugin. This is used to automatically
132
- * generate documentation that can be retrieved via the getDocsMetadata() method.
133
- */
134
- docs: PluginApiDoc[]
135
-
136
116
  /**
137
117
  * The TypeScript AST nodes this plugin handles. Plugins that do not introduce new
138
118
  * syntax should not need to define any handlers here.
@@ -427,7 +407,7 @@ export type PluginConfig<
427
407
  /**
428
408
  * A regex pattern or predicate to apply to a file's path to determine if it should be handled by this plugin.
429
409
  */
430
- matcher?: RegExp | ((path: string, context: Context) => boolean | Promise<boolean>)
410
+ matcher?: RegExp | FileMatcher
431
411
 
432
412
  /**
433
413
  * Indicates whether this file should be parsed as an entry point when generating output. (Default: false)
@@ -444,6 +424,12 @@ export type PluginConfig<
444
424
  */
445
425
  toRecord?(file: File, context: Context): Result<Record> | Promise<Result<Record>>
446
426
  }[]
427
+
428
+ /**
429
+ * @deprecated This property is no longer read and will be removed in a
430
+ * future release.
431
+ */
432
+ docs?: any[]
447
433
  }
448
434
 
449
435
  function setCreator<V extends Product | Product[]>(creator: Plugin, result: Result<V>): Result<V> {
@@ -466,10 +452,229 @@ type SearchableReferences = {
466
452
  }
467
453
  }
468
454
 
455
+ /**
456
+ * Recursively sets `sys_policy` on a record and all its descendants,
457
+ * skipping any record that already has `sys_policy` set by the plugin.
458
+ */
459
+ function propagateSysPolicyToDescendants(record: Record, sysPolicyShape: Shape): Record {
460
+ const patched = record.get('sys_policy').isUndefined() ? record.merge({ sys_policy: sysPolicyShape }) : record
461
+
462
+ // flat() cannot substitute here: it collapses the entire descendant tree into a single array,
463
+ // discarding which records are direct children of which parent. This function rebuilds the
464
+ // hierarchy bottom-up via .with(), so it must operate one level at a time using getRelated().
465
+ const children = patched.getRelated()
466
+ if (children.length === 0) {
467
+ return patched
468
+ }
469
+
470
+ return patched.with(...children.map((child) => propagateSysPolicyToDescendants(child, sysPolicyShape)))
471
+ }
472
+
473
+ /**
474
+ * Auto-injects `sys_policy` into the record and all its descendants from the shape's
475
+ * `protectionPolicy` property, so that plugins don't need to explicitly map it.
476
+ * Only injects if `sys_policy` is not already set by the plugin, and only propagates
477
+ * if the root record's plugin did not already handle `sys_policy` itself.
478
+ */
479
+ function mergeSysPolicyFromShape(shape: Shape, record: Record): Record {
480
+ if (!(shape instanceof CallExpressionShape)) {
481
+ return record
482
+ }
483
+
484
+ const source = shape.getArgument(0).ifObject()
485
+ if (!source) {
486
+ return record
487
+ }
488
+
489
+ const protectionPolicy = source.get('protectionPolicy')
490
+ if (protectionPolicy.isUndefined()) {
491
+ return record
492
+ }
493
+
494
+ // If the plugin already mapped sys_policy on the root record, skip propagation entirely.
495
+ // The plugin owns the protectionPolicy handling in this case.
496
+ if (record.get('sys_policy').isDefined()) {
497
+ return record
498
+ }
499
+
500
+ return propagateSysPolicyToDescendants(record, protectionPolicy)
501
+ }
502
+
503
+ /**
504
+ * Auto-injects `protectionPolicy` into the shape from the record's `sys_policy` field,
505
+ * so that plugins don't need to explicitly map it. Only injects if `protectionPolicy` is
506
+ * not already set by the plugin.
507
+ */
508
+ function mergeProtectionPolicyFromRecord(record: Record, shape: Shape): Shape {
509
+ if (!(shape instanceof CallExpressionShape)) {
510
+ return shape
511
+ }
512
+
513
+ const firstArg = shape.getArgument(0).ifObject()
514
+ if (!firstArg || firstArg.get('protectionPolicy').isDefined()) {
515
+ return shape
516
+ }
517
+
518
+ const sysPolicy = record.get('sys_policy').ifString()?.getValue()
519
+ if (!sysPolicy) {
520
+ return shape
521
+ }
522
+
523
+ return new CallExpressionShape({
524
+ source: shape.getSource(),
525
+ callee: shape.getCallee(),
526
+ args: [firstArg.merge({ protectionPolicy: sysPolicy }), ...shape.getArguments().slice(1)],
527
+ exportName: shape.getExportName(),
528
+ })
529
+ }
530
+
531
+ /**
532
+ * Auto-injects `$meta.installMethod` into the shape based on the record's install category.
533
+ * When transforming from `unload.demo/`, `unload/`, or `apply_once/` directories, the generated
534
+ * Fluent code should include `$meta: { installMethod: 'demo' | 'first install' | 'once' }`.
535
+ * Only injects if `$meta.installMethod` is not already set by the plugin.
536
+ */
537
+ function mergeMetaFromRecord(record: Record, shape: Shape): Shape {
538
+ if (!(shape instanceof CallExpressionShape)) {
539
+ return shape
540
+ }
541
+
542
+ const firstArg = shape.getArgument(0).ifObject()
543
+ const existingMeta = firstArg?.get('$meta').ifObject()
544
+ if (!firstArg || existingMeta?.get('installMethod').isDefined()) {
545
+ return shape
546
+ }
547
+
548
+ const installCategory = record.getInstallCategory()
549
+ let installMethod: 'demo' | 'first install' | 'once' | undefined
550
+ if (installCategory === 'unload.demo') {
551
+ installMethod = 'demo'
552
+ } else if (installCategory === 'unload') {
553
+ installMethod = 'first install'
554
+ } else if (installCategory === 'apply_once') {
555
+ installMethod = 'once'
556
+ }
557
+
558
+ if (!installMethod) {
559
+ return shape
560
+ }
561
+
562
+ return new CallExpressionShape({
563
+ source: shape.getSource(),
564
+ callee: shape.getCallee(),
565
+ args: [firstArg.merge({ $meta: { installMethod } }), ...shape.getArguments().slice(1)],
566
+ exportName: shape.getExportName(),
567
+ })
568
+ }
569
+
570
+ /**
571
+ * Injects `$override` properties from the Fluent shape into the record, so plugins don't
572
+ * need to explicitly map them. Only injects fields not already set by the plugin.
573
+ */
574
+ function mergeOverrideFromShape(shape: Shape, record: Record, diagnostics: Diagnostics): Record {
575
+ if (!(shape instanceof CallExpressionShape)) {
576
+ return record
577
+ }
578
+ const override = shape.getArgument(0).ifObject()?.get('$override').ifObject()
579
+ if (!override) {
580
+ return record
581
+ }
582
+
583
+ const extra: { [key: string]: Shape } = {}
584
+ const overrideKeys = new Set<string>()
585
+ for (const [key, value] of override.entries()) {
586
+ if (key.startsWith('sys_')) {
587
+ diagnostics.warn(
588
+ value,
589
+ `$override: '${key}' starts with 'sys_' and will not be restored on transform — remove it or use a supported API property instead.`
590
+ )
591
+ continue
592
+ }
593
+ if (record.get(key).isDefined()) {
594
+ diagnostics.warn(
595
+ value,
596
+ `$override: '${key}' is already set by the plugin and will be ignored — use the corresponding API property instead.`
597
+ )
598
+ continue
599
+ }
600
+ overrideKeys.add(key)
601
+ extra[key] = value
602
+ }
603
+
604
+ const result = overrideKeys.size > 0 ? record.merge(extra) : record
605
+ return result.withOverrideKeys(overrideKeys)
606
+ }
607
+
608
+ /**
609
+ * Injects `$override` into the shape. Includes:
610
+ * - Custom fields (`u_*` / `x_*`) with non-empty values.
611
+ * - Any field that was explicitly in the existing record's `$override` — re-emitted with the
612
+ * fresh DB value.
613
+ */
614
+ function mergeOverrideFromRecord(record: Record, shape: Shape, accessed: Set<string>, existingRecord?: Record): Shape {
615
+ const inner = shape instanceof VariableStatementShape ? shape.getInitializer() : shape
616
+ const ce = inner instanceof CallExpressionShape ? inner : null
617
+ if (!ce) {
618
+ return shape
619
+ }
620
+
621
+ const firstArg = ce.getArgument(0).ifObject()
622
+ if (!firstArg || firstArg.get('$override').isDefined()) {
623
+ return shape
624
+ }
625
+
626
+ const existingOverrideKeys = existingRecord?.getOverrideKeys()
627
+ const override: globalThis.Record<string, Shape> = {}
628
+ for (const [key, value] of record.entries()) {
629
+ if (key.startsWith('sys_') || accessed.has(key)) {
630
+ continue
631
+ }
632
+ const isCustomField = key.startsWith('u_') || key.startsWith('x_')
633
+ if (!isCustomField && !existingOverrideKeys?.has(key)) {
634
+ continue
635
+ }
636
+ // Preserve empty strings when the key was explicitly put in $override previously —
637
+ // that's how users clear a field. Only skip empty strings for newly-discovered fields.
638
+ const wasExplicitlyOverridden = existingOverrideKeys?.has(key)
639
+ if (!value.isDefined() || (!wasExplicitlyOverridden && value.ifString()?.isEmpty())) {
640
+ continue
641
+ }
642
+ override[key] = value
643
+ }
644
+
645
+ if (Object.keys(override).length === 0) {
646
+ return shape
647
+ }
648
+
649
+ const modified = new CallExpressionShape({
650
+ source: ce.getSource(),
651
+ callee: ce.getCallee(),
652
+ args: [firstArg.merge({ $override: override }), ...ce.getArguments().slice(1)],
653
+ exportName: ce.getExportName(),
654
+ })
655
+
656
+ if (!(shape instanceof VariableStatementShape)) {
657
+ return modified
658
+ }
659
+ return new VariableStatementShape({
660
+ source: shape.getSource(),
661
+ variableName: shape.getVariableName(),
662
+ initializer: modified,
663
+ isExported: shape.isExported(),
664
+ })
665
+ }
666
+
667
+ type FileMatcher = (path: string, context: Context) => boolean | Promise<boolean>
668
+
669
+ type EntryPoints = {
670
+ readonly nodes: Map<SupportedKindName, Set<FileType | '*'>>
671
+ readonly files: {
672
+ readonly plugin: Plugin
673
+ readonly matcher: RegExp | FileMatcher
674
+ }[]
675
+ }
676
+
469
677
  export class Plugin {
470
- private readonly nodeToShapeCache = new Cache<ts.ts.Node, Result<Shape>>()
471
- private readonly shapeToSubclassCache = new Cache<Shape, Result<Shape>>()
472
- private readonly shapeToRecordCache = new Cache<Shape, Result<Record>>()
473
678
  private readonly relationships: SearchableRelationships = {}
474
679
  private readonly references: SearchableReferences = {}
475
680
 
@@ -630,23 +835,6 @@ export class Plugin {
630
835
  return this.config.name
631
836
  }
632
837
 
633
- /**
634
- * Get documentation metadata for APIs provided by this plugin.
635
- */
636
- getDocsMetadata(): { [apiName: string]: { tags: string[] } } | undefined {
637
- if (!this.config.docs) {
638
- return undefined
639
- }
640
-
641
- const manifest: { [apiName: string]: { tags: string[] } } = {}
642
-
643
- for (const doc of this.config.docs) {
644
- manifest[doc.apiName] = { tags: doc.tags }
645
- }
646
-
647
- return manifest
648
- }
649
-
650
838
  getDescendants(parent: Record, database: Database): Record[] {
651
839
  return this.traverseDescendants(parent, database)
652
840
  }
@@ -778,38 +966,6 @@ export class Plugin {
778
966
  return this.relationships
779
967
  }
780
968
 
781
- flushCache(): void {
782
- this.nodeToShapeCache.clear()
783
- this.shapeToRecordCache.clear()
784
- }
785
-
786
- async nodeToShape(node: SupportedNode, context: Omit<Context, 'self'>): Promise<Result<Shape>> {
787
- const entry = this.nodeToShapeCache.get(node.compilerNode)
788
- if (entry) {
789
- return entry.success ? entry.value : { success: false }
790
- }
791
-
792
- try {
793
- for (const config of this.config.nodes ?? []) {
794
- if (
795
- config.toShape &&
796
- getKindName(node) === config.node &&
797
- (!config.fileTypes ||
798
- config.fileTypes.includes(getFileType(node.getSourceFile().getFilePath(), context.project)))
799
- ) {
800
- const result = setCreator(this, await config.toShape.bind(this)(node, { ...context, self: this }))
801
- if (result.success) {
802
- return this.nodeToShapeCache.put(node.compilerNode, result)
803
- }
804
- }
805
- }
806
-
807
- return this.nodeToShapeCache.put(node.compilerNode, { success: false })
808
- } catch (e) {
809
- throw this.nodeToShapeCache.error(node.compilerNode, e)
810
- }
811
- }
812
-
813
969
  async commit(shape: Shape, target: ts.Node, context: Omit<CommitContext, 'self'>): Promise<CommitResult> {
814
970
  for (const { shape: shapeClass, commit } of this.config.shapes ?? []) {
815
971
  if (shape.is(shapeClass) && commit) {
@@ -820,7 +976,7 @@ export class Plugin {
820
976
  }
821
977
  }
822
978
 
823
- return { success: false }
979
+ return FAILURE
824
980
  }
825
981
 
826
982
  async getTarget(shape: Shape, context: Omit<Context, 'self'>): Promise<Result<ts.Node>> {
@@ -832,116 +988,70 @@ export class Plugin {
832
988
  }
833
989
  }
834
990
  }
835
-
836
- return { success: false }
991
+ return FAILURE
837
992
  }
838
993
 
839
- async shapeToSubclass<const S extends Shape>(shape: S, context: Omit<Context, 'self'>): Promise<Result<S>> {
840
- const entry = this.shapeToSubclassCache.get(shape)
841
- if (entry) {
842
- return entry.success ? (entry.value as Result<S>) : { success: false }
843
- }
844
-
845
- try {
846
- for (const config of this.config.shapes ?? []) {
847
- if (
848
- config.toSubclass &&
849
- shape.constructor === config.shape &&
850
- (!config.fileTypes ||
851
- config.fileTypes.includes(getFileType(shape.getOriginalFilePath(), context.project)))
852
- ) {
853
- const result = setCreator(
854
- this,
855
- await config.toSubclass.bind(this)(shape, {
856
- ...context,
857
- self: this,
858
- })
859
- )
860
-
861
- if (result.success) {
862
- if (result.value.constructor === config.shape) {
863
- throw new Error(
864
- `Result of subclassing "${config.shape.name}" is an instance of the same class. The result MUST be a subclass.`
865
- )
866
- }
867
-
868
- return this.shapeToSubclassCache.put(shape, result as Result<S>)
869
- }
870
- }
871
- }
872
-
873
- return this.shapeToSubclassCache.put(shape, { success: false })
874
- } catch (e) {
875
- throw this.shapeToSubclassCache.error(shape, e)
876
- }
877
- }
878
-
879
- async shapeToRecord(shape: Shape, context: Omit<Context, 'self'>): Promise<Result<Record>> {
880
- const entry = this.shapeToRecordCache.get(shape)
881
- if (entry) {
882
- return entry.success ? entry.value : { success: false }
883
- }
884
-
885
- try {
886
- for (const config of this.config.shapes ?? []) {
887
- if (
888
- config.toRecord &&
889
- shape.is(config.shape) &&
890
- (!config.fileTypes ||
891
- config.fileTypes.includes(getFileType(shape.getOriginalFilePath(), context.project)))
892
- ) {
893
- const result = setCreator(this, await config.toRecord.bind(this)(shape, { ...context, self: this }))
894
- if (result.success) {
895
- return this.shapeToRecordCache.put(shape, result)
896
- }
897
- }
898
- }
899
-
900
- return this.shapeToRecordCache.put(shape, { success: false })
901
- } catch (e) {
902
- throw this.shapeToRecordCache.error(shape, e)
903
- }
904
- }
905
-
906
- private async recordToShape0(record: Record, context: Omit<RecordContext, 'self'>): Promise<Result<Shape>> {
994
+ private async recordToShape0(
995
+ record: Record,
996
+ context: Omit<RecordContext, 'self'>,
997
+ existingRecord?: Record
998
+ ): Promise<Result<Shape>> {
907
999
  if (record.getAction() !== 'INSERT_OR_UPDATE') {
908
1000
  return { success: true, value: new DeletedShape({ source: record }) }
909
1001
  }
910
1002
 
911
1003
  const table = record.getTable()
912
- for (const [configTable, { toShape }] of Object.entries(this.config.records ?? {})) {
913
- if ((configTable === '*' || configTable === table) && toShape) {
914
- const result = setCreator(this, await toShape.bind(this)(record, { ...context, self: this }))
915
- if (result.success) {
916
- return result
917
- }
1004
+ for (const [configTable, recordConfig] of Object.entries(this.config.records ?? {})) {
1005
+ if (configTable !== '*' && configTable !== table) {
1006
+ continue
1007
+ }
1008
+ if (!recordConfig.toShape) {
1009
+ continue
1010
+ }
1011
+
1012
+ record.startTracking()
1013
+ const result = setCreator(this, await recordConfig.toShape.bind(this)(record, { ...context, self: this }))
1014
+ if (result.success) {
1015
+ const accessed = record.getTrackedFields()
1016
+ let value = mergeOverrideFromRecord(record, result.value, accessed, existingRecord)
1017
+ value = mergeProtectionPolicyFromRecord(record, value)
1018
+ value = mergeMetaFromRecord(record, value)
1019
+ return { ...result, value }
918
1020
  }
919
1021
  }
920
1022
 
921
- return { success: false }
1023
+ return FAILURE
922
1024
  }
923
1025
 
924
1026
  async recordToShape({
925
1027
  record,
926
1028
  database,
1029
+ existingDatabase,
927
1030
  context,
928
1031
  }: {
929
1032
  record: Record
930
1033
  database: Database
1034
+ existingDatabase?: Database
931
1035
  context: Omit<Context, 'self'>
932
1036
  }): Promise<Result<Shape>> {
933
1037
  context.logger.debug(`Transforming record into shape: ${record.getTable()}.${record.getId().getValue()}`)
934
1038
 
935
1039
  const descendants = this.getDescendants(record, database)
936
- return this.recordToShape0(record, {
937
- ...context,
938
- database,
939
- descendants: new Database(descendants.filter((r) => r.getAction() === 'INSERT_OR_UPDATE')),
940
- })
1040
+ const existingRecord = existingDatabase?.resolve(record.getId())
1041
+ return this.recordToShape0(
1042
+ record,
1043
+ {
1044
+ ...context,
1045
+ database,
1046
+ descendants: new Database(descendants.filter((r) => r.getAction() === 'INSERT_OR_UPDATE')),
1047
+ },
1048
+ existingRecord
1049
+ )
941
1050
  }
942
1051
 
943
1052
  async recordsToShapes({
944
1053
  database,
1054
+ existingDatabase,
945
1055
  creatorOnly,
946
1056
  changedOnly = false,
947
1057
  mode,
@@ -949,6 +1059,7 @@ export class Plugin {
949
1059
  context,
950
1060
  }: {
951
1061
  database: Database
1062
+ existingDatabase?: Database
952
1063
  creatorOnly: boolean
953
1064
  changedOnly?: boolean
954
1065
  mode: 'explicit' | 'catch-all'
@@ -1007,11 +1118,16 @@ export class Plugin {
1007
1118
  }
1008
1119
  }
1009
1120
 
1010
- const result = await this.recordToShape0(record, {
1011
- ...context,
1012
- database,
1013
- descendants: nonDeletedDescendants,
1014
- })
1121
+ const existingRecord = existingDatabase?.resolve(record.getId())
1122
+ const result = await this.recordToShape0(
1123
+ record,
1124
+ {
1125
+ ...context,
1126
+ database,
1127
+ descendants: nonDeletedDescendants,
1128
+ },
1129
+ existingRecord
1130
+ )
1015
1131
 
1016
1132
  if (result.success === 'partial') {
1017
1133
  for (const unhandledRecord of result.unhandledRecords) {
@@ -1041,7 +1157,7 @@ export class Plugin {
1041
1157
  } else if (
1042
1158
  !descendant.getCreator() ||
1043
1159
  !record.getCreator() ||
1044
- descendant.getCreator()!.getName() === record.getCreator()!.getName()
1160
+ descendant.getCreator()?.getName() === record.getCreator()?.getName()
1045
1161
  ) {
1046
1162
  // This descendant is handled by the same plugin that handled the root record
1047
1163
  handledRecords.insert(descendant)
@@ -1054,7 +1170,7 @@ export class Plugin {
1054
1170
 
1055
1171
  if (unhandledRecords.length > 0) {
1056
1172
  context.logger.debug(
1057
- `Plugin ${this.getName()} deferred handling of records: ${unhandledRecords.map((r) => r.getTable() + '_' + r.getId().getValue()).join(', ')}`
1173
+ `Plugin ${this.getName()} deferred handling of records: ${unhandledRecords.map((r) => `${r.getTable()}_${r.getId().getValue()}`).join(', ')}`
1058
1174
  )
1059
1175
  return { success: 'partial', value: shapes, unhandledRecords }
1060
1176
  }
@@ -1135,12 +1251,31 @@ export class Plugin {
1135
1251
  const filePath = path.normalize(record.getOriginalFilePath())
1136
1252
  if (isSNScope(context.config.scope) && filePath.endsWith(NOW_FILE_EXTENSION)) {
1137
1253
  const relativePath = path.relative(path.join(context.config.fluentDir, 'if'), filePath)
1138
- if (!relativePath.startsWith('..') && path.dirname(relativePath) !== '.') {
1254
+ const relativePathGenerated = path.relative(
1255
+ path.join(context.config.generatedDir, 'if'),
1256
+ filePath
1257
+ )
1258
+ if (
1259
+ !relativePath.startsWith('..') ||
1260
+ (!relativePathGenerated.startsWith('..') && path.dirname(relativePath) !== '.')
1261
+ ) {
1262
+ const subPath = relativePath.startsWith('..') ? relativePathGenerated : relativePath
1263
+ return {
1264
+ success: true,
1265
+ value: outputFiles.map((f) => ({
1266
+ ...f,
1267
+ ifDirectoryPackage: path.join('if', subPath.split(path.sep)[0]!),
1268
+ })),
1269
+ }
1270
+ }
1271
+
1272
+ const hosted = NowConfig.getHostedDirectory(context.config, filePath)
1273
+ if (hosted) {
1139
1274
  return {
1140
1275
  success: true,
1141
1276
  value: outputFiles.map((f) => ({
1142
1277
  ...f,
1143
- ifDirectoryPackage: path.join('if', path.dirname(relativePath)),
1278
+ hostedPluginDir: hosted,
1144
1279
  })),
1145
1280
  }
1146
1281
  }
@@ -1151,7 +1286,7 @@ export class Plugin {
1151
1286
  }
1152
1287
  }
1153
1288
 
1154
- return { success: false }
1289
+ return FAILURE
1155
1290
  }
1156
1291
 
1157
1292
  async recordsToFiles({
@@ -1210,38 +1345,44 @@ export class Plugin {
1210
1345
  return { success, value: files }
1211
1346
  }
1212
1347
 
1213
- async isEntryPoint(pathOrNode: string | SupportedNode, context: Omit<Context, 'self'>): Promise<boolean> {
1214
- if (ts.Node.isNode(pathOrNode)) {
1215
- for (const config of this.config.nodes ?? []) {
1216
- if (getKindName(pathOrNode) !== config.node) {
1217
- continue
1218
- }
1348
+ getEntryPoints(): EntryPoints {
1349
+ const entryPoints: EntryPoints = {
1350
+ nodes: new Map(),
1351
+ files: [],
1352
+ }
1219
1353
 
1220
- const fileType = getFileType(pathOrNode.getSourceFile().getFilePath(), context.project)
1221
- if (config.fileTypes && !config.fileTypes.includes(fileType)) {
1222
- continue
1223
- }
1354
+ for (const config of this.config.nodes ?? []) {
1355
+ if (!config.entryPoint) {
1356
+ continue
1357
+ }
1224
1358
 
1225
- return !!config.entryPoint
1359
+ const fileTypes = entryPoints.nodes.get(config.node) ?? new Set()
1360
+ if (config.fileTypes) {
1361
+ // Plugin only accepts the node from certain file types
1362
+ config.fileTypes.forEach((t) => fileTypes.add(t))
1363
+ } else {
1364
+ // Plugin accepts the node from any file type
1365
+ fileTypes.add('*')
1226
1366
  }
1227
- } else {
1228
- for (const config of this.config.files ?? []) {
1229
- if (config.matcher instanceof RegExp && !config.matcher.test(pathOrNode)) {
1230
- continue
1231
- }
1232
1367
 
1233
- if (
1234
- typeof config.matcher === 'function' &&
1235
- !(await config.matcher(pathOrNode, { ...context, self: this }))
1236
- ) {
1237
- continue
1238
- }
1368
+ entryPoints.nodes.set(config.node, fileTypes)
1369
+ }
1239
1370
 
1240
- return !!config.entryPoint
1371
+ for (const config of this.config.files ?? []) {
1372
+ if (!config.entryPoint) {
1373
+ continue
1241
1374
  }
1375
+
1376
+ if (!config.matcher) {
1377
+ throw new Error(
1378
+ `Plugin "${this.getName()}" defines a file entry point with no matcher. This would cause all files to be treated as entry points.`
1379
+ )
1380
+ }
1381
+
1382
+ entryPoints.files.push({ plugin: this, matcher: config.matcher })
1242
1383
  }
1243
1384
 
1244
- return false
1385
+ return entryPoints
1245
1386
  }
1246
1387
 
1247
1388
  async fileToRecord(file: File, context: Omit<Context, 'self'>): Promise<Result<Record>> {
@@ -1264,7 +1405,7 @@ export class Plugin {
1264
1405
  }
1265
1406
  }
1266
1407
 
1267
- return { success: false }
1408
+ return FAILURE
1268
1409
  }
1269
1410
 
1270
1411
  inspect(shape: Shape, context: Omit<Context, 'self'>): void {
@@ -1286,34 +1427,142 @@ export class Plugin {
1286
1427
  }
1287
1428
  }
1288
1429
 
1289
- return { success: false }
1430
+ return FAILURE
1431
+ }
1432
+
1433
+ getConfig(): PluginConfig {
1434
+ return this.config
1290
1435
  }
1291
1436
  }
1292
1437
 
1293
- export class Plugins {
1294
- constructor(private readonly plugins: Plugin[]) {}
1438
+ type PluginEntry = {
1439
+ plugin: Plugin
1440
+ shapeCount: number
1441
+ nodeToShapeCache: WeakMap<ts.ts.Node, Result<Shape>>
1442
+ shapeToSubclassCache: WeakMap<Shape, Result<Shape>>
1443
+ shapeToRecordCache: WeakMap<Shape, Result<Record>>
1444
+ }
1445
+
1446
+ export class Plugins implements Iterable<Plugin> {
1447
+ private plugins = new Map<string, PluginEntry>()
1448
+
1449
+ private nodeToShapeCache = new WeakMap<ts.ts.Node, Result<Shape>>()
1450
+ private shapeToSubclassCache = new WeakMap<Shape, Shape>()
1451
+ private shapeToRecordCache = new WeakMap<Shape, Result<Record>>()
1452
+
1453
+ private readonly entryPoints: EntryPoints = {
1454
+ nodes: new Map(),
1455
+ files: [],
1456
+ }
1457
+
1458
+ constructor(plugins: Plugin[]) {
1459
+ for (const plugin of plugins) {
1460
+ this.plugins.set(plugin.getName(), Plugins.createEntry(plugin))
1461
+ }
1462
+
1463
+ this.reload()
1464
+ }
1465
+
1466
+ private static createEntry(plugin: Plugin): PluginEntry {
1467
+ return {
1468
+ plugin,
1469
+ shapeCount: 0,
1470
+ nodeToShapeCache: new WeakMap(),
1471
+ shapeToSubclassCache: new WeakMap(),
1472
+ shapeToRecordCache: new WeakMap(),
1473
+ }
1474
+ }
1475
+
1476
+ private get(plugin: string | Plugin): PluginEntry {
1477
+ const name = plugin instanceof Plugin ? plugin.getName() : plugin
1478
+ const entry = this.plugins.get(name)
1479
+ if (entry) {
1480
+ return entry
1481
+ }
1482
+
1483
+ if (plugin instanceof Plugin) {
1484
+ // Some plugins (mainly Flows) use "helper" plugins that are not actually registered. This is temporarily supporting that use case until we can get rid of those "helper" plugins.
1485
+ return Plugins.createEntry(plugin)
1486
+ }
1295
1487
 
1296
- toArray(): Plugin[] {
1297
- return this.plugins
1488
+ throw new Error(`No such plugin with name "${name}"`)
1298
1489
  }
1299
1490
 
1300
- remove(name: string): void {
1301
- const index = this.plugins.findIndex((plugin) => plugin.getName() === name)
1302
- if (index !== -1) {
1303
- this.plugins.splice(index, 1)
1491
+ *[Symbol.iterator](): Iterator<Plugin> {
1492
+ for (const entry of this.plugins.values()) {
1493
+ yield entry.plugin
1494
+ }
1495
+ }
1496
+
1497
+ reload(): void {
1498
+ this.nodeToShapeCache = new WeakMap()
1499
+ this.shapeToSubclassCache = new WeakMap()
1500
+ this.shapeToRecordCache = new WeakMap()
1501
+
1502
+ this.entryPoints.nodes.clear()
1503
+ this.entryPoints.files.length = 0
1504
+
1505
+ for (const entry of this.plugins.values()) {
1506
+ entry.nodeToShapeCache = new WeakMap()
1507
+ entry.shapeToSubclassCache = new WeakMap()
1508
+ entry.shapeToRecordCache = new WeakMap()
1509
+ entry.shapeCount = 0
1510
+
1511
+ const { nodes, files } = entry.plugin.getEntryPoints()
1512
+ for (const [nodeKind, fileTypes] of nodes.entries()) {
1513
+ const existing = this.entryPoints.nodes.get(nodeKind) ?? new Set()
1514
+ for (const type of fileTypes) {
1515
+ existing.add(type)
1516
+ }
1517
+
1518
+ this.entryPoints.nodes.set(nodeKind, existing)
1519
+ }
1520
+
1521
+ this.entryPoints.files.push(...files)
1304
1522
  }
1305
1523
  }
1306
1524
 
1307
1525
  prepend(...plugins: Plugin[]): void {
1308
- this.plugins.unshift(...plugins)
1526
+ const newMap = new Map<string, PluginEntry>()
1527
+ for (const plugin of plugins) {
1528
+ const name = plugin.getName()
1529
+ newMap.set(name, Plugins.createEntry(plugin))
1530
+ this.plugins.delete(name)
1531
+ }
1532
+
1533
+ for (const [name, entry] of this.plugins.entries()) {
1534
+ newMap.set(name, entry)
1535
+ }
1536
+
1537
+ this.plugins = newMap
1538
+ this.reload()
1309
1539
  }
1310
1540
 
1311
1541
  append(...plugins: Plugin[]): void {
1312
- this.plugins.push(...plugins)
1542
+ for (const plugin of plugins) {
1543
+ const name = plugin.getName()
1544
+ this.plugins.delete(name)
1545
+ this.plugins.set(name, Plugins.createEntry(plugin))
1546
+ }
1547
+
1548
+ this.reload()
1549
+ }
1550
+
1551
+ getShapeCountPerPlugin() {
1552
+ return [...this.plugins.entries()].map(([name, { shapeCount }]) => ({
1553
+ plugin: name.replace(/Plugin$/, ''), //Report just the names 'ACL', 'Record', etc for existing metrics compatibility
1554
+ count: shapeCount,
1555
+ }))
1556
+ }
1557
+
1558
+ clearShapeCounts() {
1559
+ for (const entry of this.plugins.values()) {
1560
+ entry.shapeCount = 0
1561
+ }
1313
1562
  }
1314
1563
 
1315
1564
  getCoalesceStrategy(table: string): CoalesceStrategy | undefined {
1316
- for (const plugin of this.plugins) {
1565
+ for (const plugin of this) {
1317
1566
  const coalesceStrategy = plugin.getCoalesceStrategy(table)
1318
1567
  if (coalesceStrategy) {
1319
1568
  return coalesceStrategy
@@ -1324,7 +1573,7 @@ export class Plugins {
1324
1573
  }
1325
1574
 
1326
1575
  isComposite(table: string): boolean {
1327
- for (const plugin of this.plugins) {
1576
+ for (const plugin of this) {
1328
1577
  if (plugin.isComposite(table)) {
1329
1578
  return true
1330
1579
  }
@@ -1335,7 +1584,7 @@ export class Plugins {
1335
1584
 
1336
1585
  getCoalesceTables(): Set<string> {
1337
1586
  const tables = new Set<string>()
1338
- for (const plugin of this.plugins) {
1587
+ for (const plugin of this) {
1339
1588
  plugin.getCoalesceTables().forEach((t) => {
1340
1589
  tables.add(t)
1341
1590
  })
@@ -1346,7 +1595,7 @@ export class Plugins {
1346
1595
 
1347
1596
  getReferenceColumns(table: string): { [column: string]: string } {
1348
1597
  const refColumnEntries: [string, string][] = []
1349
- for (const plugin of this.plugins) {
1598
+ for (const plugin of this) {
1350
1599
  refColumnEntries.push(...Object.entries(plugin.getReferenceColumns(table)))
1351
1600
  }
1352
1601
 
@@ -1354,25 +1603,249 @@ export class Plugins {
1354
1603
  }
1355
1604
 
1356
1605
  async isEntryPoint(pathOrNode: string | SupportedNode, context: Omit<Context, 'self'>): Promise<boolean> {
1357
- for (const plugin of this.plugins) {
1358
- if (await plugin.isEntryPoint(pathOrNode, context)) {
1606
+ if (ts.Node.isNode(pathOrNode)) {
1607
+ const fileTypes = this.entryPoints.nodes.get(getKindName(pathOrNode))
1608
+ if (!fileTypes || fileTypes.size <= 0) {
1609
+ return false
1610
+ } else if (fileTypes.has('*')) {
1359
1611
  return true
1612
+ } else {
1613
+ return fileTypes.has(getFileType(pathOrNode.getSourceFile().getFilePath(), context.project))
1614
+ }
1615
+ } else {
1616
+ for (const { plugin, matcher } of this.entryPoints.files) {
1617
+ if (matcher instanceof RegExp && matcher.test(pathOrNode)) {
1618
+ return true
1619
+ } else if (typeof matcher === 'function' && (await matcher(pathOrNode, { ...context, self: plugin }))) {
1620
+ return true
1621
+ }
1360
1622
  }
1361
1623
  }
1362
1624
 
1363
1625
  return false
1364
1626
  }
1365
1627
 
1366
- getDocsMetadata(): { [apiName: string]: { tags: string[] } } {
1367
- return this.plugins.reduce(
1368
- (aggregated, plugin) => {
1369
- const pluginManifest = plugin.getDocsMetadata()
1370
- if (pluginManifest) {
1371
- Object.assign(aggregated, pluginManifest)
1628
+ async nodeToShape(
1629
+ node: SupportedNode,
1630
+ context: Omit<Context, 'self'>,
1631
+ ...plugins: (string | Plugin)[]
1632
+ ): Promise<Result<Shape>> {
1633
+ if (plugins.length > 0) {
1634
+ // Bypass global cache when requesting specific plugins
1635
+ return this.nodeToShape0(node, context, ...plugins)
1636
+ }
1637
+
1638
+ // We're not requesting specific plugins, so it's safe to use the global cache
1639
+ const cached = this.nodeToShapeCache.get(node.compilerNode)
1640
+ if (cached) {
1641
+ return cached
1642
+ }
1643
+
1644
+ try {
1645
+ const result = await this.nodeToShape0(node, context)
1646
+ this.nodeToShapeCache.set(node.compilerNode, result)
1647
+ return result
1648
+ } catch (e) {
1649
+ this.nodeToShapeCache.set(node.compilerNode, FAILURE)
1650
+ throw e
1651
+ }
1652
+ }
1653
+
1654
+ private async nodeToShape0(
1655
+ node: SupportedNode,
1656
+ context: Omit<Context, 'self'>,
1657
+ ...plugins: (string | Plugin)[]
1658
+ ): Promise<Result<Shape>> {
1659
+ for (const entry of plugins.length > 0 ? plugins.map((plugin) => this.get(plugin)) : this.plugins.values()) {
1660
+ const cached = entry.nodeToShapeCache.get(node.compilerNode)
1661
+ if (cached) {
1662
+ if (cached.success) {
1663
+ return cached
1664
+ } else {
1665
+ continue
1372
1666
  }
1373
- return aggregated
1374
- },
1375
- {} as { [apiName: string]: { tags: string[] } }
1376
- )
1667
+ }
1668
+
1669
+ for (const config of entry.plugin.getConfig().nodes ?? []) {
1670
+ if (
1671
+ config.toShape &&
1672
+ getKindName(node) === config.node &&
1673
+ (!config.fileTypes ||
1674
+ config.fileTypes.includes(getFileType(node.getSourceFile().getFilePath(), context.project)))
1675
+ ) {
1676
+ try {
1677
+ const result = setCreator(
1678
+ entry.plugin,
1679
+ await config.toShape.bind(entry.plugin)(node, { ...context, self: entry.plugin })
1680
+ )
1681
+
1682
+ entry.nodeToShapeCache.set(node.compilerNode, result)
1683
+ if (result.success) {
1684
+ entry.shapeCount++
1685
+ return result
1686
+ }
1687
+ } catch (error) {
1688
+ entry.nodeToShapeCache.set(node.compilerNode, FAILURE)
1689
+ context.logger.error(
1690
+ `Plugin "${entry.plugin.getName()}" failed to transform "${node.getKindName()}" node into shape:`,
1691
+ error
1692
+ )
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1697
+
1698
+ return FAILURE
1699
+ }
1700
+
1701
+ async shapeToSubclass<const S extends Shape>(
1702
+ shape: S,
1703
+ context: Omit<Context, 'self'>,
1704
+ ...plugins: (string | Plugin)[]
1705
+ ): Promise<S> {
1706
+ if (plugins.length > 0) {
1707
+ // Bypass global cache when requesting specific plugins
1708
+ return this.shapeToSubclass0(shape, context, ...plugins)
1709
+ }
1710
+
1711
+ // We're not requesting specific plugins, so it's safe to use the global cache
1712
+ const cached = this.shapeToSubclassCache.get(shape)
1713
+ if (cached) {
1714
+ return cached as S
1715
+ }
1716
+
1717
+ const subclass = await this.shapeToSubclass0(shape, context)
1718
+ this.shapeToSubclassCache.set(shape, subclass)
1719
+ return subclass
1720
+ }
1721
+
1722
+ private async shapeToSubclass0<const S extends Shape>(
1723
+ shape: S,
1724
+ context: Omit<Context, 'self'>,
1725
+ ...plugins: (string | Plugin)[]
1726
+ ): Promise<S> {
1727
+ for (const entry of plugins.length > 0 ? plugins.map((plugin) => this.get(plugin)) : this.plugins.values()) {
1728
+ const cached = entry.shapeToSubclassCache.get(shape)
1729
+ if (cached) {
1730
+ if (cached.success) {
1731
+ return cached.value as S
1732
+ } else {
1733
+ continue
1734
+ }
1735
+ }
1736
+
1737
+ for (const config of entry.plugin.getConfig().shapes ?? []) {
1738
+ if (
1739
+ config.toSubclass &&
1740
+ shape.constructor === config.shape &&
1741
+ (!config.fileTypes ||
1742
+ config.fileTypes.includes(getFileType(shape.getOriginalFilePath(), context.project)))
1743
+ ) {
1744
+ try {
1745
+ const result = setCreator(
1746
+ entry.plugin,
1747
+ await config.toSubclass.bind(entry.plugin)(shape, { ...context, self: entry.plugin })
1748
+ ) as Result<S>
1749
+
1750
+ entry.shapeToSubclassCache.set(shape, result)
1751
+ if (result.success) {
1752
+ if (result.value.constructor === config.shape) {
1753
+ throw new Error(
1754
+ `Plugin "${entry.plugin.getName()}" tried to subclass "${config.shape.name}" to an instance of the same class. The result MUST be a subclass.`
1755
+ )
1756
+ }
1757
+
1758
+ entry.shapeCount++
1759
+ return await this.shapeToSubclass(result.value, context, ...plugins)
1760
+ }
1761
+ } catch (error) {
1762
+ entry.shapeToSubclassCache.set(shape, FAILURE)
1763
+ context.logger.error(
1764
+ `Plugin "${entry.plugin.getName()}" failed to subclass "${shape.getKind()}" shape:`,
1765
+ error
1766
+ )
1767
+ }
1768
+ }
1769
+ }
1770
+ }
1771
+
1772
+ return shape
1773
+ }
1774
+
1775
+ async shapeToRecord(
1776
+ shape: Shape,
1777
+ context: Omit<Context, 'self'>,
1778
+ ...plugins: (string | Plugin)[]
1779
+ ): Promise<Result<Record>> {
1780
+ if (shape.isRecord()) {
1781
+ return { success: true, value: shape }
1782
+ }
1783
+
1784
+ if (plugins.length > 0) {
1785
+ // Bypass global cache when requesting specific plugins
1786
+ return this.shapeToRecord0(shape, context, ...plugins)
1787
+ }
1788
+
1789
+ // We're not requesting specific plugins, so it's safe to use the global cache
1790
+ const cached = this.shapeToRecordCache.get(shape)
1791
+ if (cached) {
1792
+ return cached
1793
+ }
1794
+
1795
+ const result = await this.shapeToRecord0(shape, context)
1796
+ this.shapeToRecordCache.set(shape, result)
1797
+ return result
1798
+ }
1799
+
1800
+ private async shapeToRecord0(
1801
+ shape: Shape,
1802
+ context: Omit<Context, 'self'>,
1803
+ ...plugins: (string | Plugin)[]
1804
+ ): Promise<Result<Record>> {
1805
+ for (const entry of plugins.length > 0 ? plugins.map((plugin) => this.get(plugin)) : this.plugins.values()) {
1806
+ const cached = entry.shapeToRecordCache.get(shape)
1807
+ if (cached) {
1808
+ if (cached.success) {
1809
+ return cached
1810
+ } else {
1811
+ continue
1812
+ }
1813
+ }
1814
+
1815
+ for (const config of entry.plugin.getConfig().shapes ?? []) {
1816
+ if (
1817
+ config.toRecord &&
1818
+ shape.is(config.shape) &&
1819
+ (!config.fileTypes ||
1820
+ config.fileTypes.includes(getFileType(shape.getOriginalFilePath(), context.project)))
1821
+ ) {
1822
+ try {
1823
+ const result = setCreator(
1824
+ entry.plugin,
1825
+ await config.toRecord.bind(entry.plugin)(shape, { ...context, self: entry.plugin })
1826
+ )
1827
+
1828
+ entry.shapeToRecordCache.set(shape, result)
1829
+ if (result.success) {
1830
+ entry.shapeCount++
1831
+ result.value = mergeOverrideFromShape(
1832
+ shape,
1833
+ mergeSysPolicyFromShape(shape, result.value),
1834
+ context.diagnostics
1835
+ )
1836
+ return result
1837
+ }
1838
+ } catch (error) {
1839
+ entry.shapeToRecordCache.set(shape, FAILURE)
1840
+ context.logger.error(
1841
+ `Plugin "${entry.plugin.getName()}" failed to transform "${shape.getKind()}" shape into record:`,
1842
+ error
1843
+ )
1844
+ }
1845
+ }
1846
+ }
1847
+ }
1848
+
1849
+ return FAILURE
1377
1850
  }
1378
1851
  }