Skip to content

Commit

Permalink
Merge pull request #334 from jianzs/feat-custom-port
Browse files Browse the repository at this point in the history
feat: support custom port for website resource and simulator
  • Loading branch information
jianzs authored Aug 23, 2024
2 parents e4e66c5 + abc80a8 commit 8feb3e2
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 31 deletions.
6 changes: 6 additions & 0 deletions .changeset/chilly-crabs-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@plutolang/simulator-adapter": patch
"@plutolang/cli": patch
---

feat(simulator): support custom address configuration
8 changes: 8 additions & 0 deletions .changeset/strange-boxes-heal.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions apps/cli/src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
7 changes: 6 additions & 1 deletion components/adapters/simulator/src/simAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
40 changes: 28 additions & 12 deletions components/adapters/simulator/src/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, simulator.IResourceInstance>;
private closures: Map<string, ComputeClosure<AnyFunction>>;

Expand All @@ -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();

Expand Down Expand Up @@ -212,19 +212,35 @@ export class Simulator {

public async start(): Promise<void> {
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;
}
}
}

Expand Down
71 changes: 55 additions & 16 deletions packages/pluto-infra/src/simulator/website.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,88 @@
import fs from "fs";
import http from "http";
import path from "path";
import cors from "cors";
import express from "express";
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;

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<number>((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 {
Expand Down
10 changes: 9 additions & 1 deletion packages/pluto-infra/src/utils/impl-class-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ export class ImplClassMap<T, K extends new (...args: any[]) => T> {
...args: any[]
): Promise<T> {
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;
}
}
12 changes: 12 additions & 0 deletions packages/pluto-py/pluto_client/universal_class.py
Original file line number Diff line number Diff line change
@@ -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()
16 changes: 15 additions & 1 deletion packages/pluto-py/pluto_client/website.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,6 +9,8 @@
IResourceInfraApi,
)

from .universal_class import UniversalClass


@dataclass
class WebsiteOptions:
Expand All @@ -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):
Expand Down Expand Up @@ -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}'")
12 changes: 12 additions & 0 deletions packages/pluto/src/website.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down

0 comments on commit 8feb3e2

Please sign in to comment.