microsoft / microsoft/TypeScript
SES compatibility for '--emitDecoratorMetadata'
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 111k
- Forks
- 14.3k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 132
Description
Suggestion
🔍 Search Terms
ses reflect metadata decorator emitDecoratorMetadata experimentalDecorators
✅ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
❗ Problem
SES (Secure ECMAScript) is an initiative to create a locked-down and sandboxed JavaScript execution environment. One of the tenets of SES is that "primordials" (built-in/native ECMAScript global objects, prototypes, functions, etc.) should be immutable and be locked down to a specific set of APIs. This set of APIs includes those specified in the ECMAScript standard and other "blessed" APIs depending on the runtime environment.
A longstanding package used by many in the community is reflect-metadata, which is used with TypeScript's --emitDecoratorMetadata compiler flag and the __metadata helper:
Example:
tsc example.ts \
--target esnext \
--experimentalDecorators \
--emitDecoratorMetadata
example.ts
declare const dec: any;
@dec
class C {
constructor(x: int) {
}
}
example.js
var __decorate = ...
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
let C = class C {
constructor(x) {
}
};
C = __decorate([
dec,
__metadata("design:paramtypes", [Number])
], C);
The issue is that the __metadata helper function explicitly relies on an early draft of a proposal that has not yet been brought to committee (as it depends on the decorators proposal, which has not yet reached Stage 3). Unfortunately, reflect-metadata mutates the global Reflect object to shim its API, which violates SES. This was recently brought up in https://github.com/endojs/endo/issues/612 and brought to my attention by @erights.
Alternatives to reflect-metadata exist that do not rely on global mutation, such as https://esfx.js.org/esfx/api/metadata.html. However, they cannot be easily used with TypeScript's --emitDecoratorMetadata since our helper is hardcoded to check for the specific global. While it is possible to overwrite the helper locally by providing your own __metadata helper, this is behavior is intentionally undocumented and is also burdensome on developers.
⭐ Suggestion
I am proposing that we add two new compiler options to control how we emit decorator metadata:
--metadataDecorator <string>— Accepts a validEntityNamethat should be used as the decorator in place of__metadata- This option can only be specified if both
--experimentalDecoratorsand--emitDecoratorMetadataare set.
- This option can only be specified if both
--metadataDecoratorImportSource <string>— A module specifier used to import the provided metadata decorator.- This option can only be specified if
--experimentalDecorators,--emitDecoratorMetadata, and--metadataDecoratorare set.
- This option can only be specified if
NOTE: These two options are somewhat analogous to our existing --jsxFactory and --jsxImportSource options.
--metadataDecorator option
If the --metadataDecorator option is set without specifying --metadataDecoratorImportSource, then the following occurs:
- During command line validation we verify that the provided option is a valid
EntityName. - During check we verify that the provided name can be resolved to a value that can be called as a decorator appropriate to the element on which it would appear as a result of
--emitDecoratorMetadata. - During emit, we would emit the provided
EntityNamein place of__metadata.
NOTE: This scenario assumes that the provided metadata decorator exists as a global, or is manually imported into any module.
Example:
tsc example.ts \
--target esnext \
--experimentalDecorators \
--emitDecoratorMetadata \
--metadataDecorator myCustomMetadata
example.ts
declare const dec: any;
@dec
class C {
constructor(x: number) {}
}
example.js
var __decorate = ...
let C = class C {
constructor(x) {
}
}
C = __decorate([
dec,
myCustomMetadata("design:paramtypes", [Number])
], C);
--metadataDecoratorImportSource option
If the --metadataDecoratorImportSource option is set along with --metadataDecorator, then the following occurs:
- During command line validation we verify that the provided option is a valid
EntityName. - During check we verify that the provided import source can be resolved as a module.
- During check we verify that the first identifier in the provided
EntityNameis an export of the import source. - During check we verify that the remainder of the provided
EntityNamecan be resolved to a value relative to the imported binding above, that can be called as a decorator appropriate to the element on which it would appear as a result of--emitDecoratorMetadata. - During emit, we would emit a synthetic
importstatement for the import source. - During emit, we would emit the provided
EntityNamein place of__metadata.
Example:
tsc example.ts \
--target esnext \
--experimentalDecorators \
--emitDecoratorMetadata \
--metadataDecorator myCustomMetadata \
--metadataDecoratorImportSource my-custom-metadata
example.ts
declare const dec: any;
@dec
export export class C {
constructor(x: number) {}
}
example.js
var __decorate = ...
import { myCustomMetadata } from "my-custom-metadata"
let C = class C {
constructor(x) {
}
}
C = __decorate([
dec,
myCustomMetadata("design:paramtypes", [Number])
], C);
export { C }
Caveats
Specifying --metadataDecoratorImportSource implies that the custom metadata decorator can only be reached from within a module, therefore any source file that expects metadata must have at least one import or export declaration, or the --isolatedModules option must be set. If a file is not determined to be a module, we will fall back to the existing __metadata helper (which is essentially a no-op if the Reflect.metadata function does not exist at runtime).
Out-of-scope
- Further changes to type metadata emit related to
--emitDecoratorMetadataare out of scope. This is only intended to address an ecosystem concern brought to us by those involved with SES. - Changes to decorator emit related to the ongoing work in the Decorators proposal are out of scope. We do not intend to make any substantial changes to our decorator support until that proposal has reached Stage 3.
Related Issues
- #43450 — ES5 class rewriting incompatible with frozen intrinsics
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the existing --jsxFactory and --jsxImportSource option handling, then trace the compiler's command-line validation, checking, and emit paths described in the proposal. Done means supporting the two metadata decorator options, including validation, resolution, synthetic imports, and fallback behavior for non-module files.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100