@cratis/fundamentals 7.17.1 → 7.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/Fields.ts +9 -3
  2. package/StandardClassDecoratorContext.ts +14 -0
  3. package/StandardFieldDecoratorContext.ts +13 -0
  4. package/derivedTypeDecorator.ts +21 -4
  5. package/dist/ConceptAs.d.ts.map +1 -1
  6. package/dist/ConceptAs.js.map +1 -1
  7. package/dist/DateOnly.d.ts.map +1 -1
  8. package/dist/DateOnly.js.map +1 -1
  9. package/dist/DerivedType.d.ts.map +1 -1
  10. package/dist/Field.d.ts.map +1 -1
  11. package/dist/Field.js.map +1 -1
  12. package/dist/Fields.d.ts.map +1 -1
  13. package/dist/Fields.js +4 -0
  14. package/dist/Fields.js.map +1 -1
  15. package/dist/Guid.d.ts.map +1 -1
  16. package/dist/Guid.js.map +1 -1
  17. package/dist/JsonSerializer.d.ts.map +1 -1
  18. package/dist/JsonSerializer.js.map +1 -1
  19. package/dist/PropertyAccessorDescriptor.d.ts.map +1 -1
  20. package/dist/PropertyAccessorDescriptor.js.map +1 -1
  21. package/dist/PropertyPathResolverProxyHandler.d.ts.map +1 -1
  22. package/dist/PropertyPathResolverProxyHandler.js.map +1 -1
  23. package/dist/StandardClassDecoratorContext.d.ts +8 -0
  24. package/dist/StandardClassDecoratorContext.d.ts.map +1 -0
  25. package/dist/StandardClassDecoratorContext.js +2 -0
  26. package/dist/StandardClassDecoratorContext.js.map +1 -0
  27. package/dist/StandardFieldDecoratorContext.d.ts +8 -0
  28. package/dist/StandardFieldDecoratorContext.d.ts.map +1 -0
  29. package/dist/StandardFieldDecoratorContext.js +2 -0
  30. package/dist/StandardFieldDecoratorContext.js.map +1 -0
  31. package/dist/TimeOnly.d.ts.map +1 -1
  32. package/dist/TimeOnly.js.map +1 -1
  33. package/dist/TimeSpan.d.ts.map +1 -1
  34. package/dist/TimeSpan.js.map +1 -1
  35. package/dist/ValueMap.d.ts.map +1 -1
  36. package/dist/ValueMap.js.map +1 -1
  37. package/dist/cjs/Fields.d.ts.map +1 -1
  38. package/dist/cjs/Fields.js +4 -0
  39. package/dist/cjs/Fields.js.map +1 -1
  40. package/dist/cjs/StandardClassDecoratorContext.d.ts +8 -0
  41. package/dist/cjs/StandardClassDecoratorContext.d.ts.map +1 -0
  42. package/dist/cjs/StandardFieldDecoratorContext.d.ts +8 -0
  43. package/dist/cjs/StandardFieldDecoratorContext.d.ts.map +1 -0
  44. package/dist/cjs/derivedTypeDecorator.d.ts +5 -1
  45. package/dist/cjs/derivedTypeDecorator.d.ts.map +1 -1
  46. package/dist/cjs/derivedTypeDecorator.js +18 -2
  47. package/dist/cjs/derivedTypeDecorator.js.map +1 -1
  48. package/dist/cjs/fieldDecorator.d.ts +5 -1
  49. package/dist/cjs/fieldDecorator.d.ts.map +1 -1
  50. package/dist/cjs/fieldDecorator.js +33 -2
  51. package/dist/cjs/fieldDecorator.js.map +1 -1
  52. package/dist/cjs/fieldDecoratorMetadata.d.ts +6 -0
  53. package/dist/cjs/fieldDecoratorMetadata.d.ts.map +1 -0
  54. package/dist/cjs/fieldDecoratorMetadata.js +23 -0
  55. package/dist/cjs/fieldDecoratorMetadata.js.map +1 -0
  56. package/dist/cjs/reflection.d.ts.map +1 -1
  57. package/dist/cjs/reflection.js +9 -0
  58. package/dist/cjs/reflection.js.map +1 -1
  59. package/dist/derivedTypeDecorator.d.ts +5 -1
  60. package/dist/derivedTypeDecorator.d.ts.map +1 -1
  61. package/dist/derivedTypeDecorator.js +18 -2
  62. package/dist/derivedTypeDecorator.js.map +1 -1
  63. package/dist/esm/Fields.d.ts.map +1 -1
  64. package/dist/esm/Fields.js +4 -0
  65. package/dist/esm/Fields.js.map +1 -1
  66. package/dist/esm/StandardClassDecoratorContext.d.ts +8 -0
  67. package/dist/esm/StandardClassDecoratorContext.d.ts.map +1 -0
  68. package/dist/esm/StandardFieldDecoratorContext.d.ts +8 -0
  69. package/dist/esm/StandardFieldDecoratorContext.d.ts.map +1 -0
  70. package/dist/esm/derivedTypeDecorator.d.ts +5 -1
  71. package/dist/esm/derivedTypeDecorator.d.ts.map +1 -1
  72. package/dist/esm/derivedTypeDecorator.js +18 -2
  73. package/dist/esm/derivedTypeDecorator.js.map +1 -1
  74. package/dist/esm/fieldDecorator.d.ts +5 -1
  75. package/dist/esm/fieldDecorator.d.ts.map +1 -1
  76. package/dist/esm/fieldDecorator.js +33 -2
  77. package/dist/esm/fieldDecorator.js.map +1 -1
  78. package/dist/esm/fieldDecoratorMetadata.d.ts +6 -0
  79. package/dist/esm/fieldDecoratorMetadata.d.ts.map +1 -0
  80. package/dist/esm/fieldDecoratorMetadata.js +20 -0
  81. package/dist/esm/fieldDecoratorMetadata.js.map +1 -0
  82. package/dist/esm/reflection.d.ts.map +1 -1
  83. package/dist/esm/reflection.js +9 -0
  84. package/dist/esm/reflection.js.map +1 -1
  85. package/dist/fieldDecorator.d.ts +5 -1
  86. package/dist/fieldDecorator.d.ts.map +1 -1
  87. package/dist/fieldDecorator.js +33 -2
  88. package/dist/fieldDecorator.js.map +1 -1
  89. package/dist/fieldDecoratorMetadata.d.ts +6 -0
  90. package/dist/fieldDecoratorMetadata.d.ts.map +1 -0
  91. package/dist/fieldDecoratorMetadata.js +17 -0
  92. package/dist/fieldDecoratorMetadata.js.map +1 -0
  93. package/dist/geospatial/LineString.d.ts.map +1 -1
  94. package/dist/geospatial/LineString.js.map +1 -1
  95. package/dist/geospatial/LinearRing.d.ts.map +1 -1
  96. package/dist/geospatial/Point.d.ts.map +1 -1
  97. package/dist/geospatial/Point.js.map +1 -1
  98. package/dist/geospatial/Polygon.d.ts.map +1 -1
  99. package/dist/geospatial/Polygon.js.map +1 -1
  100. package/dist/json/DateJsonConverter.d.ts.map +1 -1
  101. package/dist/json/DateOnlyJsonConverter.d.ts.map +1 -1
  102. package/dist/json/GuidJsonConverter.d.ts.map +1 -1
  103. package/dist/json/JsonConverter.d.ts.map +1 -1
  104. package/dist/json/LineStringJsonConverter.d.ts.map +1 -1
  105. package/dist/json/PointJsonConverter.d.ts.map +1 -1
  106. package/dist/json/PolygonJsonConverter.d.ts.map +1 -1
  107. package/dist/json/TimeOnlyJsonConverter.d.ts.map +1 -1
  108. package/dist/json/TimeSpanJsonConverter.d.ts.map +1 -1
  109. package/dist/json/ValueMapJsonConverter.d.ts.map +1 -1
  110. package/dist/json/ValueMapJsonConverter.js.map +1 -1
  111. package/dist/reflection.d.ts.map +1 -1
  112. package/dist/reflection.js +9 -0
  113. package/dist/reflection.js.map +1 -1
  114. package/dist/tsconfig.tsbuildinfo +1 -1
  115. package/dist/typeKey.d.ts.map +1 -1
  116. package/dist/vite.config.d.mts.map +1 -1
  117. package/fieldDecorator.ts +36 -5
  118. package/fieldDecoratorMetadata.ts +34 -0
  119. package/for_JsonSerializer/when_metadata_is_written_by_another_copy_of_the_package.ts +23 -5
  120. package/for_derivedTypeDecorator/when_an_outer_standard_decorator_replaces_the_class.ts +28 -0
  121. package/for_fieldDecorator/fixtures/standard_decorated_types.ts +76 -0
  122. package/for_fieldDecorator/when_executing_standard_typescript_decorator_emit.ts +115 -0
  123. package/for_fieldDecorator/when_invoked_as_a_legacy_decorator.ts +21 -0
  124. package/for_fieldDecorator/when_invoked_as_a_standard_decorator.ts +43 -0
  125. package/for_fieldDecorator/when_invoked_for_an_unsupported_legacy_field/with_a_static_field.ts +12 -0
  126. package/for_fieldDecorator/when_invoked_for_an_unsupported_legacy_field/with_a_symbol_name.ts +12 -0
  127. package/for_fieldDecorator/when_invoked_for_an_unsupported_standard_field/with_a_private_field.ts +16 -0
  128. package/for_fieldDecorator/when_invoked_for_an_unsupported_standard_field/with_a_static_field.ts +16 -0
  129. package/for_fieldDecorator/when_invoked_for_an_unsupported_standard_field/with_a_symbol_name.ts +16 -0
  130. package/for_fieldDecorator/when_invoked_for_an_unsupported_standard_field/without_decorator_metadata.ts +16 -0
  131. package/for_fieldDecorator/when_legacy_and_standard_metadata_are_mixed_across_inheritance.ts +43 -0
  132. package/for_fieldDecorator/when_standard_metadata_contains_unrelated_fields_metadata.ts +34 -0
  133. package/for_fieldDecorator/when_standard_metadata_is_inherited_and_overridden.ts +37 -0
  134. package/for_reflection/when_loading_with_existing_symbol_metadata.ts +25 -0
  135. package/package.json +8 -1
  136. package/reflection.ts +13 -0
