diff --git a/docs/docs/07-examples/10-using-javascript.md b/docs/docs/07-examples/10-using-javascript.md index 843ee116998..4b7b6640e63 100644 --- a/docs/docs/07-examples/10-using-javascript.md +++ b/docs/docs/07-examples/10-using-javascript.md @@ -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. diff --git a/examples/tests/sdk_tests/function/logging.extern.d.ts b/examples/tests/sdk_tests/function/logging.extern.d.ts new file mode 100644 index 00000000000..5bff5738bf1 --- /dev/null +++ b/examples/tests/sdk_tests/function/logging.extern.d.ts @@ -0,0 +1,3 @@ +export default interface extern { + logging: () => Promise, +} diff --git a/examples/tests/sdk_tests/service/http-server.extern.d.ts b/examples/tests/sdk_tests/service/http-server.extern.d.ts new file mode 100644 index 00000000000..0c67ddf5937 --- /dev/null +++ b/examples/tests/sdk_tests/service/http-server.extern.d.ts @@ -0,0 +1,10 @@ +export default interface extern { + createServer: (body: string) => Promise, +} +export interface Address { + readonly port: number; +} +export interface IHttpServer$Inflight { + readonly address: () => Promise
; + readonly close: () => Promise; +} \ No newline at end of file diff --git a/examples/tests/sdk_tests/util/util.extern.d.ts b/examples/tests/sdk_tests/util/util.extern.d.ts new file mode 100644 index 00000000000..06d5b1d6059 --- /dev/null +++ b/examples/tests/sdk_tests/util/util.extern.d.ts @@ -0,0 +1,3 @@ +export default interface extern { + platform: () => Promise, +} diff --git a/examples/tests/sdk_tests/util/uuidv4-helper.extern.d.ts b/examples/tests/sdk_tests/util/uuidv4-helper.extern.d.ts new file mode 100644 index 00000000000..c92962f9d24 --- /dev/null +++ b/examples/tests/sdk_tests/util/uuidv4-helper.extern.d.ts @@ -0,0 +1,3 @@ +export default interface extern { + validateUUIDv4: (uuidv4: string) => Promise, +} diff --git a/examples/tests/sdk_tests/util/uuidv4.test.w b/examples/tests/sdk_tests/util/uuidv4.test.w index da958280ad6..9a4f3898dc1 100644 --- a/examples/tests/sdk_tests/util/uuidv4.test.w +++ b/examples/tests/sdk_tests/util/uuidv4.test.w @@ -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; @@ -14,4 +9,5 @@ class JSHelperInflight { test "inflight uuidv4" { let data = util.uuidv4(); assert(JSHelperInflight.validateUUIDv4(data) == true); + assert(JSHelperInflight.validateUUIDv4(preflightData) == true); } \ No newline at end of file diff --git a/examples/tests/valid/capture_tokens.test.w b/examples/tests/valid/capture_tokens.test.w index 5212f6e66d6..a0ec28807a8 100644 --- a/examples/tests/valid/capture_tokens.test.w +++ b/examples/tests/valid/capture_tokens.test.w @@ -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(); diff --git a/examples/tests/valid/dynamo.extern.d.ts b/examples/tests/valid/dynamo.extern.d.ts new file mode 100644 index 00000000000..e0fda52f3c7 --- /dev/null +++ b/examples/tests/valid/dynamo.extern.d.ts @@ -0,0 +1,4 @@ +export default interface extern { + _getItem: (tableName: string, key: Readonly) => Promise>, + _putItem: (tableName: string, item: Readonly) => Promise, +} diff --git a/examples/tests/valid/dynamo.test.w b/examples/tests/valid/dynamo.test.w index c95d3276f75..d1a8817b6d8 100644 --- a/examples/tests/valid/dynamo.test.w +++ b/examples/tests/valid/dynamo.test.w @@ -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) { let json = this._itemToJson(item); diff --git a/examples/tests/valid/dynamo.js b/examples/tests/valid/dynamo.ts similarity index 55% rename from examples/tests/valid/dynamo.js rename to examples/tests/valid/dynamo.ts index 04c09c8c2c1..9517414f156 100644 --- a/examples/tests/valid/dynamo.js +++ b/examples/tests/valid/dynamo.ts @@ -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, @@ -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; -} \ No newline at end of file +}; diff --git a/examples/tests/valid/dynamo_awscdk.test.w b/examples/tests/valid/dynamo_awscdk.test.w index 3e916121efc..ad1682860ea 100644 --- a/examples/tests/valid/dynamo_awscdk.test.w +++ b/examples/tests/valid/dynamo_awscdk.test.w @@ -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) { 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): Json { let json = this._itemToJson(key); return DynamoTable._getItem(this.tableName, json); diff --git a/examples/tests/valid/extern_implementation.test.w b/examples/tests/valid/extern_implementation.test.w index 98425d6b80b..10d7bdfa8a3 100644 --- a/examples/tests/valid/extern_implementation.test.w +++ b/examples/tests/valid/extern_implementation.test.w @@ -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")); @@ -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(); } diff --git a/examples/tests/valid/external_js.extern.d.ts b/examples/tests/valid/external_js.extern.d.ts new file mode 100644 index 00000000000..079bebe0c07 --- /dev/null +++ b/examples/tests/valid/external_js.extern.d.ts @@ -0,0 +1,244 @@ +export default interface extern { + getData: () => Promise, + getGreeting: (name: string) => string, + getUuid: () => Promise, + preflightBucket: (bucket: Bucket, id: string) => Readonly, + print: (msg: string) => Promise, + regexInflight: (pattern: string, text: string) => Promise, +} +/** Trait marker for classes that can be depended upon. +The presence of this interface indicates that an object has +an `IDependable` implementation. + +This interface can be used to take an (ordering) dependency on a set of +constructs. An ordering dependency implies that the resources represented by +those constructs are deployed before the resources depending ON them are +deployed. */ +export interface IDependable { +} +/** Options for `construct.addMetadata()`. */ +export interface MetadataOptions { + /** Include stack trace with metadata entry. */ + readonly stackTrace?: (boolean) | undefined; + /** A JavaScript function to begin tracing from. + This option is ignored unless `stackTrace` is `true`. */ + readonly traceFromFunction?: (any) | undefined; +} +/** Implement this interface in order for the construct to be able to validate itself. */ +export interface IValidation { + /** Validate the current construct. + This method can be implemented by derived constructs in order to perform + validation logic. It is called on all constructs before synthesis. + @returns An array of validation error messages, or an empty array if there the construct is valid. */ + readonly validate: () => (readonly (string)[]); +} +/** In what order to return constructs. */ +export enum ConstructOrder { + PREORDER = 0, + POSTORDER = 1, +} +/** An entry in the construct metadata table. */ +export interface MetadataEntry { + /** The data. */ + readonly data?: any; + /** Stack trace at the point of adding the metadata. + Only available if `addMetadata()` is called with `stackTrace: true`. */ + readonly trace?: ((readonly (string)[])) | undefined; + /** The metadata entry type. */ + readonly type: string; +} +/** Represents the construct node in the scope tree. */ +export class Node { + /** Add an ordering dependency on another construct. + An `IDependable` */ + readonly addDependency: (deps?: ((readonly (IDependable)[])) | undefined) => void; + /** Adds a metadata entry to this construct. + Entries are arbitrary values and will also include a stack trace to allow tracing back to + the code location for when the entry was added. It can be used, for example, to include source + mapping in CloudFormation templates to improve diagnostics. */ + readonly addMetadata: (type: string, data?: any, options?: (MetadataOptions) | undefined) => void; + /** Adds a validation to this construct. + When `node.validate()` is called, the `validate()` method will be called on + all validations and all errors will be returned. */ + readonly addValidation: (validation: IValidation) => void; + /** Returns an opaque tree-unique address for this construct. + Addresses are 42 characters hexadecimal strings. They begin with "c8" + followed by 40 lowercase hexadecimal characters (0-9a-f). + + Addresses are calculated using a SHA-1 of the components of the construct + path. + + To enable refactorings of construct trees, constructs with the ID `Default` + will be excluded from the calculation. In those cases constructs in the + same tree may have the same addreess. + c83a2846e506bcc5f10682b564084bca2d275709ee */ + readonly addr: string; + /** All direct children of this construct. */ + readonly children: (readonly (IConstruct)[]); + /** Returns the child construct that has the id `Default` or `Resource"`. + This is usually the construct that provides the bulk of the underlying functionality. + Useful for modifications of the underlying construct that are not available at the higher levels. + Override the defaultChild property. + + This should only be used in the cases where the correct + default child is not named 'Resource' or 'Default' as it + should be. + + If you set this to undefined, the default behavior of finding + the child named 'Resource' or 'Default' will be used. + @returns a construct or undefined if there is no default child */ + defaultChild?: (IConstruct) | undefined; + /** Return all dependencies registered on this node (non-recursive). */ + readonly dependencies: (readonly (IConstruct)[]); + /** Return this construct and all of its children in the given order. */ + readonly findAll: (order?: (ConstructOrder) | undefined) => (readonly (IConstruct)[]); + /** Return a direct child by id. + Throws an error if the child is not found. + @returns Child with the given id. */ + readonly findChild: (id: string) => IConstruct; + /** Retrieves the all context of a node from tree context. + Context is usually initialized at the root, but can be overridden at any point in the tree. + @returns The context object or an empty object if there is discovered context */ + readonly getAllContext: (defaults?: (Readonly) | undefined) => any; + /** Retrieves a value from tree context if present. Otherwise, would throw an error. + Context is usually initialized at the root, but can be overridden at any point in the tree. + @returns The context value or throws error if there is no context value for this key */ + readonly getContext: (key: string) => any; + /** The id of this construct within the current scope. + This is a scope-unique id. To obtain an app-unique id for this construct, use `addr`. */ + readonly id: string; + /** Locks this construct from allowing more children to be added. + After this + call, no more children can be added to this construct or to any children. */ + readonly lock: () => void; + /** Returns true if this construct or the scopes in which it is defined are locked. */ + readonly locked: boolean; + /** An immutable array of metadata objects associated with this construct. + This can be used, for example, to implement support for deprecation notices, source mapping, etc. */ + readonly metadata: (readonly (MetadataEntry)[]); + /** The full, absolute path of this construct in the tree. + Components are separated by '/'. */ + readonly path: string; + /** Returns the root of the construct tree. + @returns The root of the construct tree. */ + readonly root: IConstruct; + /** Returns the scope in which this construct is defined. + The value is `undefined` at the root of the construct scope tree. */ + readonly scope?: (IConstruct) | undefined; + /** All parent scopes of this construct. + @returns a list of parent scopes. The last element in the list will always + be the current construct and the first element will be the root of the + tree. */ + readonly scopes: (readonly (IConstruct)[]); + /** This can be used to set contextual values. + Context must be set before any children are added, since children may consult context info during construction. + If the key already exists, it will be overridden. */ + readonly setContext: (key: string, value?: any) => void; + /** Return a direct child by id, or undefined. + @returns the child if found, or undefined */ + readonly tryFindChild: (id: string) => (IConstruct) | undefined; + /** Retrieves a value from tree context. + Context is usually initialized at the root, but can be overridden at any point in the tree. + @returns The context value or `undefined` if there is no context value for this key. */ + readonly tryGetContext: (key: string) => any; + /** Remove the child with the given name, if present. + @returns Whether a child with the given name was deleted. */ + readonly tryRemoveChild: (childName: string) => boolean; + /** Validates this construct. + Invokes the `validate()` method on all validations added through + `addValidation()`. + @returns an array of validation error messages associated with this + construct. */ + readonly validate: () => (readonly (string)[]); +} +/** Represents a construct. */ +export interface IConstruct extends IDependable { + /** The tree node. */ + readonly node: Node; +} +/** Represents the building block of the construct graph. +All constructs besides the root construct must be created within the scope of +another construct. */ +export class Construct implements IConstruct { + /** The tree node. */ + readonly node: Node; + /** Returns a string representation of this construct. */ + readonly toString: () => string; +} +/** Data that can be lifted into inflight. */ +export interface ILiftable { +} +/** A resource that can run inflight code. */ +export interface IInflightHost extends IResource { + /** Adds an environment variable to the host. */ + readonly addEnvironment: (name: string, value: string) => void; +} +/** A liftable object that needs to be registered on the host as part of the lifting process. +This is generally used so the host can set up permissions +to access the lifted object inflight. */ +export interface IHostedLiftable extends ILiftable { + /** A hook called by the Wing compiler once for each inflight host that needs to use this object inflight. + The list of requested inflight methods + needed by the inflight host are given by `ops`. + + This method is commonly used for adding permissions, environment variables, or + other capabilities to the inflight host. */ + readonly onLift: (host: IInflightHost, ops: (readonly (string)[])) => void; +} +/** Abstract interface for `Resource`. */ +export interface IResource extends IConstruct, IHostedLiftable { + readonly node: Node; + readonly onLift: (host: IInflightHost, ops: (readonly (string)[])) => void; +} +/** Shared behavior between all Wing SDK resources. */ +export class Resource extends Construct implements IResource { + /** A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight. + You can override this method to perform additional logic like granting + IAM permissions to the host based on what methods are being called. But + you must call `super.bind(host, ops)` to ensure that the resource is + actually bound. */ + readonly onLift: (host: IInflightHost, ops: (readonly (string)[])) => void; +} +/** Code that runs at runtime and implements your application's behavior. +For example, handling API requests, processing queue messages, etc. +Inflight code can be executed on various compute platforms in the cloud, +such as function services (such as AWS Lambda or Azure Functions), +containers (such as ECS or Kubernetes), VMs or even physical servers. + +This data represents the code together with the bindings to preflight data required to run. */ +export interface IInflight extends IHostedLiftable { + readonly onLift: (host: IInflightHost, ops: (readonly (string)[])) => void; +} +/** A resource with an inflight "handle" method that can be passed to the bucket events. */ +export interface IBucketEventHandler extends IInflight { + readonly onLift: (host: IInflightHost, ops: (readonly (string)[])) => void; +} +/** `onCreate` event options. */ +export interface BucketOnCreateOptions { +} +/** `onDelete` event options. */ +export interface BucketOnDeleteOptions { +} +/** `onEvent` options. */ +export interface BucketOnEventOptions { +} +/** `onUpdate` event options. */ +export interface BucketOnUpdateOptions { +} +/** A cloud object store. */ +export class Bucket extends Resource { + /** Add a file to the bucket from system folder. */ + readonly addFile: (key: string, path: string, encoding?: (string) | undefined) => void; + /** Add a file to the bucket that is uploaded when the app is deployed. + TODO: In the future this will support uploading any `Blob` type or + referencing a file from the local filesystem. */ + readonly addObject: (key: string, body: string) => void; + /** Run an inflight whenever a file is uploaded to the bucket. */ + readonly onCreate: (fn: IBucketEventHandler, opts?: (BucketOnCreateOptions) | undefined) => void; + /** Run an inflight whenever a file is deleted from the bucket. */ + readonly onDelete: (fn: IBucketEventHandler, opts?: (BucketOnDeleteOptions) | undefined) => void; + /** Run an inflight whenever a file is uploaded, modified, or deleted from the bucket. */ + readonly onEvent: (fn: IBucketEventHandler, opts?: (BucketOnEventOptions) | undefined) => void; + /** Run an inflight whenever a file is updated in the bucket. */ + readonly onUpdate: (fn: IBucketEventHandler, opts?: (BucketOnUpdateOptions) | undefined) => void; +} \ No newline at end of file diff --git a/examples/tests/valid/external_js.js b/examples/tests/valid/external_js.js index dbfdd7651f8..2c054a63d9c 100644 --- a/examples/tests/valid/external_js.js +++ b/examples/tests/valid/external_js.js @@ -1,21 +1,25 @@ -exports.getGreeting = function(name) { - return `Hello, ${name}!`; -}; - -exports.regexInflight = async function(pattern, text) { - const regex = new RegExp(pattern); - return regex.test(text); -}; - -exports.getUuid = async function() { - let uuid = require("uuid"); - return uuid.v4(); -}; - -exports.getData = async function() { - return require("./exported_data.js"); -}; +const assert = require("node:assert"); -exports.print = function(msg) { - console.log(`printing ${msg}`); +/** @type {import("./external_js.extern").default} */ +module.exports = { + getGreeting(name) { + return `Hello, ${name}!`; + }, + preflightBucket(bucket, id) { + assert.strictEqual(bucket.node.id, id); + }, + async regexInflight(pattern, text) { + const regex = new RegExp(pattern); + return regex.test(text); + }, + async getUuid() { + let uuid = require("uuid"); + return uuid.v4(); + }, + async getData() { + return require("./exported_data.js"); + }, + async print(msg) { + console.log(`printing ${msg}`); + }, }; diff --git a/examples/tests/valid/subdir/subfile.w b/examples/tests/valid/subdir/subfile.w index 7858188260d..0981074aee2 100644 --- a/examples/tests/valid/subdir/subfile.w +++ b/examples/tests/valid/subdir/subfile.w @@ -1,5 +1,5 @@ bring math; pub class Q { - extern "./util.js" static inflight greet(name: str): str; + extern "./util.ts" static inflight greet(name: str): str; } diff --git a/examples/tests/valid/subdir/util.extern.d.ts b/examples/tests/valid/subdir/util.extern.d.ts new file mode 100644 index 00000000000..d941b3480e7 --- /dev/null +++ b/examples/tests/valid/subdir/util.extern.d.ts @@ -0,0 +1,3 @@ +export default interface extern { + greet: (name: string) => Promise, +} diff --git a/examples/tests/valid/subdir/util.js b/examples/tests/valid/subdir/util.js deleted file mode 100644 index 9613a35374a..00000000000 --- a/examples/tests/valid/subdir/util.js +++ /dev/null @@ -1,3 +0,0 @@ -exports.greet = function(name) { - return 'Hello ' + name; -} diff --git a/examples/tests/valid/subdir/util.ts b/examples/tests/valid/subdir/util.ts new file mode 100644 index 00000000000..833d6b144c2 --- /dev/null +++ b/examples/tests/valid/subdir/util.ts @@ -0,0 +1,5 @@ +import type extern from "./util.extern"; + +export const greet: extern["greet"] = async (name) => { + return "Hello " + name; +} diff --git a/examples/tests/valid/url_utils.extern.d.ts b/examples/tests/valid/url_utils.extern.d.ts new file mode 100644 index 00000000000..d44c96f5206 --- /dev/null +++ b/examples/tests/valid/url_utils.extern.d.ts @@ -0,0 +1,3 @@ +export default interface extern { + isValidUrl: (url: string) => Promise, +} diff --git a/examples/tests/valid/url_utils.js b/examples/tests/valid/url_utils.js deleted file mode 100644 index 77bc7a93eef..00000000000 --- a/examples/tests/valid/url_utils.js +++ /dev/null @@ -1,8 +0,0 @@ -exports.isValidUrl = function(url) { - try { - new URL(url); - return true; - } catch { - return false; - } -}; \ No newline at end of file diff --git a/examples/tests/valid/url_utils.ts b/examples/tests/valid/url_utils.ts new file mode 100644 index 00000000000..5354d5835aa --- /dev/null +++ b/examples/tests/valid/url_utils.ts @@ -0,0 +1,5 @@ +import type extern from "./url_utils.extern"; + +export const isValidUrl: extern["isValidUrl"] = async (url) => { + return URL.canParse(url); +} \ No newline at end of file diff --git a/examples/wing-fixture/util.extern.d.ts b/examples/wing-fixture/util.extern.d.ts new file mode 100644 index 00000000000..1ff1975996d --- /dev/null +++ b/examples/wing-fixture/util.extern.d.ts @@ -0,0 +1,4 @@ +export default interface extern { + makeKey: (name: string) => string, + makeKeyInflight: (name: string) => Promise, +} diff --git a/libs/wingc/src/ast.rs b/libs/wingc/src/ast.rs index f8ea0436218..6f45c3044a1 100644 --- a/libs/wingc/src/ast.rs +++ b/libs/wingc/src/ast.rs @@ -279,7 +279,7 @@ pub enum FunctionBody { /// The function body implemented within a Wing scope. Statements(Scope), /// The `extern` modifier value, pointing to an external implementation file - External(String), + External(Utf8PathBuf), } #[derive(Debug)] diff --git a/libs/wingc/src/docs.rs b/libs/wingc/src/docs.rs index c57b6da02c4..81087211b38 100644 --- a/libs/wingc/src/docs.rs +++ b/libs/wingc/src/docs.rs @@ -45,6 +45,49 @@ impl Docs { ..Default::default() } } + + pub fn as_jsdoc_comment(&self) -> Option { + let mut markdown = CodeMaker::default(); + let mut has_data = false; + markdown.line("/** "); + + if let Some(s) = &self.summary { + has_data = true; + markdown.append(s); + } + + if let Some(s) = &self.remarks { + has_data = true; + markdown.line(s); + } + + if let Some(s) = &self.example { + has_data = true; + markdown.line(s); + } + + if let Some(s) = &self.returns { + has_data = true; + markdown.line(format!("@returns {s}")); + } + + if let Some(s) = &self.deprecated { + has_data = true; + markdown.line(format!("@deprecated {s}")); + } + + if let Some(s) = &self.see { + has_data = true; + markdown.line(format!("@see {s}")); + } + + if has_data { + markdown.append(" */"); + Some(markdown.to_string()) + } else { + None + } + } } impl Documented for SymbolKind { diff --git a/libs/wingc/src/dtsify/extern_dtsify.rs b/libs/wingc/src/dtsify/extern_dtsify.rs new file mode 100644 index 00000000000..b9c931322e9 --- /dev/null +++ b/libs/wingc/src/dtsify/extern_dtsify.rs @@ -0,0 +1,351 @@ +use std::collections::HashMap; + +use camino::Utf8PathBuf; +use const_format::formatcp; +use itertools::Itertools; + +use crate::{ + ast::{AccessModifier, Phase}, + dtsify::{ignore_member_phase, TYPE_INFLIGHT_POSTFIX}, + files::{update_file, FilesError}, + jsify::codemaker::CodeMaker, + type_check::*, + SymbolEnv, WINGSDK_ASSEMBLY_NAME, WINGSDK_DURATION, +}; + +const DURATION_FQN: &str = formatcp!("{WINGSDK_ASSEMBLY_NAME}.{WINGSDK_DURATION}"); + +/// Generates a self-contained .d.ts file for a given extern file. +pub struct ExternDTSifier<'a> { + libraries: &'a SymbolEnv, + extern_file: &'a Utf8PathBuf, + extern_file_env: &'a SymbolEnvOrNamespace, + /// The type information for any named types seen along the way that need to be hoisted to the top of the file + hoisted_types: CodeMaker, + /// Named types ecountered so far that have been hoisted. + /// The key is a psuedo-FQN and the value is the actual name to use in the .d.ts file. This name is based on name_counter since there may be duplicates. + known_types: HashMap, + /// Because all types will be in the same namespace, if you types have the same name this map will be used to disambiguate them by incrementing the counter. + name_counter: HashMap, +} + +/// Checks if given file has a valid extension to be considered an extern file +pub fn is_extern_file(file: &Utf8PathBuf) -> bool { + if let Some(ext) = file.extension() { + match ext { + "js" | "cjs" | "mjs" | "jsx" | "ts" | "cts" | "mts" | "tsx" => true, + _ => false, + } + } else { + false + } +} + +impl<'a> ExternDTSifier<'a> { + pub fn new( + extern_file: &'a Utf8PathBuf, + extern_file_env: &'a SymbolEnvOrNamespace, + libraries: &'a SymbolEnv, + ) -> Self { + Self { + libraries, + extern_file, + extern_file_env, + known_types: HashMap::new(), + name_counter: HashMap::new(), + hoisted_types: CodeMaker::default(), + } + } + + pub fn dtsify(&mut self) -> Result<(), FilesError> { + let mut dts = CodeMaker::default(); + dts.open("export default interface extern {"); + + if let SymbolEnvOrNamespace::SymbolEnv(extern_env) = self.extern_file_env { + for env_entry in extern_env.iter(false) { + if let Some(variable_info) = env_entry.1.as_variable() { + let sym_type = variable_info.type_; + let func_phase = sym_type.as_function_sig().unwrap().phase; + + let type_string = self.dtsify_type(sym_type, matches!(func_phase, Phase::Inflight)); + dts.line(format!("{}: {},", env_entry.0, type_string)); + } + } + } + dts.close("}"); + + // add all the known types we found + dts.line(self.hoisted_types.to_string()); + + let dts_filename = self.extern_file.with_extension("extern.d.ts"); + + update_file(&dts_filename, &dts.to_string()) + } + + fn dtsify_type(&mut self, type_: TypeRef, is_inflight: bool) -> String { + match &*type_ { + Type::Anything => "any".to_string(), + Type::Number => "number".to_string(), + Type::String => "string".to_string(), + Type::Boolean => "boolean".to_string(), + Type::Void => "void".to_string(), + Type::Nil => "undefined".to_string(), + Type::Json(_) => "Readonly".to_string(), + Type::MutJson => "any".to_string(), + Type::Duration => { + let duration_type = self + .libraries + .lookup_nested_str(DURATION_FQN, None) + .unwrap() + .0 + .as_type() + .unwrap(); + self.dtsify_type(duration_type, false) + } + Type::Optional(t) => format!("({}) | undefined", self.dtsify_type(*t, is_inflight)), + Type::Array(t) => format!("(readonly ({})[])", self.dtsify_type(*t, is_inflight)), + Type::MutArray(t) => format!("({})[]", self.dtsify_type(*t, is_inflight)), + Type::Map(t) => format!("Readonly>", self.dtsify_type(*t, is_inflight)), + Type::MutMap(t) => format!("Record", self.dtsify_type(*t, is_inflight)), + Type::Set(t) => format!("Readonly>", self.dtsify_type(*t, is_inflight)), + Type::MutSet(t) => format!("Set<{}>", self.dtsify_type(*t, is_inflight)), + Type::Function(f) => self.dtsify_function_signature(&f, is_inflight), + Type::Class(_) | Type::Interface(_) | Type::Struct(_) | Type::Enum(_) => { + self.resolve_named_type(type_, is_inflight) + } + Type::Inferred(_) | Type::Unresolved => { + panic!("Extern must use resolved types") + } + } + } + + fn resolve_named_type(&mut self, type_: TypeRef, is_inflight: bool) -> String { + let fqn = match &*type_ { + Type::Class(c) => c.fqn.as_ref().unwrap_or(&c.name.span.file_id), + Type::Interface(i) => i.fqn.as_ref().unwrap_or(&i.name.span.file_id), + Type::Struct(s) => s.fqn.as_ref().unwrap_or(&s.name.span.file_id), + Type::Enum(e) => &e.name.span.file_id, + _ => panic!("Not a named type"), + }; + let base_name = match &*type_ { + Type::Class(c) => { + if is_inflight { + format!("{}{}", c.name.name, TYPE_INFLIGHT_POSTFIX) + } else { + c.name.name.clone() + } + } + Type::Interface(i) => { + if is_inflight { + format!("{}{}", i.name.name, TYPE_INFLIGHT_POSTFIX) + } else { + i.name.name.clone() + } + } + Type::Struct(s) => s.name.name.clone(), + Type::Enum(e) => e.name.name.clone(), + _ => panic!("Not a named type"), + }; + let type_key = format!("{fqn}|{base_name}"); + + if let Some(name) = self.known_types.get(&type_key) { + name.clone() + } else { + let name_counter = self.name_counter.get(&base_name).unwrap_or(&0); + let name = if *name_counter == 0 { + base_name.to_string() + } else { + format!("{base_name}{name_counter}") + }; + + self.name_counter.insert(base_name, name_counter + 1); + self.known_types.insert(type_key, name.clone()); + + let type_code = match &*type_ { + Type::Class(c) => self.dtsify_class(c, is_inflight), + Type::Interface(i) => self.dtsify_interface(i, is_inflight), + Type::Struct(s) => self.dtsify_struct(s), + Type::Enum(e) => self.dtsify_enum(e), + _ => panic!("Not a named type"), + }; + self.hoisted_types.line(type_code); + + name + } + } + + fn dtsify_function_signature(&mut self, f: &FunctionSignature, is_inflight: bool) -> String { + let args = self.dtsify_parameters(&f.parameters, is_inflight); + + let is_inflight = matches!(f.phase, Phase::Inflight); + + let return_type = self.dtsify_type(f.return_type, is_inflight); + let return_type = if is_inflight { + format!("Promise<{return_type}>") + } else { + return_type + }; + + format!("({args}) => {return_type}") + } + + fn dtsify_enum(&mut self, enum_: &Enum) -> CodeMaker { + let mut code = CodeMaker::default(); + + if let Some(docs) = &enum_.docs.as_jsdoc_comment() { + code.line(docs); + } + code.open(format!("export enum {} {{", enum_.name.name)); + + for (i, variant) in enum_.values.iter().enumerate() { + code.line(format!("{variant} = {i},")); + } + + code.close("}"); + + code + } + + fn dtsify_struct(&mut self, struct_: &Struct) -> CodeMaker { + let mut code = CodeMaker::default(); + if let Some(docs) = &struct_.docs.as_jsdoc_comment() { + code.line(docs); + } + code.open(format!("export interface {} {{", struct_.name.name)); + + code.line(self.dtsify_inner_classlike(struct_, false)); + + code.close("}"); + + code + } + + fn dtsify_interface(&mut self, interface: &Interface, is_inflight: bool) -> CodeMaker { + let mut code = CodeMaker::default(); + let interface_name = if is_inflight { + format!("{}{TYPE_INFLIGHT_POSTFIX}", &interface.name.name) + } else { + interface.name.name.to_string() + }; + + if let Some(docs) = &interface.docs.as_jsdoc_comment() { + code.line(docs); + } + code.line(format!("export interface {interface_name}")); + if !interface.extends.is_empty() { + code.append(" extends "); + code.append( + &interface + .extends + .iter() + .map(|udt| self.dtsify_type(*udt, is_inflight)) + .join(", "), + ); + } + + code.append(" {"); + code.indent(); + + code.line(self.dtsify_inner_classlike(interface, is_inflight)); + + code.close("}"); + + code + } + + fn dtsify_class(&mut self, class: &Class, is_inflight: bool) -> CodeMaker { + let mut code = CodeMaker::default(); + let class_name = if is_inflight { + format!("{}{TYPE_INFLIGHT_POSTFIX}", class.name) + } else { + class.name.name.to_string() + }; + + if let Some(docs) = &class.docs.as_jsdoc_comment() { + code.line(docs); + } + code.line(format!("export class {class_name}")); + if let Some(parent) = &class.parent { + code.append(" extends "); + code.append(self.dtsify_type(*parent, is_inflight)); + } + + if !class.implements.is_empty() { + code.append(" implements "); + code.append( + &class + .implements + .iter() + .map(|udt| self.dtsify_type(*udt, is_inflight)) + .join(", "), + ); + } + + code.append(" {"); + code.indent(); + + code.line(self.dtsify_inner_classlike(class, is_inflight)); + + code.close("}"); + code + } + + fn dtsify_parameters(&mut self, arg_list: &Vec, is_inflight: bool) -> String { + let mut args = vec![]; + + for (i, arg) in arg_list.iter().enumerate() { + let arg_name = if arg.name.is_empty() { + // function type annotations don't always have names + format!("arg{}", i) + } else { + arg.name.clone() + }; + + args.push(format!( + "{arg_name}{}: {}", + if arg.typeref.is_option() { "?" } else { "" }, + self.dtsify_type(arg.typeref, is_inflight) + )); + } + args.join(", ") + } + + fn dtsify_inner_classlike(&mut self, classlike: &impl ClassLike, is_inflight: bool) -> CodeMaker { + let mut code = CodeMaker::default(); + for member_var in classlike.get_env().iter(false).filter_map(|(_, kind, lookup)| { + if lookup.init || !matches!(lookup.access, AccessModifier::Public) { + return None; + } + + let variable = kind.as_variable()?; + + if let Some(sig) = variable.type_.as_function_sig() { + if ignore_member_phase(sig.phase, is_inflight) { + return None; + } + } else { + if ignore_member_phase(variable.phase, is_inflight) { + return None; + } + } + + if variable.kind != VariableKind::InstanceMember { + None + } else { + Some(variable) + } + }) { + if let Some(docs) = member_var.docs.as_ref().and_then(|d| d.as_jsdoc_comment()) { + code.line(docs); + } + code.line(format!( + "{}{}{}: {};", + if member_var.reassignable { "" } else { "readonly " }, + member_var.name, + if member_var.type_.is_option() { "?" } else { "" }, + self.dtsify_type(member_var.type_, is_inflight) + )); + } + code + } +} diff --git a/libs/wingc/src/dtsify/mod.rs b/libs/wingc/src/dtsify/mod.rs index 54e57abe883..1c18d3540e7 100644 --- a/libs/wingc/src/dtsify/mod.rs +++ b/libs/wingc/src/dtsify/mod.rs @@ -8,8 +8,9 @@ use crate::{ ast::*, diagnostic::report_diagnostic, file_graph::FileGraph, files::Files, jsify::codemaker::CodeMaker, type_check::Types, WINGSDK_ASSEMBLY_NAME, }; +pub mod extern_dtsify; -const TYPE_INFLIGHT_POSTFIX: &str = "$Inflight"; +pub const TYPE_INFLIGHT_POSTFIX: &str = "$Inflight"; const TYPE_INTERNAL_NAMESPACE: &str = "$internal"; const TYPE_STD: &str = "std"; @@ -389,11 +390,11 @@ impl<'a> DTSifier<'a> { } } -fn ignore_member_phase(phase: Phase, is_inflight_client: bool) -> bool { +pub fn ignore_member_phase(phase: Phase, is_inflight: bool) -> bool { // If we're an inflight client, we want to ignore preflight members // Or // If we're a preflight client, we want to ignore inflight members - (is_inflight_client && matches!(phase, Phase::Preflight)) || (!is_inflight_client && matches!(phase, Phase::Inflight)) + (is_inflight && matches!(phase, Phase::Preflight)) || (!is_inflight && matches!(phase, Phase::Inflight)) } #[test] diff --git a/libs/wingc/src/files.rs b/libs/wingc/src/files.rs index b4558ed7d05..646636aebf8 100644 --- a/libs/wingc/src/files.rs +++ b/libs/wingc/src/files.rs @@ -84,14 +84,33 @@ impl Files { fs::create_dir_all(parent).map_err(FilesError::IoError)?; } - let mut file = File::create(full_path).map_err(FilesError::IoError)?; - file.write_all(content.as_bytes()).map_err(FilesError::IoError)?; - file.flush().map_err(FilesError::IoError)?; + write_file(&full_path, content)?; } Ok(()) } } +/// Write file to disk +pub fn write_file(path: &Utf8Path, content: &str) -> Result<(), FilesError> { + let mut file = File::create(path).map_err(FilesError::IoError)?; + file.write_all(content.as_bytes()).map_err(FilesError::IoError)?; + file.flush().map_err(FilesError::IoError)?; + Ok(()) +} + +// Check if the content of a file is the same as existing content. If so, skip writing the file. +pub fn update_file(path: &Utf8Path, content: &str) -> Result<(), FilesError> { + let Ok(existing_content) = fs::read(path) else { + return write_file(path, content); + }; + + if existing_content != content.as_bytes() { + write_file(path, content) + } else { + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -165,4 +184,39 @@ mod tests { let file1_content = fs::read_to_string(file1_path).expect("Failed to read file"); assert_eq!(file1_content, "content1"); } + #[test] + fn test_update_file() { + let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory"); + let out_dir = Utf8Path::from_path(temp_dir.path()).expect("invalid unicode path"); + + let file_path = out_dir.join("file"); + + // Write the file for the first time + assert!(update_file(&file_path, "content").is_ok()); + + // Update the file + let new_content = "new content"; + assert!(update_file(&file_path, new_content).is_ok()); + + // Verify that the file was updated + let file_content = fs::read_to_string(file_path.clone()).expect("Failed to read file"); + assert_eq!(file_content, new_content); + let last_updated = file_path + .metadata() + .expect("Failed to get file metadata") + .modified() + .expect("Failed to get file modified time"); + + // Try to update the file with the same content + assert!(update_file(&file_path, new_content).is_ok()); + + // Verify that the file was not updated (check the timestamps via stat) + let updated = file_path + .metadata() + .expect("Failed to get file metadata") + .modified() + .expect("Failed to get file modified time"); + + assert_eq!(updated, last_updated); + } } diff --git a/libs/wingc/src/jsify.rs b/libs/wingc/src/jsify.rs index 1dda49a9edc..16406647d00 100644 --- a/libs/wingc/src/jsify.rs +++ b/libs/wingc/src/jsify.rs @@ -1318,7 +1318,6 @@ impl<'a> JSifier<'a> { let body = match &func_def.body { FunctionBody::Statements(scope) => self.jsify_scope_body(scope, ctx), FunctionBody::External(extern_path) => { - let extern_path = Utf8Path::new(extern_path); let entrypoint_is_file = self.compilation_init_path.is_file(); let entrypoint_dir = if entrypoint_is_file { self.compilation_init_path.parent().unwrap() diff --git a/libs/wingc/src/lib.rs b/libs/wingc/src/lib.rs index 36850203ce6..e61a502a928 100644 --- a/libs/wingc/src/lib.rs +++ b/libs/wingc/src/lib.rs @@ -12,6 +12,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use closure_transform::ClosureTransformer; use comp_ctx::set_custom_panic_hook; use diagnostic::{found_errors, report_diagnostic, Diagnostic}; +use dtsify::extern_dtsify::{is_extern_file, ExternDTSifier}; use file_graph::FileGraph; use files::Files; use fold::Fold; @@ -205,10 +206,10 @@ pub unsafe extern "C" fn wingc_compile(ptr: u32, len: u32) -> u64 { let results = compile(project_dir, source_path, None, output_dir); - if results.is_err() { - WASM_RETURN_ERROR + if let Ok(results) = results { + string_to_combined_ptr(serde_json::to_string(&results).unwrap()) } else { - string_to_combined_ptr(serde_json::to_string(&results.unwrap()).unwrap()) + WASM_RETURN_ERROR } } @@ -359,9 +360,11 @@ pub fn compile( let scope = asts.get_mut(file).expect("matching AST not found"); jsifier.jsify(file, &scope); } - match jsifier.output_files.borrow().emit_files(out_dir) { - Ok(()) => {} - Err(err) => report_diagnostic(err.into()), + if !found_errors() { + match jsifier.output_files.borrow().emit_files(out_dir) { + Ok(()) => {} + Err(err) => report_diagnostic(err.into()), + } } // -- DTSIFICATION PHASE -- @@ -372,10 +375,25 @@ pub fn compile( let scope = asts.get_mut(file).expect("matching AST not found"); dtsifier.dtsify(file, &scope); } - let output_files = dtsifier.output_files.borrow(); - match output_files.emit_files(out_dir) { - Ok(()) => {} - Err(err) => report_diagnostic(err.into()), + if !found_errors() { + let output_files = dtsifier.output_files.borrow(); + match output_files.emit_files(out_dir) { + Ok(()) => {} + Err(err) => report_diagnostic(err.into()), + } + } + } + + // -- EXTERN DTSIFICATION PHASE -- + for source_files_env in &types.source_file_envs { + if is_extern_file(source_files_env.0) { + let mut extern_dtsifier = ExternDTSifier::new(source_files_env.0, source_files_env.1, &types.libraries); + if !found_errors() { + match extern_dtsifier.dtsify() { + Ok(()) => {} + Err(err) => report_diagnostic(err.into()), + }; + } } } @@ -392,7 +410,7 @@ pub fn compile( }) .collect::>(); - return Ok(CompilerOutput { imported_namespaces }); + Ok(CompilerOutput { imported_namespaces }) } pub fn is_absolute_path(path: &Utf8Path) -> bool { diff --git a/libs/wingc/src/parser.rs b/libs/wingc/src/parser.rs index 051538ea333..31945b6b531 100644 --- a/libs/wingc/src/parser.rs +++ b/libs/wingc/src/parser.rs @@ -1615,7 +1615,7 @@ impl<'s> Parser<'s> { .report(); } - FunctionBody::External(file_path.to_string()) + FunctionBody::External(file_path) } else { FunctionBody::Statements(self.build_scope(&self.get_child_field(func_def_node, "block")?, phase)) }; diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs index c274525dd1c..4f7623860b5 100644 --- a/libs/wingc/src/type_check.rs +++ b/libs/wingc/src/type_check.rs @@ -4839,13 +4839,63 @@ impl<'a> TypeChecker<'a> { tc.inner_scopes.push((scope, tc.ctx.clone())); } - if let FunctionBody::External(_) = &method_def.body { + if let FunctionBody::External(extern_path) = &method_def.body { if !method_def.is_static { tc.spanned_error( method_name, "Extern methods must be declared \"static\" (they cannot access instance members)", ); } + if !tc.types.source_file_envs.contains_key(extern_path) { + let new_env = tc.types.add_symbol_env(SymbolEnv::new( + None, + SymbolEnvKind::Type(tc.types.void()), + method_sig.phase, + 0, + )); + tc.types + .source_file_envs + .insert(extern_path.clone(), SymbolEnvOrNamespace::SymbolEnv(new_env)); + } + + if let Some(SymbolEnvOrNamespace::SymbolEnv(extern_env)) = tc.types.source_file_envs.get_mut(extern_path) { + let lookup = extern_env.lookup(method_name, None); + if let Some(lookup) = lookup { + // check if it's the same type + if let Some(lookup) = lookup.as_variable() { + if !lookup.type_.is_same_type_as(method_type) { + report_diagnostic(Diagnostic { + message: "extern type must be the same in all usages".to_string(), + span: Some(method_name.span.clone()), + annotations: vec![DiagnosticAnnotation { + message: "First declared here".to_string(), + span: lookup.name.span.clone(), + }], + hints: vec![format!("Change type to match first declaration: {}", lookup.type_)], + }); + } + } else { + panic!("Expected extern to be a variable"); + } + } else { + extern_env + .define( + method_name, + SymbolKind::Variable(VariableInfo { + name: method_name.clone(), + type_: *method_type, + access: method_def.access, + phase: method_def.signature.phase, + docs: None, + kind: VariableKind::StaticMember, + reassignable: false, + }), + method_def.access, + StatementIdx::Top, + ) + .expect("Expected extern to be defined"); + } + } } }, ); diff --git a/tools/hangar/__snapshots__/test_corpus/valid/bring_local.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/bring_local.test.w_compile_tf-aws.md index 31d9e298877..574fb7d8459 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/bring_local.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/bring_local.test.w_compile_tf-aws.md @@ -49,7 +49,7 @@ module.exports = function({ }) { constructor({ }) { } static async greet(name) { - return (require("../../../subdir/util.js")["greet"])(name) + return (require("../../../subdir/util.ts")["greet"])(name) } } return Q; diff --git a/tools/hangar/__snapshots__/test_corpus/valid/capture_tokens.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/capture_tokens.test.w_compile_tf-aws.md index 4854c34282c..64085c279c8 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/capture_tokens.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/capture_tokens.test.w_compile_tf-aws.md @@ -52,7 +52,7 @@ module.exports = function({ }) { this.$this_url = $this_url; } static async isValidUrl(url) { - return (require("../../../url_utils.js")["isValidUrl"])(url) + return (require("../../../url_utils.ts")["isValidUrl"])(url) } async foo() { $helpers.assert((await MyResource.isValidUrl(this.$this_url)), "MyResource.isValidUrl(this.url)"); diff --git a/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md index 78c65ea6183..a6ecbf35cf2 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md @@ -87,6 +87,20 @@ module.exports = function({ }) { "aws": [ {} ] + }, + "resource": { + "aws_s3_bucket": { + "my-bucket": { + "//": { + "metadata": { + "path": "root/Default/Default/my-bucket/Default", + "uniqueId": "my-bucket" + } + }, + "bucket_prefix": "my-bucket-c8fafcc6-", + "force_destroy": false + } + } } } ``` @@ -111,6 +125,9 @@ class $Root extends $stdlib.std.Resource { static getGreeting(name) { return (require("../../../external_js.js")["getGreeting"])(name) } + static preflightBucket(bucket, id) { + return (require("../../../external_js.js")["preflightBucket"])(bucket, id) + } static _toInflightType() { return ` require("${$helpers.normalPath(__dirname)}/inflight.Foo-1.js")({ @@ -223,6 +240,8 @@ class $Root extends $stdlib.std.Resource { } $helpers.assert($helpers.eq((Foo.getGreeting("Wingding")), "Hello, Wingding!"), "Foo.getGreeting(\"Wingding\") == \"Hello, Wingding!\""); const f = new Foo(this, "Foo"); + const bucket = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "my-bucket"); + const result = (Foo.preflightBucket(bucket, "my-bucket")); this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:call", new $Closure1(this, "$Closure1")); this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:console", new $Closure2(this, "$Closure2")); }