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
Merged
15 changes: 9 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,10 @@ 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:

```cmd
bw dev -g path/to/dist
```
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 and specify the gateway url and the tag name in the `bos.config.json` file.
bb-face marked this conversation as resolved.
Show resolved Hide resolved

```cmd
bw dev -g https://ipfs.web4.near.page/ipfs/bafybeiancp5im5nfkdki3cfvo7ownl2knjovqh7bseegk4zvzsl4buryoi
bw dev -g
bb-face marked this conversation as resolved.
Show resolved Hide resolved
```

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"
}
}
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 { GatewayConfigObject } 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?: GatewayConfigObject // gateway config object
bb-face marked this conversation as resolved.
Show resolved Hide resolved
}

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
41 changes: 38 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 | Gaze = 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 GatewayConfigObject = {
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: GatewayConfigObject = 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,21 @@ async function generateDevJson(src: string, config: BaseConfig): Promise<DevJson

return devJson;
}

export function buildGatewayObject(commandGateway, configGateway) {
// Gateway logic:
// if --no-gateway is provided (or commandGateway = false), gateway should be disabled ("");
// if -g is true, gateway is enabled, takes bundleUrl and tagName from `bos.config.json`
bb-face marked this conversation as resolved.
Show resolved Hide resolved
// if noone of the above option are specified, gateway should be the default gateway object;
const gatewayObject = DEFAULT_GATEWAY;

gatewayObject.enabled = commandGateway;

if (configGateway?.bundleUrl)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering we have a default config, ability to override this config, or --no-gateway to disable it, I'm not entirely sure what this function does anymore. Take a step back and reassess it's purpose and if it's clearer to just check if gateway is disabled or not

Although we do need error checking -- we can't have bundleUrl without tagName and can't have tagName without bundleUrl

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(and let's have a test to confirm both must be populated)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error checking is in config.ts in the configuration schema, if I try to run the app without a gateway with both tagName and bundleUrl it will return this error:

(node:20505) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
✗ SyntaxError: bos.config.json: Expected double-quoted property name in JSON at position 421 (line 14 column 2)
    at JSON.parse (<anonymous>)
    at _readFile (/Users/stefano/Workspace/Work/NEAR/bos-workspace/node_modules/jsonfile/index.js:25:16)

Without the option -g the logic is simpler but still needs to be expressed somewhere, that's the purpose of that function, somehow we need to specify the logic of the gateway: if it's disabled, if it's enabled but without a configuration object in bos.config.json or if it's enabled with a correctly formatted configuration object!

Let me know if you want to introduce more error checking on top of the Joi schema or if I need to move the gateway logic somewhere else!

gatewayObject.bundleUrl = configGateway.bundleUrl;
if (configGateway?.tagName) 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, GatewayConfigObject } 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: GatewayConfigObject) {
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
62 changes: 34 additions & 28 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, GatewayConfigObject, 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: GatewayConfigObject = 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: GatewayConfigObject = DEFAULT_GATEWAY): Express.Application {
const app = express();
bb-face marked this conversation as resolved.
Show resolved Hide resolved

log.success("HTTP server setup successfully.");
Expand Down Expand Up @@ -220,18 +220,23 @@ 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) {

// use near-bos-webcomponent locallyl
// use as gateway the dist bundle in index.html;
bb-face marked this conversation as resolved.
Show resolved Hide resolved
// change something like bacjkgrind color;
// check if the example here reflect the changes;
// this is a test to understand if -g/bos.config.json gateway optino works;


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

initializeGateway(gatewayUrl, isLocalPath, opts, devJsonPath);
const isLocalPath = !gateway.bundleUrl.startsWith('http');

const gatewayInitPromise = initializeGateway(gateway, isLocalPath, opts, devJsonPath);

bb-face marked this conversation as resolved.
Show resolved Hide resolved
// Middleware to ensure gateway is initialized before handling requests
app.use(async (req, res, next) => {
Expand All @@ -255,7 +260,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 +278,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}${req.path}`);
// Proxy the request to the remote gateway
bb-face marked this conversation as resolved.
Show resolved Hide resolved
proxy.web(req, res, { target: `${gatewayUrl}${req.path}`, agent: httpsAgent });
proxy.web(req, res, { target: `${gateway}${req.path}`, agent: httpsAgent });
}
} else {
// what about images?
Expand Down Expand Up @@ -335,8 +340,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: GatewayConfigObject, isLocalPath: boolean, opts: DevOptions, devJsonPath: string) {
return setupGateway(gateway, isLocalPath, opts, devJsonPath)
.then(() => {
bb-face marked this conversation as resolved.
Show resolved Hide resolved
log.success("Gateway initialized successfully.");
})
Expand All @@ -346,12 +351,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: GatewayConfigObject, 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 +365,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 +407,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 +425,7 @@ export function startServer(server, opts, sendAddApps) {
log.log(`
┌─────────────────────────────────────────────────────────────┐
│ BosLoader Server is Up and Running │
│ │${opts.gateway
│ │${gateway.bundleUrl
? `
bb-face marked this conversation as resolved.
Show resolved Hide resolved
│ ➜ Local Gateway: \u001b[32mhttp://127.0.0.1:${opts.port}\u001b[0m │`
: ""
Expand Down Expand Up @@ -451,4 +457,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 @@ -57,11 +57,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