Skip to content

Commit

Permalink
feat: generate types for extern declarations (#5967)
Browse files Browse the repository at this point in the history
Closes #5868 (except the static part, but that's not critical for the use-case)

Compiling a wing project will now generate .d.ts files for the contract between wing and extern files. These .d.ts files are fully self-contained.

The code for this looks similar to existing dtsification code, but there are couple important distinctions that made it feel useful to separate it:
1. The new code only access to type information rather than the original ASTs
2. The new code is intentionally generating types in the same file as needed instead of producing references

Misc:
- No longer emit any files if any errors occur during compiliation
- Internally change extern to use Utf8PathBuff
- Converted some `examples` externs into typescript (I would love to do the rest, but need #3013)

Note that this may be a breaking change: extern types must be consistent. So in one class you refer to an extern as returning a `str` and in another it returns a Json, that will now be a compiler error.

*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
MarkMcCulloh authored Mar 18, 2024
1 parent a361e81 commit 75b30ad
Show file tree
Hide file tree
Showing 34 changed files with 926 additions and 103 deletions.
68 changes: 35 additions & 33 deletions docs/docs/07-examples/10-using-javascript.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,58 @@
---
title: Using JavaScript
title: Using JavaScript/TypeScript
id: using-javascript
keywords: [Wing example]
keywords: [example, javascript, extern, typescript, js, ts]
---

Calling a Javascript function from Wing requires two steps.
Calling a Javascript function from Wing requires two steps.

First, export the function from Javascript.

This examples exports `isValidUrl` from a file named`url_utils.js`:
1. Create a .js file that exports some functions

```js
exports.isValidUrl = function(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
// util.js

exports.isValidUrl = function (url) {
return URL.canParse(url);
};
```

### Preflight function

To call this in preflight code, define the function as an `extern` in a class.

**Note:** Extern functions must be `static.`
In preflight, this file must be a CommonJS module written in Javascript. Inflight, it may be CJS/ESM and either JavaScript or TypeScript.

If you want to use the function outside of the class, be sure to declare it as `pub`.
2. Use the `extern` keyword in a class to expose the function to Wing. Note that this must be `static`. It may also be `inflight`

```ts
class JsExample {
// preflight static
pub extern "./url_utils.js" static isValidUrl(url: str): bool;
```ts
class JsExample {
pub extern "./util.js" static isValidUrl(url: str): bool;
}

assert(JsExample.isValidUrl("http://www.google.com"));
assert(!JsExample.isValidUrl("X?Y"));
```

### Inflight
### Type-safe `extern`

To call the function inflight, add the `inflight` modifier.
Running `wing compile` will generate a corresponding `.d.ts` file for each `extern`. This file can be imported into the extern file itself to ensure the extern is type-safe. Either your IDE or a separate usage of the TypeScript compiler can provide type-checking.

```ts
class JsExample {
// inflight static method
extern "./url_utils.js" static inflight isValidUrl(url: str): bool;
}
// util.ts
import type extern from "./util.extern";

test "main" {
assert(JsExample.isValidUrl("http://www.google.com"));
assert(!JsExample.isValidUrl("X?Y"));
}
export const isValidUrl: extern["isValidUrl"] = (url) => {
// url is known to be a string and that we must return a boolean
return URL.canParse(url);
};
```

The .d.ts file can also be used in JavaScript via JSDoc comments and can even be applied at a module export level.

```js
// util.js
/** @type {import("./util.extern").default} */
module.exports = {
isValidUrl: (url) => {
return URL.canParse(url);
},
};
```

Coming Soon: The ability to use resources inside an `inflight extern`. See [this issue](https://github.com/winglang/wing/issues/76) for more information.
3 changes: 3 additions & 0 deletions examples/tests/sdk_tests/function/logging.extern.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default interface extern {
logging: () => Promise<void>,
}
10 changes: 10 additions & 0 deletions examples/tests/sdk_tests/service/http-server.extern.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default interface extern {
createServer: (body: string) => Promise<IHttpServer$Inflight>,
}
export interface Address {
readonly port: number;
}
export interface IHttpServer$Inflight {
readonly address: () => Promise<Address>;
readonly close: () => Promise<void>;
}
3 changes: 3 additions & 0 deletions examples/tests/sdk_tests/util/util.extern.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default interface extern {
platform: () => Promise<string>,
}
3 changes: 3 additions & 0 deletions examples/tests/sdk_tests/util/uuidv4-helper.extern.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default interface extern {
validateUUIDv4: (uuidv4: string) => Promise<boolean>,
}
8 changes: 2 additions & 6 deletions examples/tests/sdk_tests/util/uuidv4.test.w
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
bring util;

class JSHelper {
extern "./uuidv4-helper.js" pub static validateUUIDv4(uuidv4: str): bool;
}

let data = util.uuidv4();
assert(JSHelper.validateUUIDv4(data) == true);
let preflightData = util.uuidv4();

class JSHelperInflight {
extern "./uuidv4-helper.js" pub static inflight validateUUIDv4(uuidv4: str): bool;
Expand All @@ -14,4 +9,5 @@ class JSHelperInflight {
test "inflight uuidv4" {
let data = util.uuidv4();
assert(JSHelperInflight.validateUUIDv4(data) == true);
assert(JSHelperInflight.validateUUIDv4(preflightData) == true);
}
2 changes: 1 addition & 1 deletion examples/tests/valid/capture_tokens.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class MyResource {
api: cloud.Api;
url: str;

extern "./url_utils.js" pub static inflight isValidUrl(url: str): bool;
extern "./url_utils.ts" pub static inflight isValidUrl(url: str): bool;

new() {
this.api = new cloud.Api();
Expand Down
4 changes: 4 additions & 0 deletions examples/tests/valid/dynamo.extern.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default interface extern {
_getItem: (tableName: string, key: Readonly<any>) => Promise<Readonly<any>>,
_putItem: (tableName: string, item: Readonly<any>) => Promise<void>,
}
3 changes: 2 additions & 1 deletion examples/tests/valid/dynamo.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ class DynamoTable {
}
}

extern "./dynamo.js" static inflight _putItem(tableName: str, item: Json): void;
extern "./dynamo.ts" static inflight _getItem(tableName: str, key: Json): Json;
extern "./dynamo.ts" static inflight _putItem(tableName: str, item: Json): void;

pub inflight putItem(item: Map<Attribute>) {
let json = this._itemToJson(item);
Expand Down
17 changes: 11 additions & 6 deletions examples/tests/valid/dynamo.js → examples/tests/valid/dynamo.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
const { DynamoDBClient, PutItemCommand, GetItemCommand } = require("@aws-sdk/client-dynamodb");
import type extern from "./dynamo.extern";
import {
DynamoDBClient,
PutItemCommand,
GetItemCommand,
} from "@aws-sdk/client-dynamodb";

const client = new DynamoDBClient({});

export async function _putItem(tableName, item) {
export const _putItem: extern["_putItem"] = async (tableName, item) => {
const command = new PutItemCommand({
TableName: tableName,
Item: item,
Expand All @@ -11,15 +16,15 @@ export async function _putItem(tableName, item) {
const response = await client.send(command);
console.log(response);
return;
}
};

export async function _getItem(tableName, key) {
export const _getItem: extern["_getItem"] = async (tableName, key) => {
const command = new GetItemCommand({
TableName: tableName,
Key: key
Key: key,
});

const response = await client.send(command);
console.log(response);
return response;
}
};
4 changes: 2 additions & 2 deletions examples/tests/valid/dynamo_awscdk.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ class DynamoTable {
}
}

extern "./dynamo.js" static inflight _putItem(tableName: str, item: Json): void;
extern "./dynamo.ts" static inflight _putItem(tableName: str, item: Json): void;
pub inflight putItem(item: Map<Attribute>) {
let json = this._itemToJson(item);
DynamoTable._putItem(this.tableName, json);
}

extern "./dynamo.js" static inflight _getItem(tableName: str, key: Json): Json;
extern "./dynamo.ts" static inflight _getItem(tableName: str, key: Json): Json;
pub inflight getItem(key: Map<Attribute>): Json {
let json = this._itemToJson(key);
return DynamoTable._getItem(this.tableName, json);
Expand Down
4 changes: 4 additions & 0 deletions examples/tests/valid/extern_implementation.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Foo {
extern "./external_js.js" static inflight getUuid(): str;
extern "./external_js.js" static inflight getData(): str;
extern "./external_js.js" pub static inflight print(msg: str): void;
extern "./external_js.js" pub static preflightBucket(bucket: cloud.Bucket, id: str): Json;

pub inflight call() {
assert(Foo.regexInflight("[a-z]+-\\d+", "abc-123"));
Expand All @@ -20,6 +21,9 @@ assert(Foo.getGreeting("Wingding") == "Hello, Wingding!");

let f = new Foo();

let bucket = new cloud.Bucket() as "my-bucket";
let result = Foo.preflightBucket(bucket, "my-bucket");

test "call" {
f.call();
}
Expand Down
Loading

0 comments on commit 75b30ad

Please sign in to comment.