package/fieldDecorator.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { Constructor } from './Constructor';
5
+ import { Field } from './Field';
5
6
  import { Fields } from './Fields';
6
-
7
- /* eslint-disable @typescript-eslint/no-explicit-any */
7
+ import { addFieldToDecoratorMetadata } from './fieldDecoratorMetadata';
8
+ import { StandardFieldDecoratorContext } from './StandardFieldDecoratorContext';
8
9
 
9
10
  type fieldOptions = {
10
11
  enumerable?: boolean;
@@ -21,7 +22,37 @@ export function field(targetType: Constructor, enumerableOrOptions?: boolean | f
21
22
  const actualDerivatives = isFieldOptions(enumerableOrOptions) ? (enumerableOrOptions.derivatives || []) : (derivatives || []);
22
23
  const actualGenericArguments = isFieldOptions(enumerableOrOptions) ? (enumerableOrOptions.genericArguments || []) : (genericArguments || []);
23
24
 
24
- return function (target: any, propertyKey: string) {
25
- Fields.addFieldToType(target.constructor, propertyKey, targetType, enumerable, actualDerivatives, actualGenericArguments);
26
- };
25
+ function decorator(target: object, propertyKey: string): void;
26
+ function decorator(initialValue: undefined, context: StandardFieldDecoratorContext): void;
27
+ function decorator(targetOrInitialValue: object | undefined, propertyKeyOrContext: string | symbol | StandardFieldDecoratorContext): void {
28
+ if (typeof propertyKeyOrContext === 'object' && propertyKeyOrContext !== null) {
29
+ if (targetOrInitialValue !== undefined) throw new TypeError('@field received an invalid standard field value');
30
+ addStandardField(propertyKeyOrContext);
31
+ return;
32
+ }
33
+
34
+ addLegacyField(targetOrInitialValue, propertyKeyOrContext);
35
+ }
36
+
37
+ function addStandardField(context: StandardFieldDecoratorContext): void {
38
+ if (context.kind !== 'field') throw new TypeError('@field can only decorate fields');
39
+ if (context.static) throw new TypeError('@field cannot decorate static fields');
40
+ if (context.private) throw new TypeError('@field cannot decorate private fields');
41
+ if (typeof context.name !== 'string') throw new TypeError('@field cannot decorate symbol-named fields');
42
+ if (!context.metadata) throw new TypeError('@field requires standard decorator metadata support');
43
+
44
+ addFieldToDecoratorMetadata(
45
+ context.metadata,
46
+ new Field(context.name, targetType, enumerable, actualDerivatives, actualGenericArguments));
47
+ }
48
+
49
+ function addLegacyField(target: object | undefined, propertyKey: string | symbol): void {
50
+ if (typeof propertyKey === 'symbol') throw new TypeError('@field cannot decorate symbol-named fields');
51
+ if (typeof propertyKey !== 'string') throw new TypeError('@field received an invalid legacy decorator invocation');
52
+ if (!target || typeof target === 'function') throw new TypeError('@field cannot decorate static fields');
53
+
54
+ Fields.addFieldToType(target.constructor as Constructor, propertyKey, targetType, enumerable, actualDerivatives, actualGenericArguments);
55
+ }
56
+
57
+ return decorator;
27
58
  }
@@ -0,0 +1,34 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import './reflection';
5
+ import { Constructor } from './Constructor';
6
+ import { Field } from './Field';
7
+
8
+ const fieldsMetadataKey = Symbol.for('@cratis/fundamentals.fields');
9
+
10
+ /**
11
+ * Adds a field to the metadata object supplied to a standard decorator.
12
+ * @param metadata The class metadata object supplied by the decorator runtime.
13
+ * @param field The field to add.
14
+ */
15
+ export function addFieldToDecoratorMetadata(metadata: Record<PropertyKey, unknown>, field: Field): void {
16
+ const fields = Reflect.getOwnMetadata(fieldsMetadataKey, metadata) as Map<string, Field> | undefined ?? new Map<string, Field>();
17
+ fields.set(field.name, field);
18
+ Reflect.defineMetadata(fieldsMetadataKey, fields, metadata);
19
+ }
20
+
21
+ /**
22
+ * Gets the fields stored in the standard decorator metadata owned by a type.
23
+ * @param target The type to read metadata from.
24
+ * @returns The fields declared by the type through standard decorators.
25
+ */
26
+ export function getOwnDecoratorFieldsForType(target: Constructor): Field[] {
27
+ if (!Object.prototype.hasOwnProperty.call(target, Symbol.metadata)) return [];
28
+
29
+ const metadata = (target as Constructor & { [Symbol.metadata]?: Record<PropertyKey, unknown> | null })[Symbol.metadata];
30
+ if (!metadata) return [];
31
+
32
+ const fields = Reflect.getOwnMetadata(fieldsMetadataKey, metadata) as Map<string, Field> | undefined;
33
+ return fields ? [...fields.values()] : [];
34
+ }
@@ -2,17 +2,17 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { DerivedType } from '../DerivedType';
5
+ import { Field } from '../Field';
5
6
  import { Fields } from '../Fields';
7
+ import { addFieldToDecoratorMetadata, getOwnDecoratorFieldsForType } from '../fieldDecoratorMetadata';
6
8
 
7
9
  /**
8
10
  * Pins why `Fields` and `DerivedType` were left keyed on the constructor while the converter registry
9
11
  * was not.
10
12
  *
11
- * Neither holds state in module scope. Both write reflect metadata keyed on the target constructor, so
12
- * the state lives with the type being described rather than with the class describing it, and a
13
- * duplicated `Fields` reads what another `Fields` wrote. (In a real duplicate install the two copies
14
- * also share one metadata store, because the polyfill installs only when the global `Reflect` lacks it
15
- * - here a single `reflection` module is loaded, which is the same store by construction.)
13
+ * Neither holds state in module scope. Legacy metadata is keyed on the target constructor and standard
14
+ * metadata is keyed on the metadata object attached to that constructor. Both live in the shared
15
+ * Reflect metadata store, so a duplicated reader can read what another package copy wrote.
16
16
  *
17
17
  * That is the whole reason those two kept comparing constructors while the converter registry stopped.
18
18
  * If it ever stops being true, this goes red and they need the same treatment the converters got.
@@ -22,25 +22,43 @@ describe('when metadata is written by another copy of the package', () => {
22
22
  let derivedTypeIsDistinct: boolean;
23
23
  let fieldNamesReadByTheOtherCopy: string[];
24
24
  let derivedTypeReadByTheOtherCopy: string | undefined;
25
+ let standardFieldNamesReadByOtherFieldsCopy: string[];
26
+ let standardFieldNamesReadByTheOtherCopy: string[];
27
+ let standardMetadataCopiesAreDistinct: boolean;
25
28
 
26
29
  beforeEach(async () => {
27
30
  const otherFields = await import(/* @vite-ignore */ '../Fields?anotherCopy') as typeof import('../Fields');
