Skip to content

Commit

Permalink
fix!: refactor platforms to expose global class factory (#6099)
Browse files Browse the repository at this point in the history
[Wing Platforms](https://www.winglang.io/docs/concepts/platforms) allow users to substitute in their own class definitions whenever a class is used in Wing code. To generate classes in JavaScript that extend a class defined by a Wing platform, the Wing compiler needs to generate code shaped something like this:

```
class Child extends getParentType() { ... }
```

where `getParentType()` is just some expression that obtains the parent class type from the Wing platform system. Surprisingly, JavaScript allows for putting expressions like function calls here (which is cool).

The compiler currently generates some code that no-ops, so classes defined by Wing platforms are never injected. We would like to instead generate code that accesses the current Wing platform, but it isn't possible because the only ways to obtain the platform are through the `App`.

To make a fix for this possible, this PR refactors our platform-related code so that we're now exposing the dependency injection factory through the [JavaScript global object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis). Wing generated code can obtain the target-specific version of any class it needs (if an implementation is available) by calling `globalThis.$ClassFactory.new(fqn, scope, id, ...args)`.

The unfortunate drawback of this change is that it pollutes the global namespace, and it means only a single Wing platform can be used for synthesizing apps in a given Node process. (If you want to compile apps to two different platforms, you'll likely need multiple node processes).

It's possible we could avoid this by making Wing-generated JavaScript exports parameterized -- however this may negatively affect the Wing TypeScript experience by introducing more steps for importing Wing libraries:

```
import { main, cloud } from "@wingcloud/framework";
import initPostgres from "@winglibs/postgres";

main((root) => {
  const postgres = initPostgres(root);
  new cloud.Bucket(root, "Bucket");
  new postgres.Database(root, "Database");
});
```

But such an alternative could cause problems if we want to eventually [make Wing libraries into JSII libraries](#6655).

BREAKING CHANGE: Several platform-related APIs have been changed. `new()`, `newAbstract()`, `typeForFqn()`, and `tryNew()` methods have been removed from `core.App`. Instead, you can obtain a `ClassFactory` through `ClassFactory.of(scope)`, which has methods like `new()`, `tryNewInstance()` and `resolveType()`. `AppProps` now does not expect `newInstanceOverrides` anymore, and instead expects a `classFactory`.

## Checklist

- [ ] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted)
- [ ] Description explains motivation and solution
- [ ] Tests added (always)
- [ ] Docs updated (only required for features)
- [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing

*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
Chriscbr committed Jul 9, 2024
1 parent 8c92b10 commit a734f03
Show file tree
Hide file tree
Showing 352 changed files with 1,944 additions and 1,947 deletions.
7 changes: 7 additions & 0 deletions apps/wing/src/commands/test/test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,17 @@ describe("wing test (custom platform)", () => {
newApp(appProps) {
return new tfaws.App(appProps);
}
newInstance(fqn, scope, id, ...args) {
if (fqn === "@winglang/sdk.std.TestRunner") {
return new tfaws.TestRunner(scope, id, ...args);
}
}
}
module.exports = { Platform }`
);

// entrypoint array is empty because foo.test.w is inferred as the only entrypoint
await wingTest([], {
clean: true,
platform: ["./custom-platform.js"],
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/04-standard-library/aws/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1747,6 +1747,8 @@ Add a Lambda layer to the function.

- *Type:* str

The ARN of the layer.

---

#### Properties <a name="Properties" id="Properties"></a>
Expand Down

This file was deleted.

16 changes: 0 additions & 16 deletions examples/tests/doc_examples/valid/01-cli.md_example_1/main.w

This file was deleted.

6 changes: 0 additions & 6 deletions examples/tests/doc_examples/valid/01-cli.md_example_2/main.w

This file was deleted.

26 changes: 0 additions & 26 deletions examples/tests/doc_examples/valid/03-platforms.md_example_1/main.w

This file was deleted.

This file was deleted.

8 changes: 8 additions & 0 deletions examples/tests/valid/platforms/example.main.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
bring cloud;

let t = new cloud.Topic();
let q = new cloud.Queue();

q.setConsumer(inflight (msg: str) => {
t.publish("msg");
});
2 changes: 1 addition & 1 deletion examples/tests/valid/platforms/main.w
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ let q = new cloud.Queue();

q.setConsumer(inflight (msg: str) => {
b.put("file.txt", msg);
});
});
5 changes: 5 additions & 0 deletions libs/@wingcloud/framework/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ClassFactory } from "@winglang/sdk/lib/core";

declare global {
var $ClassFactory: ClassFactory;
}
4 changes: 2 additions & 2 deletions libs/@wingcloud/framework/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ This is a Wing app and must be run through the Wing CLI (npm install -f winglang
const entrypointDir = process.env.WING_SOURCE_DIR!;
const rootId = process.env.WING_ROOT_ID;

const $PlatformManager = new platform.PlatformManager({ platformPaths });
const app = $PlatformManager.createApp({
const platformManager = new platform.PlatformManager({ platformPaths });
const app = platformManager.createApp({
outdir,
name,
rootConstruct,
Expand Down
103 changes: 12 additions & 91 deletions libs/awscdk/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,11 @@ import { mkdirSync } from "fs";
import { join } from "path";
import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import { Construct } from "constructs";
import stringify from "safe-stable-stringify";
import { Api } from "./api";
import { Bucket } from "./bucket";
import { Counter } from "./counter";
import { Endpoint } from "./endpoint";
import { Function } from "./function";
import { OnDeploy } from "./on-deploy";
import { Queue } from "./queue";
import { Schedule } from "./schedule";
import { Secret } from "./secret";
import { TestRunner } from "./test-runner";
import { CdkTokens } from "./tokens";
import { Topic } from "./topic";
import { Website } from "./website";
import { cloud } from "@winglang/sdk";

const {
API_FQN,
BUCKET_FQN,
COUNTER_FQN,
ENDPOINT_FQN,
FUNCTION_FQN,
ON_DEPLOY_FQN,
QUEUE_FQN,
SECRET_FQN,
TOPIC_FQN,
SCHEDULE_FQN,
WEBSITE_FQN,
} = cloud;

import { core, std } from "@winglang/sdk";

import { core } from "@winglang/sdk";
import { Util } from "@winglang/sdk/lib/util";
import { registerTokenResolver } from "@winglang/sdk/lib/core/tokens";

Expand All @@ -54,7 +27,11 @@ export interface CdkAppProps extends core.AppProps {
*
* @default - creates a standard `cdk.Stack`
*/
readonly stackFactory?: (app: cdk.App, stackName: string, props?: cdk.StackProps) => cdk.Stack;
readonly stackFactory?: (
app: cdk.App,
stackName: string,
props?: cdk.StackProps
) => cdk.Stack;
}

/**
Expand All @@ -76,7 +53,8 @@ export class App extends core.App {
private synthedOutput: string | undefined;

constructor(props: CdkAppProps) {
const account = process.env.CDK_AWS_ACCOUNT ?? process.env.CDK_DEFAULT_ACCOUNT;
const account =
process.env.CDK_AWS_ACCOUNT ?? process.env.CDK_DEFAULT_ACCOUNT;
const region = process.env.CDK_AWS_REGION ?? process.env.CDK_DEFAULT_REGION;

let stackName = props.stackName ?? process.env.CDK_STACK_NAME;
Expand All @@ -98,30 +76,14 @@ export class App extends core.App {
const cdkApp = new cdk.App({ outdir: cdkOutdir });

const createStack =
props.stackFactory ?? ((app, stackName, props) => new cdk.Stack(app, stackName, props));
props.stackFactory ??
((app, stackName, props) => new cdk.Stack(app, stackName, props));

const cdkStack = createStack(cdkApp, stackName, {
env: { account, region }
env: { account, region },
});

super(cdkStack, props.rootId ?? "Default", props);

// HACK: monkey patch the `new` method on the cdk app (which is the root of the tree) so that
// we can intercept the creation of resources and replace them with our own.
(cdkApp as any).new = (
fqn: string,
ctor: any,
scope: Construct,
id: string,
...args: any[]
) => this.new(fqn, ctor, scope, id, ...args);

(cdkApp as any).newAbstract = (
fqn: string,
scope: Construct,
id: string,
...args: any[]
) => this.newAbstract(fqn, scope, id, ...args);

this.outdir = outdir;
this.cdkApp = cdkApp;
Expand Down Expand Up @@ -167,47 +129,6 @@ export class App extends core.App {
return this.synthedOutput;
}

protected typeForFqn(fqn: string): any {
switch (fqn) {
case API_FQN:
return Api;

case FUNCTION_FQN:
return Function;

case BUCKET_FQN:
return Bucket;

case COUNTER_FQN:
return Counter;

case SCHEDULE_FQN:
return Schedule;

case QUEUE_FQN:
return Queue;

case TOPIC_FQN:
return Topic;

case std.TEST_RUNNER_FQN:
return TestRunner;

case SECRET_FQN:
return Secret;

case ON_DEPLOY_FQN:
return OnDeploy;

case WEBSITE_FQN:
return Website;

case ENDPOINT_FQN:
return Endpoint;
}
return undefined;
}

/**
* The AWS account ID of the App
*/
Expand Down
87 changes: 85 additions & 2 deletions libs/awscdk/src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
import { App } from "./app";
import { platform } from "@winglang/sdk";
import { platform, std } from "@winglang/sdk";
import { Api } from "./api";
import { Bucket } from "./bucket";
import { Counter } from "./counter";
import { Endpoint } from "./endpoint";
import { Function } from "./function";
import { OnDeploy } from "./on-deploy";
import { Queue } from "./queue";
import { Schedule } from "./schedule";
import { Secret } from "./secret";
import { TestRunner } from "./test-runner";
import { Topic } from "./topic";
import { Website } from "./website";
import { cloud } from "@winglang/sdk";
import { Construct } from "constructs";

const {
API_FQN,
BUCKET_FQN,
COUNTER_FQN,
ENDPOINT_FQN,
FUNCTION_FQN,
ON_DEPLOY_FQN,
QUEUE_FQN,
SECRET_FQN,
TOPIC_FQN,
SCHEDULE_FQN,
WEBSITE_FQN,
} = cloud;

/**
* AWS CDK Platform
Expand All @@ -8,7 +36,62 @@ export class Platform implements platform.IPlatform {
/** Platform model */
public readonly target = "awscdk";

public newApp?(appProps: any): any {
public newApp(appProps: any): any {
return new App(appProps);
}

public newInstance(
type: string,
scope: Construct,
id: string,
...args: any[]
): any {
const Type = this.resolveType(type);
if (Type) {
return new Type(scope, id, ...args);
}

return undefined;
}

public resolveType(fqn: string): any {
switch (fqn) {
case API_FQN:
return Api;

case FUNCTION_FQN:
return Function;

case BUCKET_FQN:
return Bucket;

case COUNTER_FQN:
return Counter;

case SCHEDULE_FQN:
return Schedule;

case QUEUE_FQN:
return Queue;

case TOPIC_FQN:
return Topic;

case std.TEST_RUNNER_FQN:
return TestRunner;

case SECRET_FQN:
return Secret;

case ON_DEPLOY_FQN:
return OnDeploy;

case WEBSITE_FQN:
return Website;

case ENDPOINT_FQN:
return Endpoint;
}
return undefined;
}
}
Loading

0 comments on commit a734f03

Please sign in to comment.