@simplito/webendpoint-manager
Version:
ClI tool for managing PrivMX Webendpoint assets
407 lines (346 loc) • 12.3 kB
text/typescript
#!/usr/bin/env node
import { confirm, input } from '@inquirer/prompts';
import { join, resolve as pathResolve } from 'path';
import { rm } from 'node:fs/promises';
import { existsSync, readFileSync, createWriteStream, renameSync, mkdirSync, rmSync } from 'fs';
import { Readable } from 'stream';
import * as cp from 'child_process';
import * as UUID from 'uuid';
// @ts-ignore
import AdmZip from 'adm-zip';
import ora from 'ora';
type ProjectType = {
name: string;
matchFn: (pckgJson: PackageJsonDependencies) => Promise<boolean>;
setup: (assetsPath: string) => Promise<void>;
};
interface AssetResult {
path: string;
isCompressed: boolean;
cleanup?: Function;
}
type AssetFetcher = (version: string, saveTo: string) => Promise<AssetResult>;
async function pressEnterToContinue() {
await input({
message: 'Press ENTER to continue'
});
}
async function getZipUrlOfLatest() {
const latestInfoUrl =
'https://api.github.com/repos/simplito/privmx-webendpoint/releases/latest';
const headers = {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
};
const zipInfo = await fetch(latestInfoUrl, { headers });
if (zipInfo.ok) {
return (await zipInfo.json()).zipball_url;
}
return null;
}
const fetchGithubRepository: AssetFetcher = async (version, saveTo) => {
const actualVersion = version ? version : (await getZipUrlOfLatest()).split('/').pop();
const versionName = actualVersion.startsWith('v') ? actualVersion : `v${actualVersion}`;
const link = `https://github.com/simplito/privmx-webendpoint/releases/download/${versionName}/privmx-webendpoint-${versionName}.zip`;
const fileName = link.split('/').pop() || 'wasm-assets.zip';
const resp = await fetch(link, { redirect: 'follow' });
const savedFilePath = join(saveTo, fileName);
if (existsSync(savedFilePath)) {
await rm(savedFilePath);
}
if (resp.ok && resp.body) {
const writer = createWriteStream(savedFilePath);
Readable.fromWeb(resp.body as any).pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
} else {
throw new Error('Unable to download assets aborting.');
}
return { path: savedFilePath, isCompressed: true };
};
const fetchNpmJSPackage: AssetFetcher = async (version, saveTo) => {
const versionName = version.startsWith('v') ? version.substring(1) : version;
const pkgName = `@simplito/privmx-webendpoint${version.length > 0 ? '@' + versionName : ''}`;
let tmpPkgDir: string = '';
while (true) {
tmpPkgDir = pathResolve(saveTo, 'tmp-' + UUID.v4());
if (!existsSync(tmpPkgDir)) {
break;
}
}
mkdirSync(tmpPkgDir, { recursive: true });
cp.execSync('npm init -y', { cwd: tmpPkgDir });
cp.execSync('npm i ' + pkgName, { cwd: tmpPkgDir });
const assetsPath = pathResolve(
tmpPkgDir,
'node_modules/@simplito/privmx-webendpoint/webAssets'
);
const licenseFile = pathResolve(assetsPath, 'LICENSE.md');
rmSync(licenseFile);
return {
path: assetsPath,
isCompressed: false,
cleanup: () => {
rmSync(tmpPkgDir, { recursive: true, force: true });
}
};
};
async function extractAssets(archivePath: string, extractPath: string): Promise<void> {
const assetsZip = new AdmZip(join(process.cwd(), archivePath));
assetsZip.getEntries().forEach((zipFile) => {
if (zipFile.name.endsWith('js') || zipFile.name.endsWith('.wasm')) {
assetsZip.extractEntryTo(zipFile, extractPath, false);
}
});
}
async function clearArchive(archivePath: string) {
if (existsSync(archivePath)) {
await rm(archivePath);
}
}
async function checkDestPath(destPath: string) {
if (existsSync(destPath)) {
const override = await confirm({
message: 'Assets already exists, override ?',
default: false
});
if (override) {
await rm(destPath, { recursive: true, force: true });
return false;
}
return true;
} else {
return false;
}
}
async function getLicenseConfirmation() {
const confirmation = await confirm({
message:
'PrivMX Endpoint and PrivMX Bridge are licensed under the PrivMX Free License.\n' +
' See the License for the specific language governing permissions and limitations under the License at https://privmx.dev/licensing, proceed ?',
default: true
});
return confirmation;
}
async function choseDestFolder() {
const destinationFolder = await input({
message:
'Please specify the destination folder for the assets (e.g., ./public/privmx-assets):',
default: './public/privmx-assets'
});
return destinationFolder;
}
async function setupNext(destinationFolder: string) {
console.log('\n-- Paste following script tags in to your root layout <head> tag --');
let withoutPublic = destinationFolder.replace('/public', '');
if (withoutPublic.startsWith('.')) {
withoutPublic = withoutPublic.slice(1);
}
console.log(`\x1b[34m
<Script src="${withoutPublic}/privmx-endpoint-web.js"></Script>
<Script src="${withoutPublic}/driver-web-context.js"></Script>
<Script src="${withoutPublic}/endpoint-wasm-module.js"></Script>\x1b[0m\n`);
await pressEnterToContinue();
console.log('-- Setup your next.config.mjs to handle specified headers --');
console.log(`\x1b[34m
/** @type {import("next").NextConfig} */
const nextConfig = {
headers: async () => {
return [
{
source: "/(.*)",
headers: [
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
],
},
];
},
};
export default nextConfig;
\x1b[0m
`);
}
async function setupVite(destinationFolder: string) {
console.log('\n-- Paste following script tags in to your index.html <head> tag --');
let withoutPublic = destinationFolder.replace('/public', '');
if (withoutPublic.startsWith('.')) {
withoutPublic = withoutPublic.slice(1);
}
console.log(`\x1b[34m
<script src="${withoutPublic}/privmx-endpoint-web.js"></script>
<script src="${withoutPublic}/driver-web-context.js"></script>
<script src="${withoutPublic}/endpoint-wasm-module.js"></script>\x1b[0m\n`);
await pressEnterToContinue();
console.log('-- Setup your vite.config.ts to handle specified headers --\n');
console.log(`\x1b[34m
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
{
name: "configure-response-headers",
configureServer: (server) => {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
next();
});
},
},
],
});
\x1b[0m
`);
}
type PackageJsonDependencies = [
dependencies: Record<string, string>,
devDependencies: Record<string, string>
];
function getProjectDependencies(): PackageJsonDependencies {
const packageJsonPath: string = join(process.cwd(), 'package.json');
if (!existsSync(packageJsonPath)) {
ora('No package.json found. Exiting...').fail();
process.exit(1);
}
let packageJson: any;
try {
const packageJsonContent = readFileSync(packageJsonPath, 'utf8');
packageJson = JSON.parse(packageJsonContent);
} catch (error) {
console.error('Error reading or parsing package.json:', error);
process.exit(1);
}
const dependencies: Record<string, string> = packageJson.dependencies || {};
const devDependencies: Record<string, string> = packageJson.devDependencies || {};
return [dependencies, devDependencies];
}
const PROJECT_TYPES = [
{
name: 'Vite',
matchFn: async (packageJson) => {
const [dep, devDep] = packageJson;
const isVite = 'vite' in dep || 'vite' in devDep;
return isVite;
},
setup: async (assetsPath) => {
await setupVite(assetsPath);
}
},
{
name: 'Next',
matchFn: async (packageJson) => {
const [dep, devDep] = packageJson;
const isNext = 'next' in dep || 'next' in devDep;
return isNext;
},
setup: async (assetsPath) => {
await setupNext(assetsPath);
}
}
] satisfies ProjectType[];
const DEFAULT_PROJECT_TYPE = {
name: 'Default',
matchFn: async () => {
console.log('Unknown project type');
return true;
},
setup: async () => {}
} satisfies ProjectType;
function extractParam(params: string[]): { version?: string; repository?: string } {
const args = {};
const repositoryArg = params.find((param) => param.startsWith('--repository'));
if (repositoryArg) {
Object.assign(args, { repository: repositoryArg.split('=')[1] });
}
const versionArg = params.find((param) => param.startsWith('--version'));
if (versionArg) {
Object.assign(args, { version: versionArg.split('=')[1] });
}
return args;
}
async function main() {
const params = extractParam(process.argv);
const projectDependencies = getProjectDependencies();
let userProjectType: ProjectType = DEFAULT_PROJECT_TYPE;
for (const projectType of PROJECT_TYPES) {
if (await projectType.matchFn(projectDependencies)) {
const answer = await confirm({
message: `Detected as ${projectType.name} project
Please confirm if detected properly: Y/N`
});
if (!answer) {
continue;
}
userProjectType = projectType;
break;
}
}
// get versions
// select version
const version = params.version ? params.version : '';
// select folder
const destFolder = await choseDestFolder();
//check if assets exists
const abort = await checkDestPath(destFolder);
if (abort) {
process.exit(0);
}
const isLicenseAccepted = await getLicenseConfirmation();
if (!isLicenseAccepted) {
ora('License not accepted, aborting...').fail();
process.exit(0);
}
// fetch assets
let fetcher: AssetFetcher;
const spinner = ora(
`Fetching assets of version: ${version.length > 0 ? version : 'latest'}`
).start();
if (params.repository && params.repository.startsWith('https://github.com/simplito')) {
fetcher = fetchGithubRepository;
} else {
fetcher = fetchNpmJSPackage;
}
try {
const fetchedAsset = await fetcher(version, './');
if (fetchedAsset.isCompressed) {
spinner.text = 'Extracting...';
await extractAssets(fetchedAsset.path, destFolder);
await clearArchive(fetchedAsset.path);
} else {
renameSync(fetchedAsset.path, destFolder);
if (fetchedAsset.cleanup) {
fetchedAsset.cleanup();
}
}
spinner.succeed(`Assets fetched to: ${destFolder}`);
await userProjectType.setup(destFolder);
} catch (e) {
if (e instanceof Error && e.name === 'ExitPromptError') {
spinner.fail('Aborting script...');
return;
}
spinner.fail(e.message || e);
}
// extract assets
}
main()
.then(() => {
ora('For more information please visit our documentation: https://docs.privmx.dev/').info();
})
.catch((e) => {
if (e instanceof Error && e.name === 'ExitPromptError') {
ora('Aborting script...').fail();
return;
}
console.error(e);
process.exit(1);
});