@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,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 };
@@ -548,12 +646,25 @@ class Plugin {
548
646
  const filePath = sdk_build_core_1.path.normalize(record.getOriginalFilePath());
549
647
  if ((0, util_1.isSNScope)(context.config.scope) && filePath.endsWith(__1.NOW_FILE_EXTENSION)) {
550
648
  const relativePath = sdk_build_core_1.path.relative(sdk_build_core_1.path.join(context.config.fluentDir, 'if'), filePath);
551
- if (!relativePath.startsWith('..') && sdk_build_core_1.path.dirname(relativePath) !== '.') {
649
+ const relativePathGenerated = sdk_build_core_1.path.relative(sdk_build_core_1.path.join(context.config.generatedDir, 'if'), filePath);
650
+ if (!relativePath.startsWith('..') ||
651
+ (!relativePathGenerated.startsWith('..') && sdk_build_core_1.path.dirname(relativePath) !== '.')) {
652
+ const subPath = relativePath.startsWith('..') ? relativePathGenerated : relativePath;
552
653
  return {
553
654
  success: true,
554
655
  value: outputFiles.map((f) => ({
555
656
  ...f,
556
- ifDirectoryPackage: sdk_build_core_1.path.join('if', sdk_build_core_1.path.dirname(relativePath)),
657
+ ifDirectoryPackage: sdk_build_core_1.path.join('if', subPath.split(sdk_build_core_1.path.sep)[0]),
658
+ })),
659
+ };
660
+ }
661
+ const hosted = __1.NowConfig.getHostedDirectory(context.config, filePath);
662
+ if (hosted) {
663
+ return {
664
+ success: true,
665
+ value: outputFiles.map((f) => ({
666
+ ...f,
667
+ hostedPluginDir: hosted,
557
668
  })),
558
669
  };
559
670
  }
@@ -562,7 +673,7 @@ class Plugin {
562
673
  }
563
674
  }
564
675
  }
565
- return { success: false };
676
+ return FAILURE;
566
677
  }
567
678
  async recordsToFiles({ database, mode, handledGuids, context, }) {
568
679
  let success = false;
@@ -605,32 +716,36 @@ class Plugin {
605
716
  }
606
717
  return { success, value: files };
607
718
  }
608
- async isEntryPoint(pathOrNode, context) {
609
- if (typescript_1.ts.Node.isNode(pathOrNode)) {
610
- for (const config of this.config.nodes ?? []) {
611
- if ((0, typescript_1.getKindName)(pathOrNode) !== config.node) {
612
- continue;
613
- }
614
- const fileType = (0, util_1.getFileType)(pathOrNode.getSourceFile().getFilePath(), context.project);
615
- if (config.fileTypes && !config.fileTypes.includes(fileType)) {
616
- continue;
617
- }
618
- 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;
619
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));
732
+ }
733
+ else {
734
+ // Plugin accepts the node from any file type
735
+ fileTypes.add('*');
736
+ }
737
+ entryPoints.nodes.set(config.node, fileTypes);
620
738
  }
621
- else {
622
- for (const config of this.config.files ?? []) {
623
- if (config.matcher instanceof RegExp && !config.matcher.test(pathOrNode)) {
624
- continue;
625
- }
626
- if (typeof config.matcher === 'function' &&
627
- !(await config.matcher(pathOrNode, { ...context, self: this }))) {
628
- continue;
629
- }
630
- 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.`);
631
745
  }
746
+ entryPoints.files.push({ plugin: this, matcher: config.matcher });
632
747
  }
633
- return false;
748
+ return entryPoints;
634
749
  }
635
750
  async fileToRecord(file, context) {
636
751
  for (const config of this.config.files ?? []) {
@@ -648,7 +763,7 @@ class Plugin {
648
763
  return result;
649
764
  }
650
765
  }
651
- return { success: false };
766
+ return FAILURE;
652
767
  }
653
768
  inspect(shape, context) {
654
769
  for (const config of this.config.shapes ?? []) {
@@ -667,32 +782,110 @@ class Plugin {
667
782
  }
668
783
  }
669
784
  }
670
- return { success: false };
785
+ return FAILURE;
786
+ }
787
+ getConfig() {
788
+ return this.config;
671
789
  }
672
790
  }
673
791
  exports.Plugin = Plugin;
674
792
  class Plugins {
675
- 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
+ };
676
801
  constructor(plugins) {
677
- this.plugins = plugins;
802
+ for (const plugin of plugins) {
803
+ this.plugins.set(plugin.getName(), Plugins.createEntry(plugin));
804
+ }
805
+ this.reload();
678
806
  }
679
- toArray() {
680
- 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
+ };
681
815
  }
682
- remove(name) {
683
- const index = this.plugins.findIndex((plugin) => plugin.getName() === name);
684
- if (index !== -1) {
685
- this.plugins.splice(index, 1);
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
+ }
832
+ }
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);
686
853
  }
687
854
  }
688
855
  prepend(...plugins) {
689
- 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();
690
867
  }
691
868
  append(...plugins) {
692
- 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
+ }
693
886
  }
694
887
  getCoalesceStrategy(table) {
695
- for (const plugin of this.plugins) {
888
+ for (const plugin of this) {
696
889
  const coalesceStrategy = plugin.getCoalesceStrategy(table);
697
890
  if (coalesceStrategy) {
698
891
  return coalesceStrategy;
@@ -701,7 +894,7 @@ class Plugins {
701
894
  return undefined;
702
895
  }
703
896
  isComposite(table) {
704
- for (const plugin of this.plugins) {
897
+ for (const plugin of this) {
705
898
  if (plugin.isComposite(table)) {
706
899
  return true;
707
900
  }
@@ -710,7 +903,7 @@ class Plugins {
710
903
  }
711
904
  getCoalesceTables() {
712
905
  const tables = new Set();
713
- for (const plugin of this.plugins) {
906
+ for (const plugin of this) {
714
907
  plugin.getCoalesceTables().forEach((t) => {
715
908
  tables.add(t);
716
909
  });
@@ -719,27 +912,189 @@ class Plugins {
719
912
  }
720
913
  getReferenceColumns(table) {
721
914
  const refColumnEntries = [];
722
- for (const plugin of this.plugins) {
915
+ for (const plugin of this) {
723
916
  refColumnEntries.push(...Object.entries(plugin.getReferenceColumns(table)));
724
917
  }
725
918
  return Object.fromEntries(refColumnEntries);
726
919
  }
727
920
  async isEntryPoint(pathOrNode, context) {
728
- for (const plugin of this.plugins) {
729
- 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('*')) {
730
927
  return true;
731
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
+ }
732
942
  }
733
943
  return false;
734
944
  }
735
- getDocsMetadata() {
736
- return this.plugins.reduce((aggregated, plugin) => {
737
- const pluginManifest = plugin.getDocsMetadata();
738
- if (pluginManifest) {
739
- 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
+ }
740
1022
  }
741
- return aggregated;
742
- }, {});
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
+ }
1095
+ }
1096
+ }
1097
+ return FAILURE;
743
1098
  }
744
1099
  }
745
1100
  exports.Plugins = Plugins;