Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

127 customize gateway from bosconfigjson #147

Merged
merged 17 commits into from
Aug 6, 2024
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ A fully featured config may look like this:
"uploadApi": "https://ipfs.near.social/add",
"uploadApiHeaders": {},
},
"gateway": {
"bundleUrl": "https://ipfs.web4.near.page/ipfs/bafybeibe63hqugbqr4writdxgezgl5swgujay6t5uptw2px7q63r7crk2q/",
"tagName": "near-social-viewer"
}
}
```

Expand All @@ -131,6 +135,9 @@ The `bos.config.json` file consists of a base configuration that defines default
* `format`: (Optional) Indicates whether to format code on build. Default value is `true`.
* `aliases`: (Optional) Provides a list of alias files to use for replacing network-specific values with correct overrides.
* `index`: (Optional) Default widget src to use when using a custom gateway dist.
* `gateway`: (Optional) Configures gateway object.
* `bundleUrl`: gateway url.
* `tagName`: element tag name.

---

Expand Down Expand Up @@ -229,14 +236,13 @@ Running the bos-workspace dev server will start a local gateway with a standard
bw dev --no-gateway
```

However, there is an option to override this default gateway with a custom `/dist`. This is helpful when building widgets that utilize [custom VM elements](https://github.com/NEARBuilders/near-bos-webcomponent?tab=readme-ov-file#configuring-vm-custom-elements). To use this feature, use the `-g` flag with a path to the local custom distribution or link to package published on [nearfs](https://github.com/vgrichina/nearfs) or via cdn:
However, there is an option to override this default gateway with a custom `/dist`. This is helpful when building widgets that utilize [custom VM elements](https://github.com/NEARBuilders/near-bos-webcomponent?tab=readme-ov-file#configuring-vm-custom-elements). To use this feature, specify the gateway bundle url and the tag name in the `bos.config.json` file.

```cmd
bw dev -g path/to/dist
```

```cmd
bw dev -g https://ipfs.web4.near.page/ipfs/bafybeiancp5im5nfkdki3cfvo7ownl2knjovqh7bseegk4zvzsl4buryoi
"gateway": {
"bundleUrl": "https://ipfs.web4.near.page/ipfs/bafybeibe63hqugbqr4writdxgezgl5swgujay6t5uptw2px7q63r7crk2q/",
"tagName": "near-social-viewer"
}
```

This will automatically start the local gateway serving your widgets through the provided dist.
Expand Down
6 changes: 5 additions & 1 deletion examples/single/bos.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
"aliases": ["./aliases.testnet.json"],
"index": "quickstart.testnet/widget/home"
}
}
},
"gateway": {
"bundleUrl": "https://ipfs.web4.near.page/ipfs/bafybeibe63hqugbqr4writdxgezgl5swgujay6t5uptw2px7q63r7crk2q/",
"tagName": "near-social-viewer"
}
}
2 changes: 0 additions & 2 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ async function run() {
.option("-n, --network <network>", "network to build for", "mainnet")
.option("-l, --loglevel <loglevel>", "log level (ERROR, WARN, INFO, DEV, BUILD, DEBUG)", "DEV")
.option("-p, --port <port>", "Port to run the server on", "8080")
.option("-g, --gateway <gateway>", "Path to custom gateway dist", true)
.option("--no-gateway", "Disable the gateway")
.option("--no-hot", "Disable hot reloading")
.option("--no-open", "Disable opening the browser")
Expand Down Expand Up @@ -62,7 +61,6 @@ async function run() {
.option("-n, --network <network>", "network to build for", "mainnet")
.option("-l, --loglevel <loglevel>", "log level (ERROR, WARN, INFO, DEV, BUILD, DEBUG)")
.option("-p, --port <port>", "Port to run the server on", "8080")
.option("-g, --gateway <gateway>", "Path to custom gateway dist", true)
.option("--no-gateway", "Disable the gateway")
.option("--no-hot", "Disable hot reloading")
.option("--no-open", "Disable opening the browser")
Expand Down
6 changes: 6 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Joi from 'joi';
import { readJson } from '@/lib/utils/fs';
import { Network } from './types';
import path from 'path';
import { GatewayConfig } from './dev';

export interface BaseConfig {
account?: string; // default account to serve widgets from
Expand All @@ -20,6 +21,7 @@ export interface BaseConfig {
index?: string; // widget to use as index
aliasPrefix?: string; // prefix to use for aliases, default is "alias"
aliasesContainsPrefix?: boolean; // aliases keys contains prefix (default is false)
gateway?: GatewayConfig // gateway config object
}

interface NetworkConfig {
Expand Down Expand Up @@ -62,6 +64,10 @@ const baseConfigSchema = Joi.object({
aliasPrefix: Joi.string().allow(null),
aliasesContainsPrefix: Joi.boolean().allow(null),
index: Joi.string().allow(null),
gateway: Joi.object({
tagName: Joi.string(),
bundleUrl: Joi.string(),
}).and('tagName', 'bundleUrl').allow(null),
});

const networkConfigSchema = Joi.object({
Expand Down
42 changes: 39 additions & 3 deletions lib/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,28 @@ var appDevOptions: null | DevOptions = null;
let io: null | IoServer = null;
let fileWatcher: null | FSWatcher = null;

export const DEFAULT_GATEWAY = {
enabled: true,
bundleUrl: "https://ipfs.web4.near.page/ipfs/bafybeibe63hqugbqr4writdxgezgl5swgujay6t5uptw2px7q63r7crk2q/",
tagName: "near-social-viewer"
};

export type DevOptions = {
port?: number; // port to run dev server
hot?: boolean; // enable hot reloading
open?: boolean; // open browser
network?: Network; // network to use
gateway?: string | boolean; // path to custom gateway dist, or false to disable
gateway?: boolean; // path to custom gateway dist, or false to disable
index?: string; // widget to use as index
output?: string; // output directory
};

export type GatewayConfig = {
enabled: boolean;
tagName: string;
bundleUrl: string;
};

/**
* Build and watch app according to bos.config.json
*
Expand All @@ -38,8 +50,10 @@ export async function dev(src: string, dest: string, opts: DevOptions) {
const dist = path.join(src, dest);
const devJsonPath = path.join(dist, "bos-loader.json");

// Build the app for the first time
// Build the app for the first timo

const config = await loadConfig(src, opts.network);

let devJson = await generateApp(src, dist, config, opts);
await writeJson(devJsonPath, devJson);

Expand All @@ -55,7 +69,10 @@ export async function dev(src: string, dest: string, opts: DevOptions) {
appDevJsonPath = devJsonPath;
opts.output = dist;
appDevOptions = opts;
const server = startDevServer(appSrcs, appDists, appDevJsonPath, appDevOptions);

const gatewayObject: GatewayConfig = buildGatewayObject(opts.gateway, config.gateway)

const server = startDevServer(appSrcs, appDists, appDevJsonPath, appDevOptions, gatewayObject);

// Start the socket server if hot reload is enabled
if (opts.hot) {
Expand Down Expand Up @@ -236,3 +253,22 @@ async function generateDevJson(src: string, config: BaseConfig): Promise<DevJson

return devJson;
}

export function buildGatewayObject(commandGateway, configGateway) {
// Gateway logic:
// if --no-gateway is provided (== commandGateway = false), gateway should be disabled ("");
// if --no-gateway is not provided, gateway is enabled, takes bundleUrl and tagName from `bos.config.json`
// if --no-gateway is not provided but there's no gateway configuration in `bos.config.json`, gateway should be the default gateway object;
const gatewayObject = DEFAULT_GATEWAY;

gatewayObject.enabled = commandGateway;

if (configGateway?.bundleUrl && configGateway?.tagName) {
gatewayObject.bundleUrl = configGateway.bundleUrl;
gatewayObject.tagName = configGateway.tagName;
}

gatewayObject.bundleUrl = gatewayObject.bundleUrl.replace(/\/$/, ''); // remove trailing slash

return gatewayObject
}
6 changes: 3 additions & 3 deletions lib/gateway.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import { DevOptions } from "./dev";
import { DevOptions, GatewayConfig } from "./dev";

import { JSDOM } from "jsdom";

Expand Down Expand Up @@ -29,7 +29,7 @@ function normalizeHtml(html) {
return html.replace(/\s+/g, ' ').trim();
}

export function modifyIndexHtml(content: string, opts: DevOptions, dependencies: string[]) {
export function modifyIndexHtml(content: string, opts: DevOptions, dependencies: string[], gateway: GatewayConfig) {
const dom = new JSDOM(content);
const document = dom.window.document;

Expand All @@ -41,7 +41,7 @@ export function modifyIndexHtml(content: string, opts: DevOptions, dependencies:
document.head.appendChild(script);
});

const elementTag = "near-social-viewer";
const elementTag = gateway.tagName;

// Create and configure the near-social-viewer element
const container = document.getElementById("bw-root");
Expand Down
56 changes: 27 additions & 29 deletions lib/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DevJson, DevOptions, addApps } from '@/lib/dev';
import { DEFAULT_GATEWAY, DevJson, DevOptions, GatewayConfig, addApps } from '@/lib/dev';
import { fetchJson } from "@near-js/providers";
import axios from 'axios';
import bodyParser from "body-parser";
Expand All @@ -14,8 +14,6 @@ import { readFile, readJson, promises } from "./utils/fs";
// the gateway dist path in node_modules
export const DEFAULT_LOCAL_GATEWAY_PATH = path.join(__dirname, "../..", "gateway", "dist");

export const DEFAULT_REMOTE_GATEWAY_URL = "https://ipfs.web4.near.page/ipfs/bafybeibe63hqugbqr4writdxgezgl5swgujay6t5uptw2px7q63r7crk2q/";

const httpsAgent = new https.Agent({
secureProtocol: 'TLSv1_2_method'
});
Expand Down Expand Up @@ -48,12 +46,13 @@ export const SOCIAL_CONTRACT = {
* Starts the dev server
* @param devJsonPath path to json redirect map
* @param opts DevOptions
* @param gateway gateway
* @returns http server
*/
export function startDevServer(srcs: string[], dists: string[], devJsonPath: string, opts: DevOptions): http.Server {
const app = createApp(devJsonPath, opts);
export function startDevServer(srcs: string[], dists: string[], devJsonPath: string, opts: DevOptions, gateway: GatewayConfig = DEFAULT_GATEWAY): http.Server {
const app = createApp(devJsonPath, opts, gateway);
const server = http.createServer(app);
startServer(server, opts, () => {
startServer(server, opts, gateway, () => {
const postData = JSON.stringify({ srcs: srcs.map((src) => path.resolve(src)), dists: dists.map((dist) => path.resolve(dist)) });
const options = {
hostname: '127.0.0.1',
Expand Down Expand Up @@ -96,8 +95,9 @@ export function startDevServer(srcs: string[], dists: string[], devJsonPath: str
* (separated out to enable endpoint testing)
* @param opts
* @param devJsonPath
* @param gateway
*/
export function createApp(devJsonPath: string, opts: DevOptions): Express.Application {
export function createApp(devJsonPath: string, opts: DevOptions, gateway: GatewayConfig): Express.Application {
const app = express();

log.success("HTTP server setup successfully.");
Expand Down Expand Up @@ -220,18 +220,15 @@ export function createApp(devJsonPath: string, opts: DevOptions): Express.Applic
*/
app.all('/api/proxy-rpc', proxyMiddleware(RPC_URL[opts.network]));

if (opts.gateway) {
if (gateway.enabled) {
log.debug("Setting up gateway...");
if (opts.index) {

log.debug("Index provided. Using new gateway setup.");

// use new path
let gatewayUrl = typeof opts.gateway === 'string' ? opts.gateway : DEFAULT_REMOTE_GATEWAY_URL;
const isLocalPath = !gatewayUrl.startsWith('http');
gatewayUrl = gatewayUrl.replace(/\/$/, ''); // remove trailing slash
opts.gateway = gatewayUrl; // standardize to url string
const isLocalPath = !gateway.bundleUrl.startsWith('http');

initializeGateway(gatewayUrl, isLocalPath, opts, devJsonPath);
initializeGateway(gateway, isLocalPath, opts, devJsonPath);

// Middleware to ensure gateway is initialized before handling requests
app.use(async (req, res, next) => {
Expand All @@ -255,7 +252,7 @@ export function createApp(devJsonPath: string, opts: DevOptions): Express.Applic
log.debug(`Request for: ${req.path}`);

if (isLocalPath) {
const fullUrl = path.join(__dirname, gatewayUrl, req.path);
const fullUrl = path.join(__dirname, gateway.bundleUrl, req.path);

try {
log.debug(`Attempting to serve file from local path: ${fullUrl}`);
Expand All @@ -273,9 +270,9 @@ export function createApp(devJsonPath: string, opts: DevOptions): Express.Applic
}
}
} else {
log.debug(`Proxying request to: ${gatewayUrl}${req.path}`);
log.debug(`Proxying request to: ${gateway.bundleUrl}${req.path}`);
// Proxy the request to the remote gateway
proxy.web(req, res, { target: `${gatewayUrl}${req.path}`, agent: httpsAgent });
proxy.web(req, res, { target: `${gateway.bundleUrl}${req.path}`, agent: httpsAgent });
}
} else {
// what about images?
Expand Down Expand Up @@ -335,8 +332,8 @@ export function createApp(devJsonPath: string, opts: DevOptions): Express.Applic
return app;
}

function initializeGateway(gatewayUrl: string, isLocalPath: boolean, opts: DevOptions, devJsonPath: string) {
gatewayInitPromise = setupGateway(gatewayUrl, isLocalPath, opts, devJsonPath)
function initializeGateway(gateway: GatewayConfig, isLocalPath: boolean, opts: DevOptions, devJsonPath: string) {
gatewayInitPromise = setupGateway(gateway, isLocalPath, opts, devJsonPath)
.then(() => {
log.success("Gateway initialized successfully.");
})
Expand All @@ -346,12 +343,12 @@ function initializeGateway(gatewayUrl: string, isLocalPath: boolean, opts: DevOp
});
}

async function setupGateway(gatewayUrl: string, isLocalPath: boolean, opts: DevOptions, devJsonPath: string) {
log.debug(`Setting up ${isLocalPath ? "local " : ""}gateway: ${gatewayUrl}`);
async function setupGateway(gateway: GatewayConfig, isLocalPath: boolean, opts: DevOptions, devJsonPath: string) {
log.debug(`Setting up ${isLocalPath ? "local " : ""}gateway: ${gateway.bundleUrl}`);

const manifestUrl = isLocalPath
? path.join(gatewayUrl, "/asset-manifest.json")
: `${gatewayUrl}/asset-manifest.json`;
? path.join(gateway.bundleUrl, "/asset-manifest.json")
: `${gateway.bundleUrl}/asset-manifest.json`;

try {
log.debug(`Fetching manifest from: ${manifestUrl}`);
Expand All @@ -360,8 +357,8 @@ async function setupGateway(gatewayUrl: string, isLocalPath: boolean, opts: DevO
log.debug(`Received manifest. Modifying HTML...`);
const htmlContent = await readFile(path.join(__dirname, '../../public/index.html'), 'utf8');

const dependencies = manifest.entrypoints.map((entrypoint: string) => isLocalPath ? `${entrypoint}` : `${gatewayUrl}/${entrypoint}`);
modifiedHtml = modifyIndexHtml(htmlContent, opts, dependencies);
const dependencies = manifest.entrypoints.map((entrypoint: string) => isLocalPath ? `${entrypoint}` : `${gateway.bundleUrl}/${entrypoint}`);
modifiedHtml = modifyIndexHtml(htmlContent, opts, dependencies, gateway);

// log.debug(`Importing packages...`); <-- this used jpsm to create import map for wallet selector
// modifiedHtml = await importPackages(modifiedHtml); // but didn't want it to run each time dev server started, so commented out
Expand Down Expand Up @@ -402,10 +399,11 @@ async function fetchManifest(url: string): Promise<any> {
* Starts BosLoader Server and optionally opens gateway in browser
* @param server http server
* @param opts DevOptions
* @param gateway gateway object
*/
export function startServer(server, opts, sendAddApps) {
export function startServer(server, opts, gateway, sendAddApps) {
server.listen(opts.port, "127.0.0.1", () => {
if (opts.gateway && opts.open) {
if (gateway.enabled && opts.open) {
// open gateway in browser
let start =
process.platform == "darwin"
Expand All @@ -419,7 +417,7 @@ export function startServer(server, opts, sendAddApps) {
log.log(`
┌─────────────────────────────────────────────────────────────┐
│ BosLoader Server is Up and Running │
│ │${opts.gateway
│ │${gateway.enabled
? `
│ ➜ Local Gateway: \u001b[32mhttp://127.0.0.1:${opts.port}\u001b[0m │`
: ""
Expand Down Expand Up @@ -451,4 +449,4 @@ export function startServer(server, opts, sendAddApps) {
process.exit(1);
}
});
}
}
8 changes: 5 additions & 3 deletions tests/unit/dev.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { buildApp } from "@/lib/build";
import { DEFAULT_CONFIG, loadConfig } from "@/lib/config";
import { dev, DevOptions, addApps } from "@/lib/dev";
import { dev, DevOptions, addApps, DEFAULT_GATEWAY } from "@/lib/dev";
import { Logger, LogLevel } from "@/lib/logger";
import { startDevServer } from "@/lib/server";
import { startSocket } from "@/lib/socket";
Expand Down Expand Up @@ -56,11 +56,13 @@ describe("dev", () => {
expect(loadConfig).toHaveBeenCalledWith(mockSrc, mockOpts.network);
});

it("should call generateApp with src, dist, config, opts, and devJsonPath", async () => {
it("should call generateApp with src, dist, config, opts, gateway, and devJsonPath", async () => {
await dev(mockSrc, "build", mockOpts);
const mockDist = path.join(mockSrc, 'build');
const mockDevJsonPath = path.join(mockSrc, 'build', 'bos-loader.json');
expect(startDevServer).toHaveBeenCalledWith([mockSrc], [mockDist], mockDevJsonPath, mockOpts);
const mockGateway = DEFAULT_GATEWAY;

expect(startDevServer).toHaveBeenCalledWith([mockSrc], [mockDist], mockDevJsonPath, mockOpts, mockGateway);
});

it("should start the socket server if hot reload is enabled", async () => {
Expand Down
Loading
Loading