28
31
  const otherDerivedType = await import(/* @vite-ignore */ '../DerivedType?anotherCopy') as typeof import('../DerivedType');
32
+ const standardMetadataWriter = await import(/* @vite-ignore */ '../fieldDecoratorMetadata?writerCopy') as typeof import('../fieldDecoratorMetadata');
33
+ const standardMetadataReader = await import(/* @vite-ignore */ '../fieldDecoratorMetadata?readerCopy') as typeof import('../fieldDecoratorMetadata');
29
34
 
30
35
  fieldsAreDistinct = Fields !== otherFields.Fields;
31
36
  derivedTypeIsDistinct = DerivedType !== otherDerivedType.DerivedType;
37
+ standardMetadataCopiesAreDistinct = standardMetadataWriter.addFieldToDecoratorMetadata !== addFieldToDecoratorMetadata &&
38
+ standardMetadataReader.getOwnDecoratorFieldsForType !== getOwnDecoratorFieldsForType &&
39
+ standardMetadataWriter.addFieldToDecoratorMetadata !== standardMetadataReader.addFieldToDecoratorMetadata;
32
40
 
33
41
  class Subject { }
34
42
 
35
43
  Fields.addFieldToType(Subject, 'name', String, false, [], []);
36
44
  DerivedType.set(Subject, 'an-identifier');
