@servicenow/sdk-build-core 4.6.1 → 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 (53) 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 +37 -0
  8. package/dist/now-config.js +9 -0
  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/index.d.ts +0 -1
  15. package/dist/plugins/index.js +0 -1
  16. package/dist/plugins/index.js.map +1 -1
  17. package/dist/plugins/plugin.d.ts +41 -53
  18. package/dist/plugins/plugin.js +502 -160
  19. package/dist/plugins/plugin.js.map +1 -1
  20. package/dist/plugins/shape.d.ts +13 -2
  21. package/dist/plugins/shape.js +96 -15
  22. package/dist/plugins/shape.js.map +1 -1
  23. package/dist/taxonomy.js +7 -2
  24. package/dist/taxonomy.js.map +1 -1
  25. package/dist/telemetry/clients/detect-agent.d.ts +4 -0
  26. package/dist/telemetry/clients/detect-agent.js +84 -0
  27. package/dist/telemetry/clients/detect-agent.js.map +1 -0
  28. package/dist/telemetry/clients/node-client.d.ts +2 -0
  29. package/dist/telemetry/clients/node-client.js +10 -9
  30. package/dist/telemetry/clients/node-client.js.map +1 -1
  31. package/dist/telemetry/index.d.ts +1 -1
  32. package/now.config.schema.json +19 -0
  33. package/package.json +9 -5
  34. package/src/compiler.ts +14 -7
  35. package/src/index.ts +1 -0
  36. package/src/now-config.ts +11 -0
  37. package/src/package-inventory.ts +75 -0
  38. package/src/plugins/context.ts +2 -2
  39. package/src/plugins/index.ts +0 -1
  40. package/src/plugins/plugin.ts +682 -228
  41. package/src/plugins/shape.ts +115 -24
  42. package/src/taxonomy.ts +8 -2
  43. package/src/telemetry/clients/detect-agent.ts +88 -0
  44. package/src/telemetry/clients/node-client.ts +12 -8
  45. package/src/telemetry/index.ts +1 -1
  46. package/dist/plugins/cache.d.ts +0 -15
  47. package/dist/plugins/cache.js +0 -22
  48. package/dist/plugins/cache.js.map +0 -1
  49. package/dist/plugins/usage.d.ts +0 -11
  50. package/dist/plugins/usage.js +0 -26
  51. package/dist/plugins/usage.js.map +0 -1
  52. package/src/plugins/cache.ts +0 -23
  53. package/src/plugins/usage.ts +0 -26
@@ -5,9 +5,9 @@ const typescript_1 = require("../typescript");
5
5
  const shape_1 = require("./shape");
6
6
  const database_1 = require("./database");
7
7
  const util_1 = require("../util");
8
- const cache_1 = require("./cache");
9
8
  const __1 = require("..");
10
9
  const sdk_build_core_1 = require("@servicenow/sdk-build-core");
