@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.
- package/dist/compiler.d.ts +2 -0
- package/dist/compiler.js +13 -7
- package/dist/compiler.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/now-config.d.ts +51 -3
- package/dist/now-config.js +44 -1
- package/dist/now-config.js.map +1 -1
- package/dist/package-inventory.d.ts +15 -0
- package/dist/package-inventory.js +59 -0
- package/dist/package-inventory.js.map +1 -0
- package/dist/plugins/context.d.ts +2 -2
- package/dist/plugins/file.d.ts +1 -0
- package/dist/plugins/index.d.ts +0 -1
- package/dist/plugins/index.js +0 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/plugin.d.ts +41 -53
- package/dist/plugins/plugin.js +517 -162
- package/dist/plugins/plugin.js.map +1 -1
- package/dist/plugins/shape.d.ts +13 -2
- package/dist/plugins/shape.js +96 -15
- package/dist/plugins/shape.js.map +1 -1
- package/dist/taxonomy.js +7 -2
- package/dist/taxonomy.js.map +1 -1
- package/dist/telemetry/clients/detect-agent.d.ts +4 -0
- package/dist/telemetry/clients/detect-agent.js +84 -0
- package/dist/telemetry/clients/detect-agent.js.map +1 -0
- package/dist/telemetry/clients/node-client.d.ts +2 -0
- package/dist/telemetry/clients/node-client.js +10 -9
- package/dist/telemetry/clients/node-client.js.map +1 -1
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/xml.d.ts +2 -2
- package/dist/xml.js +2 -2
- package/dist/xml.js.map +1 -1
- package/now.config.schema.json +54 -0
- package/package.json +9 -5
- package/src/compiler.ts +14 -7
- package/src/index.ts +1 -0
- package/src/now-config.ts +56 -1
- package/src/package-inventory.ts +75 -0
- package/src/plugins/context.ts +2 -2
- package/src/plugins/file.ts +1 -0
- package/src/plugins/index.ts +0 -1
- package/src/plugins/plugin.ts +704 -231
- package/src/plugins/shape.ts +115 -24
- package/src/taxonomy.ts +8 -2
- package/src/telemetry/clients/detect-agent.ts +88 -0
- package/src/telemetry/clients/node-client.ts +12 -8
- package/src/telemetry/index.ts +1 -1
- package/src/xml.ts +11 -2
- package/dist/plugins/cache.d.ts +0 -15
- package/dist/plugins/cache.js +0 -22
- package/dist/plugins/cache.js.map +0 -1
- package/dist/plugins/usage.d.ts +0 -11
- package/dist/plugins/usage.js +0 -26
- package/dist/plugins/usage.js.map +0 -1
- package/src/plugins/cache.ts +0 -23
- package/src/plugins/usage.ts +0 -26
package/src/plugins/plugin.ts
CHANGED
|
@@ -5,17 +5,26 @@ import {
|
|
|
5
5
|
type SupportedNodeByKindName,
|
|
6
6
|
ts,
|
|
7
7
|
} from '../typescript'
|
|
8
|
-
import {
|
|
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 {
|
|
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 |
|
|
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
|
|
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
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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,
|
|
913
|
-
if (
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
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
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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()
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
1278
|
+
hostedPluginDir: hosted,
|
|
1144
1279
|
})),
|
|
1145
1280
|
}
|
|
1146
1281
|
}
|
|
@@ -1151,7 +1286,7 @@ export class Plugin {
|
|
|
1151
1286
|
}
|
|
1152
1287
|
}
|
|
1153
1288
|
|
|
1154
|
-
return
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
}
|
|
1348
|
+
getEntryPoints(): EntryPoints {
|
|
1349
|
+
const entryPoints: EntryPoints = {
|
|
1350
|
+
nodes: new Map(),
|
|
1351
|
+
files: [],
|
|
1352
|
+
}
|
|
1219
1353
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1354
|
+
for (const config of this.config.nodes ?? []) {
|
|
1355
|
+
if (!config.entryPoint) {
|
|
1356
|
+
continue
|
|
1357
|
+
}
|
|
1224
1358
|
|
|
1225
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
!(await config.matcher(pathOrNode, { ...context, self: this }))
|
|
1236
|
-
) {
|
|
1237
|
-
continue
|
|
1238
|
-
}
|
|
1368
|
+
entryPoints.nodes.set(config.node, fileTypes)
|
|
1369
|
+
}
|
|
1239
1370
|
|
|
1240
|
-
|
|
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
|
|
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
|
|
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
|
|
1430
|
+
return FAILURE
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
getConfig(): PluginConfig {
|
|
1434
|
+
return this.config
|
|
1290
1435
|
}
|
|
1291
1436
|
}
|
|
1292
1437
|
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
1297
|
-
return this.plugins
|
|
1488
|
+
throw new Error(`No such plugin with name "${name}"`)
|
|
1298
1489
|
}
|
|
1299
1490
|
|
|
1300
|
-
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1358
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
}
|