37
45
 
46
+ class StandardSubject { }
47
+ const metadata: Record<PropertyKey, unknown> = Object.create(null) as Record<PropertyKey, unknown>;
48
+ standardMetadataWriter.addFieldToDecoratorMetadata(metadata, new Field('standardName', String, false, [], []));
49
+ Object.defineProperty(StandardSubject, Symbol.metadata, { value: metadata });
50
+
38
51
  fieldNamesReadByTheOtherCopy = otherFields.Fields.getFieldsForType(Subject).map(_ => _.name);
39
52
  derivedTypeReadByTheOtherCopy = otherDerivedType.DerivedType.get(Subject);
53
+ standardFieldNamesReadByTheOtherCopy = standardMetadataReader.getOwnDecoratorFieldsForType(StandardSubject).map(_ => _.name);
54
+ standardFieldNamesReadByOtherFieldsCopy = otherFields.Fields.getFieldsForType(StandardSubject).map(_ => _.name);
40
55
  });
41
56
 
42
57
  it('should be a genuinely different Fields', () => fieldsAreDistinct.should.be.true);
43
58
  it('should be a genuinely different DerivedType', () => derivedTypeIsDistinct.should.be.true);
59
+ it('should use genuinely different standard metadata helpers', () => standardMetadataCopiesAreDistinct.should.be.true);
44
60
  it('should read a field the other copy wrote', () => fieldNamesReadByTheOtherCopy.should.deep.equal(['name']));
45
61
  it('should read a derived type the other copy wrote', () => derivedTypeReadByTheOtherCopy!.should.equal('an-identifier'));
