diff --git a/.changeset/chilly-crabs-melt.md b/.changeset/chilly-crabs-melt.md new file mode 100644 index 00000000..b6be13b8 --- /dev/null +++ b/.changeset/chilly-crabs-melt.md @@ -0,0 +1,6 @@ +--- +"@plutolang/simulator-adapter": patch +"@plutolang/cli": patch +--- + +feat(simulator): support custom address configuration diff --git a/.changeset/strange-boxes-heal.md b/.changeset/strange-boxes-heal.md new file mode 100644 index 00000000..fd6a0308 --- /dev/null +++ b/.changeset/strange-boxes-heal.md @@ -0,0 +1,8 @@ +--- +"@plutolang/pluto-infra": patch +"@plutolang/pluto": patch +--- + +feat(sdk): add support for configuring host and port for website resource + +This change introduces the ability to specify custom host and port settings for website resources, enhancing flexibility during local development. diff --git a/apps/cli/src/commands/run.ts b/apps/cli/src/commands/run.ts index 7d761cff..debc728a 100644 --- a/apps/cli/src/commands/run.ts +++ b/apps/cli/src/commands/run.ts @@ -76,6 +76,7 @@ async function executeOnce(project: config.Project, stack: config.Stack, entrypo archRef: archRef, entrypoint: "", stateDir: stateDir, + extraConfigs: project.configs, }); await deployWithAdapter(adapter, stack); } diff --git a/components/adapters/simulator/src/simAdapter.ts b/components/adapters/simulator/src/simAdapter.ts index 0d712f47..cceca461 100644 --- a/components/adapters/simulator/src/simAdapter.ts +++ b/components/adapters/simulator/src/simAdapter.ts @@ -38,7 +38,12 @@ export class SimulatorAdapter extends core.Adapter { WORK_DIR: this.stateDir, }; - this.simulator = new Simulator(this.rootpath); + let address: string | undefined; + if (this.extraConfigs?.simulator) { + address = this.extraConfigs.simulator.address; + } + + this.simulator = new Simulator(this.rootpath, address); await this.simulator.start(); envs.PLUTO_SIMULATOR_URL = this.simulator.serverUrl; diff --git a/components/adapters/simulator/src/simulator.ts b/components/adapters/simulator/src/simulator.ts index 0c480c90..6c0043b2 100644 --- a/components/adapters/simulator/src/simulator.ts +++ b/components/adapters/simulator/src/simulator.ts @@ -8,8 +8,6 @@ import { ComputeClosure, AnyFunction, createClosure } from "@plutolang/base/clos import { MethodNotFound, ResourceNotFound } from "./errors"; export class Simulator { - private readonly projectRoot: string; - private resources: Map; private closures: Map>; @@ -18,8 +16,10 @@ export class Simulator { private readonly exitHandler = async () => {}; - constructor(projectRoot: string) { - this.projectRoot = projectRoot; + constructor( + private readonly projectRoot: string, + private readonly address?: string + ) { this.resources = new Map(); this.closures = new Map(); @@ -212,19 +212,35 @@ export class Simulator { public async start(): Promise { const expressApp = this.createExpress(); - for (let port = 9001; ; port++) { - const server = await tryListen(expressApp, port); + + if (this.address) { + const [host, port] = this.address.split(":"); + const server = await tryListen(expressApp, parseInt(port), host); if (server === undefined) { - continue; + throw new Error(`Failed to listen on ${this.address}`); } - const addr = server.address(); - if (addr && typeof addr === "object" && addr.port) { - this._serverUrl = `http://${addr.address}:${addr.port}`; - } + this._serverUrl = `http://${host}:${port}`; this._server = server; + } else { + if (process.env.DEBUG) { + console.log("Starting simulator on a random port..."); + } + + for (let port = 9001; ; port++) { + const server = await tryListen(expressApp, port); + if (server === undefined) { + continue; + } - break; + const addr = server.address(); + if (addr && typeof addr === "object" && addr.port) { + this._serverUrl = `http://localhost:${addr.port}`; + } + this._server = server; + + break; + } } } diff --git a/packages/pluto-infra/src/simulator/website.ts b/packages/pluto-infra/src/simulator/website.ts index 23ab7973..2a610cf9 100644 --- a/packages/pluto-infra/src/simulator/website.ts +++ b/packages/pluto-infra/src/simulator/website.ts @@ -1,5 +1,4 @@ import fs from "fs"; -import http from "http"; import path from "path"; import cors from "cors"; import express from "express"; @@ -7,6 +6,28 @@ import { IResourceInfra } from "@plutolang/base"; import { IWebsiteClient, IWebsiteInfra, Website, WebsiteOptions } from "@plutolang/pluto"; import { genResourceId } from "@plutolang/base/utils"; +/** + * Adapts the options to the correct names for TypeScript. + * The option names for TypeScript and Python are different, so this function converts Python-style + * option names to TypeScript-style option names. + * + * @param opts - The options object that may contain Python-style option names. + * @returns The adapted options object with TypeScript-style option names. + */ +function adaptOptions(opts?: any): WebsiteOptions | undefined { + if (opts === undefined) { + return; + } + + if (opts.sim_host) { + opts.simHost = opts.sim_host; + } + if (opts.sim_port) { + opts.simPort = opts.sim_port; + } + return opts; +} + export class SimWebsite implements IResourceInfra, IWebsiteInfra, IWebsiteClient { public id: string; private websiteDir: string; @@ -14,36 +35,54 @@ export class SimWebsite implements IResourceInfra, IWebsiteInfra, IWebsiteClient private envs: { [key: string]: string } = {}; private originalPlutoJs: string | undefined; - private expressApp: express.Application; - private httpServer: http.Server; + private host: string; private port: number; - public outputs: string; + public outputs?: string; constructor(path: string, name?: string, options?: WebsiteOptions) { name = name ?? "default"; + options = adaptOptions(options); + this.id = genResourceId(Website.fqn, name); this.websiteDir = path; - this.expressApp = express(); - this.expressApp.use(cors()); - this.expressApp.use(express.static(this.websiteDir)); + this.host = options?.simHost ?? "localhost"; + this.port = parseInt(options?.simPort ?? "0"); + } - this.httpServer = this.expressApp.listen(0); - const address = this.httpServer.address(); - if (address && typeof address !== "string") { - this.port = address.port; - } else { - throw new Error(`Failed to obtain the port for the router: ${name}`); + public async init() { + const expressApp = express(); + expressApp.use(cors()); + expressApp.use(express.static(this.websiteDir)); + + const httpServer = expressApp.listen(this.port, this.host); + + async function waitForServerReady() { + return await new Promise((resolve, reject) => { + httpServer.on("listening", () => { + const address = httpServer.address(); + if (address && typeof address !== "string") { + resolve(address.port); + } else { + reject(new Error(`Failed to obtain the port for the router`)); + } + }); + + httpServer.on("error", (err) => { + console.error(err); + reject(err); + }); + }); } + const realPort = await waitForServerReady(); + this.port = realPort; this.outputs = this.url(); - - options; } public url(): string { - return `http://localhost:${this.port}`; + return `http://${this.host}:${this.port}`; } public addEnv(key: string, value: string): void { diff --git a/packages/pluto-infra/src/utils/impl-class-map.ts b/packages/pluto-infra/src/utils/impl-class-map.ts index b5fa0008..ed0f418a 100644 --- a/packages/pluto-infra/src/utils/impl-class-map.ts +++ b/packages/pluto-infra/src/utils/impl-class-map.ts @@ -79,6 +79,14 @@ export class ImplClassMap T> { ...args: any[] ): Promise { const implClass = await this.loadImplClassOrThrow(platformType, provisionType); - return new implClass(...args); + + const instance = new implClass(...args); + + // If there is the `init` method in the implementation class, use it to initialize the instance. + if (typeof implClass.prototype.init === "function") { + await (instance as any).init(); + } + + return instance; } } diff --git a/packages/pluto-py/pluto_client/universal_class.py b/packages/pluto-py/pluto_client/universal_class.py new file mode 100644 index 00000000..05b9460b --- /dev/null +++ b/packages/pluto-py/pluto_client/universal_class.py @@ -0,0 +1,12 @@ +import os +from typing import Any + + +class UniversalClass: + def __getattr__(self, name: str): + class MethodCaller: + def __call__(self, *args: Any, **kwargs: Any): + if os.getenv("DEBUG", False): + print(f"Method called: {name}") + return None + return MethodCaller() \ No newline at end of file diff --git a/packages/pluto-py/pluto_client/website.py b/packages/pluto-py/pluto_client/website.py index ffa94e6f..e27252c6 100644 --- a/packages/pluto-py/pluto_client/website.py +++ b/packages/pluto-py/pluto_client/website.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional from dataclasses import dataclass from pluto_base import utils from pluto_base.platform import PlatformType @@ -9,6 +9,8 @@ IResourceInfraApi, ) +from .universal_class import UniversalClass + @dataclass class WebsiteOptions: @@ -17,6 +19,16 @@ class WebsiteOptions: Currently, only support Vercel. If an invalid value is provided, or if no value is provided at all, it will default to your specified platform. """ + sim_host: Optional[str] = None + """ + Host address for simulating the website when running the project with `pluto run`. If not + provided, it will be `localhost`. + """ + sim_port: Optional[str] = None + """ + Port number for simulating the website when running the project with `pluto run`. If not + provided, it will be randomly assigned. + """ class IWebsiteClientApi(IResourceClientApi): @@ -55,5 +67,7 @@ def __init__( from .clients import shared self._client = shared.WebsiteClient(path, name, opts) + if platform_type in [PlatformType.Simulator]: + self._client = UniversalClass() # type: ignore else: raise ValueError(f"not support this runtime '{platform_type}'") diff --git a/packages/pluto/src/website.ts b/packages/pluto/src/website.ts index 19213f42..c8106075 100644 --- a/packages/pluto/src/website.ts +++ b/packages/pluto/src/website.ts @@ -17,6 +17,18 @@ export interface WebsiteOptions { * all, it will default to your specified platform. */ platform?: "Vercel"; + + /** + * Host address for simulating the website when running the project with `pluto run`. If not + * provided, it will be `localhost`. + */ + simHost?: string; + + /** + * Port number for simulating the website when running the project with `pluto run`. If not + * provided, it will be a random port. + */ + simPort?: string; } /**