10
+ const FAILURE = { success: false };
11
11
  function setCreator(creator, result) {
12
12
  if (result.success) {
13
13
  result.value = Array.isArray(result.value)
@@ -16,11 +16,190 @@ function setCreator(creator, result) {
16
16
  }
17
17
  return result;
18
18
  }
19
+ /**
20
+ * Recursively sets `sys_policy` on a record and all its descendants,
21
+ * skipping any record that already has `sys_policy` set by the plugin.
22
+ */
23
+ function propagateSysPolicyToDescendants(record, sysPolicyShape) {
24
+ const patched = record.get('sys_policy').isUndefined() ? record.merge({ sys_policy: sysPolicyShape }) : record;
25
+ // flat() cannot substitute here: it collapses the entire descendant tree into a single array,
26
+ // discarding which records are direct children of which parent. This function rebuilds the
27
+ // hierarchy bottom-up via .with(), so it must operate one level at a time using getRelated().
28
+ const children = patched.getRelated();
29
+ if (children.length === 0) {
30
+ return patched;
31
+ }
32
+ return patched.with(...children.map((child) => propagateSysPolicyToDescendants(child, sysPolicyShape)));
33
+ }
34
+ /**
35
+ * Auto-injects `sys_policy` into the record and all its descendants from the shape's
36
+ * `protectionPolicy` property, so that plugins don't need to explicitly map it.
37
+ * Only injects if `sys_policy` is not already set by the plugin, and only propagates
38
+ * if the root record's plugin did not already handle `sys_policy` itself.
39
+ */
40
+ function mergeSysPolicyFromShape(shape, record) {
41
+ if (!(shape instanceof shape_1.CallExpressionShape)) {
42
+ return record;
43
+ }
44
+ const source = shape.getArgument(0).ifObject();
45
+ if (!source) {
46
+ return record;
47
+ }
48
+ const protectionPolicy = source.get('protectionPolicy');
49
+ if (protectionPolicy.isUndefined()) {
50
+ return record;
51
+ }
52
+ // If the plugin already mapped sys_policy on the root record, skip propagation entirely.
53
+ // The plugin owns the protectionPolicy handling in this case.
54
+ if (record.get('sys_policy').isDefined()) {
55
+ return record;
56
+ }
57
+ return propagateSysPolicyToDescendants(record, protectionPolicy);
58
+ }
59
+ /**
60
+ * Auto-injects `protectionPolicy` into the shape from the record's `sys_policy` field,
61
+ * so that plugins don't need to explicitly map it. Only injects if `protectionPolicy` is
62
+ * not already set by the plugin.
63
+ */
64
+ function mergeProtectionPolicyFromRecord(record, shape) {
65
+ if (!(shape instanceof shape_1.CallExpressionShape)) {
66
+ return shape;
67
+ }
68
+ const firstArg = shape.getArgument(0).ifObject();
69
+ if (!firstArg || firstArg.get('protectionPolicy').isDefined()) {
70
+ return shape;
71
+ }
72
+ const sysPolicy = record.get('sys_policy').ifString()?.getValue();
73
+ if (!sysPolicy) {
74
+ return shape;
75
+ }
76
+ return new shape_1.CallExpressionShape({
77
+ source: shape.getSource(),
78
+ callee: shape.getCallee(),
79
+ args: [firstArg.merge({ protectionPolicy: sysPolicy }), ...shape.getArguments().slice(1)],
80
+ exportName: shape.getExportName(),
81
+ });
82
+ }
83
+ /**
84
+ * Auto-injects `$meta.installMethod` into the shape based on the record's install category.
85
+ * When transforming from `unload.demo/`, `unload/`, or `apply_once/` directories, the generated
86
+ * Fluent code should include `$meta: { installMethod: 'demo' | 'first install' | 'once' }`.
87
+ * Only injects if `$meta.installMethod` is not already set by the plugin.
88
+ */
89
+ function mergeMetaFromRecord(record, shape) {
90
+ if (!(shape instanceof shape_1.CallExpressionShape)) {
91
+ return shape;
92
+ }
93
+ const firstArg = shape.getArgument(0).ifObject();
94
+ const existingMeta = firstArg?.get('$meta').ifObject();
95
+ if (!firstArg || existingMeta?.get('installMethod').isDefined()) {
96
+ return shape;
97
+ }
98
+ const installCategory = record.getInstallCategory();
99
+ let installMethod;
100
+ if (installCategory === 'unload.demo') {
101
+ installMethod = 'demo';
102
+ }
103
+ else if (installCategory === 'unload') {
104
+ installMethod = 'first install';
105
+ }
106
+ else if (installCategory === 'apply_once') {
107
+ installMethod = 'once';
108
+ }
109
+ if (!installMethod) {
110
+ return shape;
111
+ }
112
+ return new shape_1.CallExpressionShape({
113
+ source: shape.getSource(),
114
+ callee: shape.getCallee(),
115
+ args: [firstArg.merge({ $meta: { installMethod } }), ...shape.getArguments().slice(1)],
116
+ exportName: shape.getExportName(),
117
+ });
118
+ }
119
+ /**
120
+ * Injects `$override` properties from the Fluent shape into the record, so plugins don't
121
+ * need to explicitly map them. Only injects fields not already set by the plugin.
122
+ */
123
+ function mergeOverrideFromShape(shape, record, diagnostics) {
124
+ if (!(shape instanceof shape_1.CallExpressionShape)) {
125
+ return record;
126
+ }
127
+ const override = shape.getArgument(0).ifObject()?.get('$override').ifObject();
128
+ if (!override) {
129
+ return record;
130
+ }
131
+ const extra = {};
132
+ const overrideKeys = new Set();
133
+ for (const [key, value] of override.entries()) {
134
+ if (key.startsWith('sys_')) {
135
+ diagnostics.warn(value, `$override: '${key}' starts with 'sys_' and will not be restored on transform — remove it or use a supported API property instead.`);
136
+ continue;
137
+ }
138
+ if (record.get(key).isDefined()) {
139
+ diagnostics.warn(value, `$override: '${key}' is already set by the plugin and will be ignored — use the corresponding API property instead.`);
140
+ continue;
141
+ }
142
+ overrideKeys.add(key);
143
+ extra[key] = value;
144
+ }
145
+ const result = overrideKeys.size > 0 ? record.merge(extra) : record;
146
+ return result.withOverrideKeys(overrideKeys);
147
+ }
148
+ /**
149
+ * Injects `$override` into the shape. Includes:
150
+ * - Custom fields (`u_*` / `x_*`) with non-empty values.
151
+ * - Any field that was explicitly in the existing record's `$override` — re-emitted with the
152
+ * fresh DB value.
153
+ */
154
+ function mergeOverrideFromRecord(record, shape, accessed, existingRecord) {
155
+ const inner = shape instanceof shape_1.VariableStatementShape ? shape.getInitializer() : shape;
156
+ const ce = inner instanceof shape_1.CallExpressionShape ? inner : null;
157
+ if (!ce) {
158
+ return shape;
159
+ }
160
+ const firstArg = ce.getArgument(0).ifObject();
161
+ if (!firstArg || firstArg.get('$override').isDefined()) {
162
+ return shape;
163
+ }
164
+ const existingOverrideKeys = existingRecord?.getOverrideKeys();
165
+ const override = {};
166
+ for (const [key, value] of record.entries()) {
167
+ if (key.startsWith('sys_') || accessed.has(key)) {
168
+ continue;
169
+ }
170
+ const isCustomField = key.startsWith('u_') || key.startsWith('x_');
171
+ if (!isCustomField && !existingOverrideKeys?.has(key)) {
172
+ continue;
173
+ }
174
+ // Preserve empty strings when the key was explicitly put in $override previously —
175
+ // that's how users clear a field. Only skip empty strings for newly-discovered fields.
176
+ const wasExplicitlyOverridden = existingOverrideKeys?.has(key);
177
+ if (!value.isDefined() || (!wasExplicitlyOverridden && value.ifString()?.isEmpty())) {
178
+ continue;
179
+ }
180
+ override[key] = value;
181
+ }
182
+ if (Object.keys(override).length === 0) {
183
+ return shape;
184
+ }
185
+ const modified = new shape_1.CallExpressionShape({
186
+ source: ce.getSource(),
187
+ callee: ce.getCallee(),
188
+ args: [firstArg.merge({ $override: override }), ...ce.getArguments().slice(1)],
189
+ exportName: ce.getExportName(),
190
+ });
191
+ if (!(shape instanceof shape_1.VariableStatementShape)) {
192
+ return modified;
193
+ }
194
+ return new shape_1.VariableStatementShape({
195
+ source: shape.getSource(),
196
+ variableName: shape.getVariableName(),
197
+ initializer: modified,
198
+ isExported: shape.isExported(),
199
+ });
200
+ }
19
201
  class Plugin {
20
202
  config;
21
- nodeToShapeCache = new cache_1.Cache();
22
- shapeToSubclassCache = new cache_1.Cache();
23
- shapeToRecordCache = new cache_1.Cache();
24
203
  relationships = {};
25
204
  references = {};
26
205
  constructor(config) {
@@ -174,19 +353,6 @@ class Plugin {
174
353
  getName() {
175
354
  return this.config.name;
176
355
  }
177
- /**
178
- * Get documentation metadata for APIs provided by this plugin.
179
- */
180
- getDocsMetadata() {
181
- if (!this.config.docs) {
182
- return undefined;
183
- }
184
- const manifest = {};
185
- for (const doc of this.config.docs) {
186
- manifest[doc.apiName] = { tags: doc.tags };
187
- }
188
- return manifest;
189
- }
190
356
  getDescendants(parent, database) {
191
357
  return this.traverseDescendants(parent, database);
192
358
  }
@@ -286,33 +452,6 @@ class Plugin {
286
452
  getRelationships() {
287
453
  return this.relationships;
288
454
  }
289
- flushCache() {
290
- this.nodeToShapeCache.clear();
291
- this.shapeToRecordCache.clear();
292
- }
293
- async nodeToShape(node, context) {
294
- const entry = this.nodeToShapeCache.get(node.compilerNode);
295
- if (entry) {
296
- return entry.success ? entry.value : { success: false };
297
- }
298
- try {
299
- for (const config of this.config.nodes ?? []) {
300
- if (config.toShape &&
301
- (0, typescript_1.getKindName)(node) === config.node &&
302
- (!config.fileTypes ||
303
- config.fileTypes.includes((0, util_1.getFileType)(node.getSourceFile().getFilePath(), context.project)))) {
304
- const result = setCreator(this, await config.toShape.bind(this)(node, { ...context, self: this }));
305
- if (result.success) {
306
- return this.nodeToShapeCache.put(node.compilerNode, result);
307
- }
308
- }
309
- }
310
- return this.nodeToShapeCache.put(node.compilerNode, { success: false });
311
- }
312
- catch (e) {
313
- throw this.nodeToShapeCache.error(node.compilerNode, e);
314
- }
315
- }
316
455
  async commit(shape, target, context) {
317
456
  for (const { shape: shapeClass, commit } of this.config.shapes ?? []) {
318
457
  if (shape.is(shapeClass) && commit) {
@@ -322,7 +461,7 @@ class Plugin {
322
461
  }
323
462
  }
324
463
  }
325
- return { success: false };
464
+ return FAILURE;
326
465
  }
327
466
  async getTarget(shape, context) {
328
467
  for (const { shape: shapeClass, getTarget } of this.config.shapes ?? []) {
@@ -333,85 +472,43 @@ class Plugin {
333
472
  }
334
473
  }
335
474
  }
336
- return { success: false };
337
- }
338
- async shapeToSubclass(shape, context) {
339
- const entry = this.shapeToSubclassCache.get(shape);
340
- if (entry) {
341
- return entry.success ? entry.value : { success: false };
342
- }
343
- try {
344
- for (const config of this.config.shapes ?? []) {
345
- if (config.toSubclass &&
346
- shape.constructor === config.shape &&
347
- (!config.fileTypes ||
348
- config.fileTypes.includes((0, util_1.getFileType)(shape.getOriginalFilePath(), context.project)))) {
349
- const result = setCreator(this, await config.toSubclass.bind(this)(shape, {
350
- ...context,
351
- self: this,
352
- }));
353
- if (result.success) {
354
- if (result.value.constructor === config.shape) {
355
- throw new Error(`Result of subclassing "${config.shape.name}" is an instance of the same class. The result MUST be a subclass.`);
356
- }
357
- return this.shapeToSubclassCache.put(shape, result);
358
- }
359
- }
360
- }
361
- return this.shapeToSubclassCache.put(shape, { success: false });
362
- }
363
- catch (e) {
364
- throw this.shapeToSubclassCache.error(shape, e);
365
- }
366
- }
367
- async shapeToRecord(shape, context) {
368
- const entry = this.shapeToRecordCache.get(shape);
369
- if (entry) {
370
- return entry.success ? entry.value : { success: false };
371
- }
372
- try {
373
- for (const config of this.config.shapes ?? []) {
374
- if (config.toRecord &&
375
- shape.is(config.shape) &&
376
- (!config.fileTypes ||
377
- config.fileTypes.includes((0, util_1.getFileType)(shape.getOriginalFilePath(), context.project)))) {
378
- const result = setCreator(this, await config.toRecord.bind(this)(shape, { ...context, self: this }));
379
- if (result.success) {
380
- return this.shapeToRecordCache.put(shape, result);
381
- }
382
- }
383
- }
384
- return this.shapeToRecordCache.put(shape, { success: false });
385
- }
386
- catch (e) {
387
- throw this.shapeToRecordCache.error(shape, e);
388
- }
475
+ return FAILURE;
389
476
  }
390
- async recordToShape0(record, context) {
477
+ async recordToShape0(record, context, existingRecord) {
391
478
  if (record.getAction() !== 'INSERT_OR_UPDATE') {
392
479
  return { success: true, value: new shape_1.DeletedShape({ source: record }) };
393
480
  }
394
481
  const table = record.getTable();
395
- for (const [configTable, { toShape }] of Object.entries(this.config.records ?? {})) {
396
- if ((configTable === '*' || configTable === table) && toShape) {
397
- const result = setCreator(this, await toShape.bind(this)(record, { ...context, self: this }));
398
- if (result.success) {
399
- return result;
400
- }
482
+ for (const [configTable, recordConfig] of Object.entries(this.config.records ?? {})) {
483
+ if (configTable !== '*' && configTable !== table) {
484
+ continue;
485
+ }
486
+ if (!recordConfig.toShape) {
487
+ continue;
488
+ }
489
+ record.startTracking();
490
+ const result = setCreator(this, await recordConfig.toShape.bind(this)(record, { ...context, self: this }));
491
+ if (result.success) {
492
+ const accessed = record.getTrackedFields();
493
+ let value = mergeOverrideFromRecord(record, result.value, accessed, existingRecord);
494
+ value = mergeProtectionPolicyFromRecord(record, value);
495
+ value = mergeMetaFromRecord(record, value);
496
+ return { ...result, value };
401
497
  }
402
498
  }
403
- return { success: false };
499
+ return FAILURE;
404
500
  }
405
- async recordToShape({ record, database, context, }) {
501
+ async recordToShape({ record, database, existingDatabase, context, }) {
406
502
  context.logger.debug(`Transforming record into shape: ${record.getTable()}.${record.getId().getValue()}`);
407
503
  const descendants = this.getDescendants(record, database);
504
+ const existingRecord = existingDatabase?.resolve(record.getId());
408
505
  return this.recordToShape0(record, {
409
506
  ...context,
410
507
  database,
411
508
  descendants: new database_1.Database(descendants.filter((r) => r.getAction() === 'INSERT_OR_UPDATE')),
412
- });
509
+ }, existingRecord);
413
510
  }
414
- async recordsToShapes({ database, creatorOnly, changedOnly = false, mode, handledRecords, context, }) {
511
+ async recordsToShapes({ database, existingDatabase, creatorOnly, changedOnly = false, mode, handledRecords, context, }) {
415
512
  let success = false;
416
513
  const shapes = [];
417
514
  const unhandledRecords = [];
@@ -454,11 +551,12 @@ class Plugin {
454
551
  nonDeletedDescendants.insert(descendant);
455
552
  }
456
553
  }
554
+ const existingRecord = existingDatabase?.resolve(record.getId());
457
555
  const result = await this.recordToShape0(record, {
458
556
  ...context,
459
557
  database,
460
558
  descendants: nonDeletedDescendants,
461
- });
559
+ }, existingRecord);
462
560
  if (result.success === 'partial') {
463
561
  for (const unhandledRecord of result.unhandledRecords) {
464
562
  unhandledRecords.push(unhandledRecord);
@@ -484,7 +582,7 @@ class Plugin {
484
582
  }
485
583
  else if (!descendant.getCreator() ||
486
584
  !record.getCreator() ||
487
- descendant.getCreator().getName() === record.getCreator().getName()) {
585
+ descendant.getCreator()?.getName() === record.getCreator()?.getName()) {
488
586
  // This descendant is handled by the same plugin that handled the root record
489
587
  handledRecords.insert(descendant);
490
588
  }
@@ -494,7 +592,7 @@ class Plugin {
494
592
  }
495
593
  }
496
594
  if (unhandledRecords.length > 0) {
497
- context.logger.debug(`Plugin ${this.getName()} deferred handling of records: ${unhandledRecords.map((r) => r.getTable() + '_' + r.getId().getValue()).join(', ')}`);
595
+ context.logger.debug(`Plugin ${this.getName()} deferred handling of records: ${unhandledRecords.map((r) => `${r.getTable()}_${r.getId().getValue()}`).join(', ')}`);
498
596
  return { success: 'partial', value: shapes, unhandledRecords };
499
597
  }
500
598
  return { success, value: shapes };
@@ -575,7 +673,7 @@ class Plugin {
575
673
  }
576
674
  }
577
675
  }
578
- return { success: false };
676
+ return FAILURE;
579
677
  }
580
678
  async recordsToFiles({ database, mode, handledGuids, context, }) {
581
679
  let success = false;
@@ -618,32 +716,36 @@ class Plugin {
618
716
  }
619
717
  return { success, value: files };
620
718
  }
621
- async isEntryPoint(pathOrNode, context) {
622
- if (typescript_1.ts.Node.isNode(pathOrNode)) {
623
- for (const config of this.config.nodes ?? []) {
624
- if ((0, typescript_1.getKindName)(pathOrNode) !== config.node) {
625
- continue;
626
- }
627
- const fileType = (0, util_1.getFileType)(pathOrNode.getSourceFile().getFilePath(), context.project);
628
- if (config.fileTypes && !config.fileTypes.includes(fileType)) {
629
- continue;
630
- }
631
- return !!config.entryPoint;
719
+ getEntryPoints() {
720
+ const entryPoints = {
721
+ nodes: new Map(),
722
+ files: [],
723
+ };
724
+ for (const config of this.config.nodes ?? []) {
725
+ if (!config.entryPoint) {
726
+ continue;
727
+ }
728
+ const fileTypes = entryPoints.nodes.get(config.node) ?? new Set();
729
+ if (config.fileTypes) {
730
+ // Plugin only accepts the node from certain file types
731
+ config.fileTypes.forEach((t) => fileTypes.add(t));
632
732
  }
733
+ else {
734
+ // Plugin accepts the node from any file type
735
+ fileTypes.add('*');
736
+ }
737
+ entryPoints.nodes.set(config.node, fileTypes);
633
738
  }
634
- else {
635
- for (const config of this.config.files ?? []) {
636
- if (config.matcher instanceof RegExp && !config.matcher.test(pathOrNode)) {
637
- continue;
638
- }
639
- if (typeof config.matcher === 'function' &&
640
- !(await config.matcher(pathOrNode, { ...context, self: this }))) {
641
- continue;
642
- }
643
- return !!config.entryPoint;
739
+ for (const config of this.config.files ?? []) {
740
+ if (!config.entryPoint) {
741
+ continue;
742
+ }
743
+ if (!config.matcher) {
744
+ throw new Error(`Plugin "${this.getName()}" defines a file entry point with no matcher. This would cause all files to be treated as entry points.`);
644
745
  }
746
+ entryPoints.files.push({ plugin: this, matcher: config.matcher });
645
747
  }
646
- return false;
748
+ return entryPoints;
647
749
  }
648
750
  async fileToRecord(file, context) {
649
751
  for (const config of this.config.files ?? []) {
@@ -661,7 +763,7 @@ class Plugin {
661
763
  return result;
662
764
  }
663
765
  }
664
- return { success: false };
766
+ return FAILURE;
665
767
  }
666
768
  inspect(shape, context) {
667
769
  for (const config of this.config.shapes ?? []) {
@@ -680,32 +782,110 @@ class Plugin {
680
782
  }
681
783
  }
682
784
  }
683
- return { success: false };
785
+ return FAILURE;
786
+ }
787
+ getConfig() {
788
+ return this.config;
684
789
  }
685
790
  }
686
791
  exports.Plugin = Plugin;
687
792
  class Plugins {
688
- plugins;
793
+ plugins = new Map();
794
+ nodeToShapeCache = new WeakMap();
795
+ shapeToSubclassCache = new WeakMap();
796
+ shapeToRecordCache = new WeakMap();
797
+ entryPoints = {
798
+ nodes: new Map(),
799
+ files: [],
800
+ };
689
801
  constructor(plugins) {
690
- this.plugins = plugins;
802
+ for (const plugin of plugins) {
803
+ this.plugins.set(plugin.getName(), Plugins.createEntry(plugin));
804
+ }
805
+ this.reload();
691
806
  }
692
- toArray() {
693
- return this.plugins;
807
+ static createEntry(plugin) {
808
+ return {
809
+ plugin,
810
+ shapeCount: 0,
811
+ nodeToShapeCache: new WeakMap(),
812
+ shapeToSubclassCache: new WeakMap(),
813
+ shapeToRecordCache: new WeakMap(),
814
+ };
815
+ }
816
+ get(plugin) {
817
+ const name = plugin instanceof Plugin ? plugin.getName() : plugin;
818
+ const entry = this.plugins.get(name);
819
+ if (entry) {
820
+ return entry;
821
+ }
822
+ if (plugin instanceof Plugin) {
823
+ // 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.
824
+ return Plugins.createEntry(plugin);
825
+ }
826
+ throw new Error(`No such plugin with name "${name}"`);
827
+ }
828
+ *[Symbol.iterator]() {
829
+ for (const entry of this.plugins.values()) {
830
+ yield entry.plugin;
831
+ }
694
832
  }
695
- remove(name) {
696
- const index = this.plugins.findIndex((plugin) => plugin.getName() === name);
697
- if (index !== -1) {
698
- this.plugins.splice(index, 1);
833
+ reload() {
834
+ this.nodeToShapeCache = new WeakMap();
835
+ this.shapeToSubclassCache = new WeakMap();
836
+ this.shapeToRecordCache = new WeakMap();
837
+ this.entryPoints.nodes.clear();
838
+ this.entryPoints.files.length = 0;
839
+ for (const entry of this.plugins.values()) {
840
+ entry.nodeToShapeCache = new WeakMap();
841
+ entry.shapeToSubclassCache = new WeakMap();
842
+ entry.shapeToRecordCache = new WeakMap();
843
+ entry.shapeCount = 0;
844
+ const { nodes, files } = entry.plugin.getEntryPoints();
845
+ for (const [nodeKind, fileTypes] of nodes.entries()) {
846
+ const existing = this.entryPoints.nodes.get(nodeKind) ?? new Set();
847
+ for (const type of fileTypes) {
848
+ existing.add(type);
849
+ }
850
+ this.entryPoints.nodes.set(nodeKind, existing);
851
+ }
852
+ this.entryPoints.files.push(...files);
699
853
  }
700
854
  }
701
855
  prepend(...plugins) {
702
- this.plugins.unshift(...plugins);
856
+ const newMap = new Map();
857
+ for (const plugin of plugins) {
858
+ const name = plugin.getName();
859
+ newMap.set(name, Plugins.createEntry(plugin));
860
+ this.plugins.delete(name);
861
+ }
862
+ for (const [name, entry] of this.plugins.entries()) {
863
+ newMap.set(name, entry);
864
+ }
865
+ this.plugins = newMap;
866
+ this.reload();
703
867
  }
704
868
  append(...plugins) {
705
- this.plugins.push(...plugins);
869
+ for (const plugin of plugins) {
870
+ const name = plugin.getName();
871
+ this.plugins.delete(name);
872
+ this.plugins.set(name, Plugins.createEntry(plugin));
873
+ }
874
+ this.reload();
875
+ }
876
+ getShapeCountPerPlugin() {
877
+ return [...this.plugins.entries()].map(([name, { shapeCount }]) => ({
878
+ plugin: name.replace(/Plugin$/, ''), //Report just the names 'ACL', 'Record', etc for existing metrics compatibility
879
+ count: shapeCount,
880
+ }));
881
+ }
882
+ clearShapeCounts() {
883
+ for (const entry of this.plugins.values()) {
884
+ entry.shapeCount = 0;
885
+ }
706
886
  }
707
887
  getCoalesceStrategy(table) {
708
- for (const plugin of this.plugins) {
888
+ for (const plugin of this) {
709
889
  const coalesceStrategy = plugin.getCoalesceStrategy(table);
710
890
  if (coalesceStrategy) {
711
891
  return coalesceStrategy;
@@ -714,7 +894,7 @@ class Plugins {
714
894
  return undefined;
715
895
  }
716
896
  isComposite(table) {
717
- for (const plugin of this.plugins) {
897
+ for (const plugin of this) {
718
898
  if (plugin.isComposite(table)) {
719
899
  return true;
720
900
  }
@@ -723,7 +903,7 @@ class Plugins {
723
903
  }
724
904
  getCoalesceTables() {
725
905
  const tables = new Set();
726
- for (const plugin of this.plugins) {
906
+ for (const plugin of this) {
727
907
  plugin.getCoalesceTables().forEach((t) => {
728
908
  tables.add(t);
729
909
  });
@@ -732,27 +912,189 @@ class Plugins {
732
912
  }
733
913
  getReferenceColumns(table) {
734
914
  const refColumnEntries = [];
735
- for (const plugin of this.plugins) {
915
+ for (const plugin of this) {
736
916
  refColumnEntries.push(...Object.entries(plugin.getReferenceColumns(table)));
737
917
  }
738
918
  return Object.fromEntries(refColumnEntries);
739
919
  }
740
920
  async isEntryPoint(pathOrNode, context) {
741
- for (const plugin of this.plugins) {
742
- if (await plugin.isEntryPoint(pathOrNode, context)) {
921
+ if (typescript_1.ts.Node.isNode(pathOrNode)) {
922
+ const fileTypes = this.entryPoints.nodes.get((0, typescript_1.getKindName)(pathOrNode));
923
+ if (!fileTypes || fileTypes.size <= 0) {
924
+ return false;
925
+ }
926
+ else if (fileTypes.has('*')) {
743
927
  return true;
744
928
  }
929
+ else {
930
+ return fileTypes.has((0, util_1.getFileType)(pathOrNode.getSourceFile().getFilePath(), context.project));
931
+ }
932
+ }
933
+ else {
934
+ for (const { plugin, matcher } of this.entryPoints.files) {
935
+ if (matcher instanceof RegExp && matcher.test(pathOrNode)) {
936
+ return true;
937
+ }
938
+ else if (typeof matcher === 'function' && (await matcher(pathOrNode, { ...context, self: plugin }))) {
939
+ return true;
940
+ }
941
+ }
745
942
  }
746
943
  return false;
747
944
  }
748
- getDocsMetadata() {
749
- return this.plugins.reduce((aggregated, plugin) => {
750
- const pluginManifest = plugin.getDocsMetadata();
751
- if (pluginManifest) {
752
- Object.assign(aggregated, pluginManifest);
945
+ async nodeToShape(node, context, ...plugins) {
946
+ if (plugins.length > 0) {
947
+ // Bypass global cache when requesting specific plugins
948
+ return this.nodeToShape0(node, context, ...plugins);
949
+ }
950
+ // We're not requesting specific plugins, so it's safe to use the global cache
951
+ const cached = this.nodeToShapeCache.get(node.compilerNode);
952
+ if (cached) {
953
+ return cached;
954
+ }
955
+ try {
956
+ const result = await this.nodeToShape0(node, context);
957
+ this.nodeToShapeCache.set(node.compilerNode, result);
958
+ return result;
959
+ }
960
+ catch (e) {
961
+ this.nodeToShapeCache.set(node.compilerNode, FAILURE);
962
+ throw e;
963
+ }
964
+ }
965
+ async nodeToShape0(node, context, ...plugins) {
966
+ for (const entry of plugins.length > 0 ? plugins.map((plugin) => this.get(plugin)) : this.plugins.values()) {
967
+ const cached = entry.nodeToShapeCache.get(node.compilerNode);
968
+ if (cached) {
969
+ if (cached.success) {
970
+ return cached;
971
+ }
972
+ else {
973
+ continue;
974
+ }
975
+ }
976
+ for (const config of entry.plugin.getConfig().nodes ?? []) {
977
+ if (config.toShape &&
978
+ (0, typescript_1.getKindName)(node) === config.node &&
979
+ (!config.fileTypes ||
980
+ config.fileTypes.includes((0, util_1.getFileType)(node.getSourceFile().getFilePath(), context.project)))) {
981
+ try {
982
+ const result = setCreator(entry.plugin, await config.toShape.bind(entry.plugin)(node, { ...context, self: entry.plugin }));
983
+ entry.nodeToShapeCache.set(node.compilerNode, result);
984
+ if (result.success) {
985
+ entry.shapeCount++;
986
+ return result;
987
+ }
988
+ }
989
+ catch (error) {
990
+ entry.nodeToShapeCache.set(node.compilerNode, FAILURE);
991
+ context.logger.error(`Plugin "${entry.plugin.getName()}" failed to transform "${node.getKindName()}" node into shape:`, error);
992
+ }
993
+ }
994
+ }
995
+ }
996
+ return FAILURE;
997
+ }
998
+ async shapeToSubclass(shape, context, ...plugins) {
999
+ if (plugins.length > 0) {
1000
+ // Bypass global cache when requesting specific plugins
1001
+ return this.shapeToSubclass0(shape, context, ...plugins);
1002
+ }
1003
+ // We're not requesting specific plugins, so it's safe to use the global cache
1004
+ const cached = this.shapeToSubclassCache.get(shape);
1005
+ if (cached) {
1006
+ return cached;
1007
+ }
1008
+ const subclass = await this.shapeToSubclass0(shape, context);
1009
+ this.shapeToSubclassCache.set(shape, subclass);
1010
+ return subclass;
1011
+ }
1012
+ async shapeToSubclass0(shape, context, ...plugins) {
1013
+ for (const entry of plugins.length > 0 ? plugins.map((plugin) => this.get(plugin)) : this.plugins.values()) {
1014
+ const cached = entry.shapeToSubclassCache.get(shape);
1015
+ if (cached) {
1016
+ if (cached.success) {
1017
+ return cached.value;
1018
+ }
1019
+ else {
1020
+ continue;
1021
+ }
1022
+ }
1023
+ for (const config of entry.plugin.getConfig().shapes ?? []) {
1024
+ if (config.toSubclass &&
1025
+ shape.constructor === config.shape &&
1026
+ (!config.fileTypes ||
1027
+ config.fileTypes.includes((0, util_1.getFileType)(shape.getOriginalFilePath(), context.project)))) {
1028
+ try {
1029
+ const result = setCreator(entry.plugin, await config.toSubclass.bind(entry.plugin)(shape, { ...context, self: entry.plugin }));
1030
+ entry.shapeToSubclassCache.set(shape, result);
1031
+ if (result.success) {
1032
+ if (result.value.constructor === config.shape) {
1033
+ throw new Error(`Plugin "${entry.plugin.getName()}" tried to subclass "${config.shape.name}" to an instance of the same class. The result MUST be a subclass.`);
1034
+ }
1035
+ entry.shapeCount++;
1036
+ return await this.shapeToSubclass(result.value, context, ...plugins);
1037
+ }
1038
+ }
1039
+ catch (error) {
1040
+ entry.shapeToSubclassCache.set(shape, FAILURE);
1041
+ context.logger.error(`Plugin "${entry.plugin.getName()}" failed to subclass "${shape.getKind()}" shape:`, error);
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+ return shape;
1047
+ }
1048
+ async shapeToRecord(shape, context, ...plugins) {
1049
+ if (shape.isRecord()) {
1050
+ return { success: true, value: shape };
1051
+ }
1052
+ if (plugins.length > 0) {
1053
+ // Bypass global cache when requesting specific plugins
1054
+ return this.shapeToRecord0(shape, context, ...plugins);
1055
+ }
1056
+ // We're not requesting specific plugins, so it's safe to use the global cache
1057
+ const cached = this.shapeToRecordCache.get(shape);
1058
+ if (cached) {
1059
+ return cached;
1060
+ }
1061
+ const result = await this.shapeToRecord0(shape, context);
1062
+ this.shapeToRecordCache.set(shape, result);
1063
+ return result;
1064
+ }
1065
+ async shapeToRecord0(shape, context, ...plugins) {
1066
+ for (const entry of plugins.length > 0 ? plugins.map((plugin) => this.get(plugin)) : this.plugins.values()) {
1067
+ const cached = entry.shapeToRecordCache.get(shape);
1068
+ if (cached) {
1069
+ if (cached.success) {
1070
+ return cached;
1071
+ }
1072
+ else {
1073
+ continue;
1074
+ }
1075
+ }
1076
+ for (const config of entry.plugin.getConfig().shapes ?? []) {
1077
+ if (config.toRecord &&
1078
+ shape.is(config.shape) &&
1079
+ (!config.fileTypes ||
1080
+ config.fileTypes.includes((0, util_1.getFileType)(shape.getOriginalFilePath(), context.project)))) {
1081
+ try {
1082
+ const result = setCreator(entry.plugin, await config.toRecord.bind(entry.plugin)(shape, { ...context, self: entry.plugin }));
1083
+ entry.shapeToRecordCache.set(shape, result);
1084
+ if (result.success) {
1085
+ entry.shapeCount++;
1086
+ result.value = mergeOverrideFromShape(shape, mergeSysPolicyFromShape(shape, result.value), context.diagnostics);
1087
+ return result;
1088
+ }
1089
+ }
1090
+ catch (error) {
1091
+ entry.shapeToRecordCache.set(shape, FAILURE);
1092
+ context.logger.error(`Plugin "${entry.plugin.getName()}" failed to transform "${shape.getKind()}" shape into record:`, error);
1093
+ }
1094
+ }
753
1095
  }
754
- return aggregated;
755
- }, {});
1096
+ }
1097
+ return FAILURE;
756
1098
  }
757
1099
  }
758
1100
  exports.Plugins = Plugins;