62
+ it('should read a standard field another metadata helper copy wrote', () => standardFieldNamesReadByTheOtherCopy.should.deep.equal(['standardName']));
63
+ it('should expose standard metadata through another Fields copy', () => standardFieldNamesReadByOtherFieldsCopy.should.deep.equal(['standardName']));
46
64
  });
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Constructor } from '../Constructor';
5
+ import { DerivedType } from '../DerivedType';
6
+ import { derivedType } from '../derivedTypeDecorator';
7
+
8
+ class BaseType { }
9
+ class OriginalType extends BaseType { }
10
+ class ReplacementType extends OriginalType { }
11
+
12
+ describe('when an outer standard decorator replaces the class', () => {
13
+ const initializers: Array<(this: Constructor) => void> = [];
14
+
15
+ derivedType('replacement-id')(OriginalType, {
16
+ addInitializer(initializer) {
17
+ initializers.push(initializer);
18
+ },
19
+ kind: 'class',
20
+ metadata: {},
21
+ name: 'OriginalType'
22
+ });
23
+ initializers.forEach(initializer => initializer.call(ReplacementType));
24
+
25
+ it('should not register the discarded original class', () => (DerivedType.get(OriginalType) === undefined).should.be.true);
26
+ it('should register the final replacement class', () => DerivedType.get(ReplacementType).should.equal('replacement-id'));
27
+ it('should register the replacement with its base type', () => DerivedType.getDerivedTypesFor(BaseType).should.deep.equal([ReplacementType]));
28
+ });
@@ -0,0 +1,76 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ declare const fundamentals: typeof import('../../index');
5
+
6
+ const { derivedType, field, JsonSerializer, ValueMap } = fundamentals;
7
+
8
+ function replaceClass<Target extends import('../../Constructor').Constructor>(target: Target): Target {
9
+ return class extends target { } as Target;
10
+ }
11
+
12
+ class Shape {
13
+ @field(String)
14
+ label!: string;
15
+ }
16
+
17
+ @derivedType('circle')
18
+ class Circle extends Shape {
19
+ @field(Number)
20
+ radius!: number;
21
+ }
22
+
23
+ @derivedType('rectangle')
24
+ class Rectangle extends Shape {
25
+ @field(Number)
26
+ width!: number;
27
+
28
+ @field(Number)
29
+ height!: number;
30
+ }
31
+
32
+ @replaceClass
33
+ @derivedType('replacement')
34
+ class ReplacedShape extends Shape { }
35
+
36
+ class Drawing {
37
+ static instanceCount = 0;
38
+
39
+ @field(String)
40
+ title!: string;
41
+
42
+ @field(Date)
43
+ createdAt!: Date;
44
+
45
+ @field(Shape, true)
46
+ shapes!: Shape[];
47
+
48
+ @field(ValueMap, { genericArguments: [String, Number] })
49
+ scores!: InstanceType<typeof ValueMap<string, number>>;
50
+
51
+ constructor() {
52
+ Drawing.instanceCount++;
53
+ }
54
+ }
55
+
56
+ const instanceCountBeforeDeserialize = Drawing.instanceCount;
57
+ const drawing = JsonSerializer.deserialize(Drawing, JSON.stringify({
58
+ createdAt: '2026-08-16T09:30:00.000Z',
59
+ scores: {
60
+ first: 42
61
+ },
62
+ shapes: [{
63
+ _derivedTypeId: 'circle',
64
+ label: 'First',
65
+ radius: 12
66
+ }, {
67
+ _derivedTypeId: 'rectangle',
68
+ height: 4,
69
+ label: 'Second',
70
+ width: 8
71
+ }],
72
+ title: 'Standard decorators'
73
+ }));
74
+ const instanceCountAfterDeserialize = Drawing.instanceCount;
75
+
76
+ void [Circle, Rectangle, ReplacedShape, instanceCountBeforeDeserialize, drawing, instanceCountAfterDeserialize];
@@ -0,0 +1,115 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Script } from 'node:vm';
7
+ // TypeScript 7 (the native compiler used for `tsc -b` in this workspace) does not ship the
8
+ // legacy programmatic compiler API (createProgram, transpileModule, etc). This spec needs
9
+ // that API to compile and execute a decorator fixture in-memory, so it imports a nested
10
+ // TypeScript 6 dependency declared under a distinct package name instead of `typescript` -
11
+ // see Source/JavaScript/package.json and the TS6/TS7 side-by-side guidance at
12
+ // https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0
13
+ import { createProgram, getPreEmitDiagnostics, ModuleKind, ModuleResolutionKind, ScriptTarget, transpileModule } from 'typescript-programmatic-api';
14
+ import * as fundamentals from '../index';
15
+
16
+ describe('when executing standard TypeScript decorator emit', () => {
17
+ const originalMetadataDescriptor = Object.getOwnPropertyDescriptor(Symbol, 'metadata');
18
+ let emittedJavaScript: string;
19
+ let symbolMetadataWasAbsent: boolean;
20
+ let symbolMetadataWasPolyfilled: boolean;
21
+ let executionResult: {
22
+ Circle: fundamentals.Constructor;
23
+ Drawing: fundamentals.Constructor;
24
+ Rectangle: fundamentals.Constructor;
25
+ ReplacedShape: fundamentals.Constructor;
26
+ Shape: fundamentals.Constructor;
27
+ drawing: {
28
+ createdAt: Date;
29
+ scores: fundamentals.ValueMap<string, number>;
30
+ shapes: object[];
31
+ title: string;
32
+ };
33
+ instanceCountAfterDeserialize: number;
34
+ instanceCountBeforeDeserialize: number;
35
+ };
36
+ let registeredDerivedTypes: fundamentals.Constructor[];
37
+ let legacySemanticDiagnosticCount: number;
38
+ let semanticDiagnosticCount: number;
39
+
40
+ beforeAll(async () => {
41
+ Reflect.deleteProperty(Symbol, 'metadata');
42
+ symbolMetadataWasAbsent = !Object.prototype.hasOwnProperty.call(Symbol, 'metadata');
43
+
44
+ await import(/* @vite-ignore */ '../reflection?standardTypeScriptEmit');
45
+ symbolMetadataWasPolyfilled = Symbol.metadata === Symbol.for('Symbol.metadata');
46
+
47
+ const fixtureUrl = new URL('./fixtures/standard_decorated_types.ts', import.meta.url);
48
+ const fixturePath = fileURLToPath(fixtureUrl);
49
+ const source = readFileSync(fixtureUrl, 'utf8');
50
+ const compilerOptions = {
51
+ lib: ['lib.esnext.d.ts', 'lib.dom.d.ts'],
52
+ module: ModuleKind.ES2022,
53
+ moduleResolution: ModuleResolutionKind.Bundler,
54
+ noEmit: true,
55
+ noImplicitAny: false,
56
+ skipLibCheck: true,
57
+ strict: true,
58
+ target: ScriptTarget.ES2022
59
+ };
60
+ const standardProgram = createProgram({
61
+ options: {
62
+ ...compilerOptions,
63
+ emitDecoratorMetadata: false,
64
+ experimentalDecorators: false,
65
+ },
66
+ rootNames: [fixturePath]
67
+ });
68
+ const legacyProgram = createProgram({
69
+ options: {
70
+ ...compilerOptions,
71
+ emitDecoratorMetadata: true,
72
+ experimentalDecorators: true,
73
+ },
74
+ rootNames: [fixturePath]
75
+ });
76
+ semanticDiagnosticCount = getPreEmitDiagnostics(standardProgram).length;
77
+ legacySemanticDiagnosticCount = getPreEmitDiagnostics(legacyProgram).length;
78
+ emittedJavaScript = transpileModule(source, {
79
+ compilerOptions: {
80
+ emitDecoratorMetadata: false,
81
+ experimentalDecorators: false,
82
+ module: ModuleKind.None,
83
+ target: ScriptTarget.ES2022
84
+ },
85
+ fileName: 'standard_decorated_types.ts',
86
+ reportDiagnostics: true
87
+ }).outputText;
88
+
89
+ const script = new Script(`${emittedJavaScript}\n({ Circle, Drawing, Rectangle, ReplacedShape, Shape, drawing, instanceCountAfterDeserialize, instanceCountBeforeDeserialize })`);
90
+ executionResult = script.runInNewContext({ Array, Boolean, Date, fundamentals, JSON, Number, Object, String, Symbol }) as typeof executionResult;
91
+ registeredDerivedTypes = fundamentals.DerivedType.getDerivedTypesFor(executionResult.Shape);
92
+ });
93
+
94
+ afterAll(() => {
95
+ Reflect.deleteProperty(Symbol, 'metadata');
96
+ if (originalMetadataDescriptor) Object.defineProperty(Symbol, 'metadata', originalMetadataDescriptor);
97
+ });
98
+
99
+ it('should begin without runtime Symbol metadata support', () => symbolMetadataWasAbsent.should.be.true);
100
+ it('should install the Symbol metadata polyfill before class evaluation', () => symbolMetadataWasPolyfilled.should.be.true);
101
+ it('should type check the standard decorator signatures', () => semanticDiagnosticCount.should.equal(0));
102
+ it('should type check the legacy decorator signatures', () => legacySemanticDiagnosticCount.should.equal(0));
103
+ it('should execute standard decorator helper emit', () => emittedJavaScript.should.contain('__esDecorate'));
104
+ it('should expose field metadata before constructing an instance', () => executionResult.instanceCountBeforeDeserialize.should.equal(0));
105
+ it('should construct the target only during first-call deserialization', () => executionResult.instanceCountAfterDeserialize.should.equal(1));
106
+ it('should deserialize primitive fields', () => executionResult.drawing.title.should.equal('Standard decorators'));
107
+ it('should deserialize date fields', () => executionResult.drawing.createdAt.should.be.instanceOf(Date));
108
+ it('should deserialize polymorphic array entries', () => (executionResult.drawing.shapes[0] instanceof executionResult.Circle).should.be.true);
109
+ it('should deserialize every registered derivative', () => (executionResult.drawing.shapes[1] instanceof executionResult.Rectangle).should.be.true);
110
+ it('should auto-register standard derived type decorators', () => registeredDerivedTypes.should.deep.equal([executionResult.Circle, executionResult.Rectangle, executionResult.ReplacedShape]));
111
+ it('should register the final class returned by an outer standard decorator', () => fundamentals.DerivedType.get(executionResult.ReplacedShape).should.equal('replacement'));
112
+ it('should deserialize inherited fields', () => (executionResult.drawing.shapes[0] as { label: string }).label.should.equal('First'));
113
+ it('should deserialize derived fields', () => (executionResult.drawing.shapes[0] as { radius: number }).radius.should.equal(12));
114
+ it('should deserialize generic arguments', () => executionResult.drawing.scores.get('first')!.should.equal(42));
115
+ });
@@ -0,0 +1,21 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Fields } from '../Fields';
5
+ import { field } from '../fieldDecorator';
6
+
7
+ class FirstDerivative { }
8
+ class SecondDerivative { }
9
+ class LegacyDecoratedType { }
10
+
11
+ describe('when invoked as a legacy decorator', () => {
12
+ field(Number)(LegacyDecoratedType.prototype, 'count');
13
+ field(String, true, [FirstDerivative, SecondDerivative], [String, Number])(LegacyDecoratedType.prototype, 'values');
14
+
15
+ const fields = Fields.getFieldsForType(LegacyDecoratedType);
16
+
17
+ it('should register a primitive field', () => fields.find(_ => _.name === 'count')!.type.should.equal(Number));
18
+ it('should preserve enumerable semantics', () => fields.find(_ => _.name === 'values')!.enumerable.should.be.true);
19
+ it('should preserve derivative semantics', () => fields.find(_ => _.name === 'values')!.derivatives.should.deep.equal([FirstDerivative, SecondDerivative]));
20
+ it('should preserve generic argument semantics', () => fields.find(_ => _.name === 'values')!.genericArguments.should.deep.equal([String, Number]));
21
+ });
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Fields } from '../Fields';
5
+ import { field } from '../fieldDecorator';
6
+ import { StandardFieldDecoratorContext } from '../StandardFieldDecoratorContext';
7
+
8
+ class FirstDerivative { }
9
+ class SecondDerivative { }
10
+
11
+ class StandardDecoratedType {
12
+ static instanceCount = 0;
13
+
14
+ constructor() {
15
+ StandardDecoratedType.instanceCount++;
16
+ }
17
+ }
18
+
19
+ describe('when invoked as a standard decorator', () => {
20
+ const metadata: Record<PropertyKey, unknown> = Object.create(null) as Record<PropertyKey, unknown>;
21
+ const contextFor = (name: string): StandardFieldDecoratorContext => ({
22
+ kind: 'field',
23
+ metadata,
24
+ name,
25
+ private: false,
26
+ static: false
27
+ });
28
+
29
+ field(Number)(undefined, contextFor('count'));
30
+ field(Date)(undefined, contextFor('createdAt'));
31
+ field(String, true)(undefined, contextFor('names'));
32
+ field(Object, { derivatives: [FirstDerivative, SecondDerivative], genericArguments: [String, Number] })(undefined, contextFor('value'));
33
+ Object.defineProperty(StandardDecoratedType, Symbol.metadata, { value: metadata });
34
+
35
+ const fields = Fields.getFieldsForType(StandardDecoratedType);
36
+
37
+ it('should expose metadata before constructing an instance', () => StandardDecoratedType.instanceCount.should.equal(0));
38
+ it('should register a primitive field', () => fields.find(_ => _.name === 'count')!.type.should.equal(Number));
39
+ it('should register a date field', () => fields.find(_ => _.name === 'createdAt')!.type.should.equal(Date));
40
+ it('should register an array field', () => fields.find(_ => _.name === 'names')!.enumerable.should.be.true);
41
+ it('should register derivatives', () => fields.find(_ => _.name === 'value')!.derivatives.should.deep.equal([FirstDerivative, SecondDerivative]));
42
+ it('should register generic arguments', () => fields.find(_ => _.name === 'value')!.genericArguments.should.deep.equal([String, Number]));
43
+ });
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ class Subject { }
7
+
8
+ describe('when invoked for an unsupported legacy field with a static field', () => {
9
+ const invoke = () => field(String)(Subject, 'value');
10
+
11
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate static fields'));
12
+ });
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ class Subject { }
7
+
8
+ describe('when invoked for an unsupported legacy field with a symbol name', () => {
9
+ const invoke = () => field(String)(Subject.prototype, Symbol('value'));
10
+
11
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate symbol-named fields'));
12
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ describe('when invoked for an unsupported standard field with a private field', () => {
7
+ const invoke = () => field(String)(undefined, {
8
+ kind: 'field',
9
+ metadata: {},
10
+ name: '#value',
11
+ private: true,
12
+ static: false
13
+ });
14
+
15
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate private fields'));
16
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ describe('when invoked for an unsupported standard field with a static field', () => {
7
+ const invoke = () => field(String)(undefined, {
8
+ kind: 'field',
9
+ metadata: {},
10
+ name: 'value',
11
+ private: false,
12
+ static: true
13
+ });
14
+
15
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate static fields'));
16
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ describe('when invoked for an unsupported standard field with a symbol name', () => {
7
+ const invoke = () => field(String)(undefined, {
8
+ kind: 'field',
9
+ metadata: {},
10
+ name: Symbol('value'),
11
+ private: false,
12
+ static: false
13
+ });
14
+
15
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field cannot decorate symbol-named fields'));
16
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '../../fieldDecorator';
5
+
6
+ describe('when invoked for an unsupported standard field without decorator metadata', () => {
7
+ const invoke = () => field(String)(undefined, {
8
+ kind: 'field',
9
+ metadata: undefined,
10
+ name: 'value',
11
+ private: false,
12
+ static: false
13
+ });
14
+
15
+ it('should reject the field', () => invoke.should.throw(TypeError, '@field requires standard decorator metadata support'));
16
+ });
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Fields } from '../Fields';
5
+ import { field } from '../fieldDecorator';
6
+ import { StandardFieldDecoratorContext } from '../StandardFieldDecoratorContext';
7
+
8
+ class LegacyBase { }
9
+ class StandardDerived extends LegacyBase { }
10
+ class StandardBase { }
11
+ class LegacyDerived extends StandardBase { }
12
+
13
+ describe('when legacy and standard metadata are mixed across inheritance', () => {
14
+ const standardDerivedMetadata: Record<PropertyKey, unknown> = Object.create(null) as Record<PropertyKey, unknown>;
15
+ const standardBaseMetadata: Record<PropertyKey, unknown> = Object.create(null) as Record<PropertyKey, unknown>;
16
+ const contextFor = (name: string, metadata: Record<PropertyKey, unknown>): StandardFieldDecoratorContext => ({
17
+ kind: 'field',
18
+ metadata,
19
+ name,
20
+ private: false,
21
+ static: false
22
+ });
23
+
24
+ field(String)(LegacyBase.prototype, 'value');
25
+ field(String)(LegacyBase.prototype, 'legacyBaseValue');
26
+ field(Number)(undefined, contextFor('value', standardDerivedMetadata));
27
+ field(Number)(undefined, contextFor('standardDerivedValue', standardDerivedMetadata));
28
+ Object.defineProperty(StandardDerived, Symbol.metadata, { value: standardDerivedMetadata });
29
+
30
+ field(String)(undefined, contextFor('value', standardBaseMetadata));
31
+ field(String)(undefined, contextFor('standardBaseValue', standardBaseMetadata));
32
+ Object.defineProperty(StandardBase, Symbol.metadata, { value: standardBaseMetadata });
33
+ field(Boolean)(LegacyDerived.prototype, 'value');
34
+ field(Boolean)(LegacyDerived.prototype, 'legacyDerivedValue');
35
+
36
+ const standardDerivedFields = Fields.getFieldsForType(StandardDerived);
37
+ const legacyDerivedFields = Fields.getFieldsForType(LegacyDerived);
38
+
39
+ it('should inherit legacy fields into a standard derived type', () => standardDerivedFields.find(_ => _.name === 'legacyBaseValue')!.type.should.equal(String));
40
+ it('should let a standard field override a legacy base field', () => standardDerivedFields.find(_ => _.name === 'value')!.type.should.equal(Number));
41
+ it('should inherit standard fields into a legacy derived type', () => legacyDerivedFields.find(_ => _.name === 'standardBaseValue')!.type.should.equal(String));
42
+ it('should let a legacy field override a standard base field', () => legacyDerivedFields.find(_ => _.name === 'value')!.type.should.equal(Boolean));
43
+ });
@@ -0,0 +1,34 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Field } from '../Field';
5
+ import { Fields } from '../Fields';
6
+ import { field } from '../fieldDecorator';
7
+
8
+ describe('when standard metadata contains unrelated fields metadata', () => {
9
+ let fields: Field[];
10
+ let metadata: Record<PropertyKey, unknown>;
11
+ let unrelatedMetadata: object;
12
+
13
+ beforeEach(() => {
14
+ class Subject { }
15
+
16
+ metadata = Object.create(null) as Record<PropertyKey, unknown>;
17
+ unrelatedMetadata = { source: 'another library' };
18
+ Reflect.defineMetadata('fields', unrelatedMetadata, metadata);
19
+
20
+ field(String)(undefined, {
21
+ kind: 'field',
22
+ metadata,
23
+ name: 'name',
24
+ private: false,
25
+ static: false
26
+ });
27
+ Object.defineProperty(Subject, Symbol.metadata, { value: metadata });
28
+ fields = Fields.getFieldsForType(Subject);
29
+ });
30
+
31
+ it('should preserve the unrelated fields metadata', () => Reflect.getOwnMetadata('fields', metadata).should.equal(unrelatedMetadata));
32
+ it('should register the decorated field independently', () => fields.map(_ => _.name).should.deep.equal(['name']));
33
+ it('should retain the decorated field type', () => fields[0].type.should.equal(String));
34
+ });