microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.3k stars 12.39k forks source link

SES compatibility for '--emitDecoratorMetadata' #43463

Open rbuckton opened 3 years ago

rbuckton commented 3 years ago

Suggestion

🔍 Search Terms

ses reflect metadata decorator emitDecoratorMetadata experimentalDecorators

✅ Viability Checklist

❗ 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:

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:

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:

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

Related Issues

erights commented 3 years ago

Hi @rbuckton thanks for writing this up!

DanielRosenwasser commented 3 years ago

One of the things we're feeling shy about is adding more compiler configuration for a feature that may change long-term - specifically, we don't like the idea of adding flags for decorators and decorator metadata when the proposal still hasn't reached stage 3. We fear this would cause churn.

An alternative work-around might be to rely on a slightly modified tslib that is SES-compatible and leveraging npm and Yarn's selective dependency resolutions/overrides (i.e. the resolutions field in package.json). This requires approximately the same amount of work for an end-user, avoids problems from older libraries that use older versions of tslib, and means we wouldn't have to prematurely add a flag that we may need to keep around long-term.

erights commented 3 years ago

@rbuckton @DanielRosenwasser any progress on this?