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

feat: @deephaven/jsapi-nodejs npm package #2260

Merged
merged 5 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
667 changes: 667 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"@deephaven/iris-grid": "file:packages/iris-grid",
"@deephaven/jsapi-bootstrap": "file:packages/jsapi-bootstrap",
"@deephaven/jsapi-components": "file:packages/jsapi-components",
"@deephaven/jsapi-nodejs": "file:packages/jsapi-nodejs",
"@deephaven/jsapi-shim": "file:packages/jsapi-shim",
"@deephaven/jsapi-types": "^1.0.0-dev0.34.0",
"@deephaven/jsapi-utils": "file:packages/jsapi-utils",
Expand Down
32 changes: 32 additions & 0 deletions packages/jsapi-nodejs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# @deephaven/jsapi-components

Deephaven utils for consuming Jsapi from a server from a nodejs app. It can
optionally convert the server module format from `ESM` -> `CJS` or `CJS` -> `ESM`
if the server and consumer don't use the same module format.

## Install

```bash
npm install --save @deephaven/jsapi-nodejs
```

## Usage

```typescript
import fs from 'node:fs';
import path from 'node:path';

import { loadModules } from '@deephaven/jsapi-nodejs';

const tmpDir = path.join(__dirname, 'tmp');

// Download jsapi `ESM` files from DH Community server and export as `CJS` module.
const dhc = await loadModules({
serverUrl: new URL('http://localhost:10000'),
serverPaths: ['jsapi/dh-core.js', 'jsapi/dh-internal.js'],
download: true,
storageDir: tmpDir,
sourceModuleType: 'esm',
targetModuleType: 'cjs',
});
```
38 changes: 38 additions & 0 deletions packages/jsapi-nodejs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@deephaven/jsapi-nodejs",
"version": "0.96.0",
"description": "Deephaven utils for consuming Jsapi from a server",
"author": "Deephaven Data Labs LLC",
"license": "Apache-2.0",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/deephaven/web-client-ui.git",
"directory": "packages/jsapi-nodejs"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"source": "src/index.ts",
"engines": {
"node": ">=16"
},
"scripts": {
"build": "cross-env NODE_ENV=production run-p build:*",
"build:babel": "babel ./src --out-dir ./dist --extensions \".ts,.tsx,.js,.jsx\" --source-maps --root-mode upward"
},
"dependencies": {
"esbuild": "^0.24.0",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/node": "^22.7.5",
"@types/ws": "^8.5.12"
},
"files": [
"dist"
],
"sideEffects": false,
"publishConfig": {
"access": "public"
}
}
34 changes: 34 additions & 0 deletions packages/jsapi-nodejs/src/errorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Return true if given error has a code:string prop. Optionally check if the
* code matches a given value.
* @param err Error to check
* @param code Optional code to check
*/
export function hasErrorCode(
err: unknown,
code?: string
): err is { code: string } {
if (
err != null &&
typeof err === 'object' &&
'code' in err &&
typeof err.code === 'string'
) {
return code == null || err.code === code;
}

return false;
}

/**
* Returns true if the given error is an AggregateError. Optionally checks if
* a given code matches the error's code.
* @param err Error to check
* @param code Optional code to check
*/
export function isAggregateError(
err: unknown,
code?: string
): err is { code: string } {
return hasErrorCode(err, code) && String(err) === 'AggregateError';
}
29 changes: 29 additions & 0 deletions packages/jsapi-nodejs/src/fsUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import fs from 'node:fs';
import path from 'node:path';

/**
* Create directories if they do not exist.
* @param dirPaths The paths of the directories to create.
*/
export function ensureDirectoriesExist(dirPaths: string[]): void {
dirPaths.forEach(dirPath => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
});
}

/**
* Get download paths for a list of server paths.
* @param targetDir The directory to download the files to.
* @param serverPaths The paths of the files on the server.
* @returns The paths to download the files to.
*/
export function getDownloadPaths(
targetDir: string,
serverPaths: string[]
): string[] {
return serverPaths.map(filePath =>
path.join(targetDir, path.basename(filePath))
);
}
5 changes: 5 additions & 0 deletions packages/jsapi-nodejs/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './errorUtils';
export * from './fsUtils';
export * from './loaderUtils';
export * from './polyfillWs';
export * from './serverUtils';
95 changes: 95 additions & 0 deletions packages/jsapi-nodejs/src/loaderUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import fs from 'node:fs';
import path from 'node:path';
import esbuild from 'esbuild';

import { downloadFromURL, urlToDirectoryName } from './serverUtils';
import { polyfillWs } from './polyfillWs';
import { ensureDirectoriesExist, getDownloadPaths } from './fsUtils';

type NonEmptyArray<T> = [T, ...T[]];

/**
* Load a list of modules from a server.
* @param serverUrl The URL of the server to load from.
* @param serverPaths The paths of the modules on the server.
* @param download Whether to download the modules from the server. If set to false,
* it's assumed that the modules have already been downloaded and still exist in
* the storage directory.
* @param storageDir The directory to store the downloaded modules.
* @param sourceModuleType module format from the server.
* @param targetFormat (optional) format to be exported. Defaults to
* sourceModuleType.
* @returns The default export of the first module in `serverPaths`.
*/
export async function loadModules<TMainModule>({
serverUrl,
serverPaths,
download,
storageDir,
sourceModuleType,
targetModuleType = sourceModuleType,
}: {
serverUrl: URL;
serverPaths: NonEmptyArray<string>;
download: boolean;
storageDir: string;
sourceModuleType: 'esm' | 'cjs';
targetModuleType?: 'esm' | 'cjs';
}): Promise<TMainModule> {
polyfillWs();

const serverStorageDir = path.join(storageDir, urlToDirectoryName(serverUrl));
const targetDir = path.join(serverStorageDir, 'target');

if (download) {
const needsTranspile = sourceModuleType !== targetModuleType;
const sourceDir = path.join(serverStorageDir, 'source');

ensureDirectoriesExist(
needsTranspile ? [sourceDir, targetDir] : [targetDir]
);

// Download from server
const serverUrls = serverPaths.map(
serverPath => new URL(serverPath, serverUrl)
);
const contents = await Promise.all(
serverUrls.map(url => downloadFromURL(url))
);

// Write to disk
const downloadPaths = getDownloadPaths(
needsTranspile ? sourceDir : targetDir,
serverPaths
);
downloadPaths.forEach((downloadPath, i) => {
fs.writeFileSync(downloadPath, contents[i]);
});

// Transpile if source and target module types differ
if (needsTranspile) {
await esbuild.build({
entryPoints: downloadPaths,
bundle: false,
format: targetModuleType,
logLevel: 'error',
platform: 'node',
outdir: targetDir,
});
}
}

// We assume the first module is the main module
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const firstFileName = serverPaths[0].split('/').pop()!;
const mainModulePath = path.join(targetDir, firstFileName);

if (targetModuleType === 'esm') {
return import(mainModulePath);
}

// eslint-disable-next-line import/no-dynamic-require, global-require
return require(mainModulePath);
}

export default loadModules;
11 changes: 11 additions & 0 deletions packages/jsapi-nodejs/src/polyfillWs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ws from 'ws';

export function polyfillWs(): void {
if (globalThis.WebSocket == null) {
// Newer versions of NodeJS should eventually have first-class support for
// WebSocket, but for older versions as late as v20, we need to polyfill it.
globalThis.WebSocket = ws as unknown as (typeof globalThis)['WebSocket'];
}
}

export default polyfillWs;
Loading
Loading