diff --git a/packages/browser/README.md b/packages/browser/README.md index c95e7db..3024062 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -51,8 +51,13 @@ See [benchmark details](https://github.com/lightpanda-io/demo)._ ## Getting Started +### Configuration +_Environment variables_ +- `LIGHTPANDA_EXECUTABLE_PATH` can be specified if you want to use your own version and avoid the binary from being installed on postinstall. The default folder is `~/.cache/lightpanda-node` + + ### Install -_On installation, the binary corresponding to your platform will be automatically downloaded_ +_When installing the package, the binary corresponding to your platform will be automatically downloaded. If your OS is not supported, download will fail_ ```bash yarn add @lightpanda/browser @@ -72,7 +77,7 @@ pnpm add @lightpanda/browser At some point in time, you might want to upgrade Lightpanda browser to a more recent version. To do so, you can run the following command: ```bash -npx @lightpanda/browser +npx @lightpanda/browser upgrade ``` diff --git a/packages/browser/cli/install.ts b/packages/browser/cli/install.ts deleted file mode 100644 index 56b4b56..0000000 --- a/packages/browser/cli/install.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env node - -import packageJson from './../package.json' with { type: 'json' } -import { download } from './../src/download' - -download(`node_modules/${packageJson.name}/dist/lightpanda`) diff --git a/packages/browser/cli/main.ts b/packages/browser/cli/main.ts new file mode 100644 index 0000000..42dec01 --- /dev/null +++ b/packages/browser/cli/main.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' + +import { download } from '../src/download' + +yargs(hideBin(process.argv)) + .command( + 'upgrade', + 'Upgrade the browser to the latest nightly version', + () => {}, + _ => { + download() + }, + ) + .command( + '$0', + 'Default', + () => {}, + _ => { + console.info('ℹ️ Please enter a command') + }, + ) + .option('verbose', { + alias: 'v', + type: 'boolean', + description: 'Run with verbose logging', + }) + .parse() diff --git a/packages/browser/package.json b/packages/browser/package.json index efbada7..8c61a08 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -4,13 +4,13 @@ "description": "Lightpanda for Node.js", "main": "./dist/index.js", "module": "./dist/index.mjs", - "types": "./dist/types/index.d.ts", + "types": "./dist/index.d.ts", "type": "module", "scripts": { "dev": "tsup index.ts --target node22 --format cjs,esm --dts --watch", "build:index": "tsup index.ts --target node22 --format cjs,esm --minify terser", - "build:decl": "yarn dlx --package=typescript tsc --emitDeclarationOnly --declaration --declarationDir dist/types", - "build:cli": "tsup cli/install.ts --target node22 --format cjs,esm --minify terser -d dist/cli", + "build:decl": "yarn dlx --package=typescript tsc --emitDeclarationOnly --declaration", + "build:cli": "tsup cli/main.ts --target node22 --format cjs,esm --minify terser -d dist/cli", "build:postinstall": "tsup scripts/postinstall.ts --target node22 --format cjs,esm --minify terser -d dist/scripts", "build": "yarn build:index && yarn build:decl && yarn build:cli && yarn build:postinstall", "postinstall": "node dist/scripts/postinstall.js || true", @@ -18,7 +18,7 @@ "typecheck": "yarn dlx --package=typescript tsc --noEmit" }, "bin": { - "lightpanda": "./dist/cli/install.js" + "lightpanda": "./dist/cli/main.js" }, "repository": { "type": "git", @@ -38,8 +38,12 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@types/node": "22.15.32", + "@types/yargs": "^17.0.33", "terser": "^5.43.1", "tsup": "^8.5.0", "typescript": "^5.8.3" + }, + "dependencies": { + "yargs": "^18.0.0" } } diff --git a/packages/browser/src/download.ts b/packages/browser/src/download.ts index 2dcf289..b8acf21 100644 --- a/packages/browser/src/download.ts +++ b/packages/browser/src/download.ts @@ -1,10 +1,7 @@ -import { constants, chmodSync, createWriteStream } from 'node:fs' +import { constants, chmodSync, createWriteStream, existsSync, mkdirSync } from 'node:fs' import https from 'node:https' import { arch, exit, platform } from 'node:process' - -const FOLDER = 'dist' -const BINARY_NAME = 'lightpanda' -const BINARY_PATH = `${FOLDER}/${BINARY_NAME}` +import { DEFAULT_CACHE_FOLDER, DEFAULT_EXECUTABLE_PATH, USER_EXECUTABLE_PATH } from './utils' const PLATFORMS = { darwin: { @@ -21,11 +18,15 @@ const PLATFORMS = { * Download Lightpanda's binary * @returns {Promise} */ -export const download = async (binaryPath: string = BINARY_PATH): Promise => { - const path = PLATFORMS?.[platform]?.[arch] +export const download = async (): Promise => { + const platformPath = PLATFORMS?.[platform]?.[arch] + + if (!existsSync(DEFAULT_CACHE_FOLDER)) { + mkdirSync(DEFAULT_CACHE_FOLDER, { recursive: true }) + } const get = (url: string, resolve: (value?: unknown) => void, reject: (reason: any) => void) => { - const file = createWriteStream(binaryPath) + const file = createWriteStream(DEFAULT_EXECUTABLE_PATH) https.get(url, res => { if ( @@ -50,20 +51,25 @@ export const download = async (binaryPath: string = BINARY_PATH): Promise return new Promise((resolve, reject) => get(url, resolve, reject)) } - if (path) { + if (platformPath) { + if (USER_EXECUTABLE_PATH) { + console.info('$LIGHTPANDA_EXECUTABLE_PATH found, skipping binary download…') + exit(0) + } + try { console.info('⏳ Downloading latest version of Lightpanda browser…') await downloadBinary( - `https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-${path}`, + `https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-${platformPath}`, ) - chmodSync(binaryPath, constants.S_IRWXU) + chmodSync(DEFAULT_EXECUTABLE_PATH, constants.S_IRWXU) - console.info('✅ Download finished!') + console.info('✅ Done!') exit(0) } catch (e) { console.log('error', e) - console.warn(`Lightpanda's failed to download the binary file "${path}".`) + console.warn(`Lightpanda's failed to download the binary file "${platformPath}".`) exit(1) } } else { diff --git a/packages/browser/src/fetch.ts b/packages/browser/src/fetch.ts index 37cd9e1..9b0175a 100644 --- a/packages/browser/src/fetch.ts +++ b/packages/browser/src/fetch.ts @@ -1,5 +1,5 @@ import { execSync } from 'node:child_process' -import { validateUrl } from './utils' +import { getExecutablePath, validateUrl } from './utils' /** * @typedef LightpandaFetchOptions * @type {object} @@ -33,6 +33,7 @@ export const fetch = (url: string, options: LightpandaFetchOptions = defaultOpti return new Promise((resolve, reject) => { try { + const executablePath = getExecutablePath() const flags = [ { flag: '--dump', condition: dump }, { flag: '--insecure_disable_tls_host_verification', condition: disableHostVerification }, @@ -41,7 +42,7 @@ export const fetch = (url: string, options: LightpandaFetchOptions = defaultOpti .map(f => (f.condition ? f.flag : '')) .join(' ') - const e = execSync(`./lightpanda fetch ${flags} ${url}`) + const e = execSync(`${executablePath} fetch ${flags} ${url}`) if (dump) { resolve(e.toString()) diff --git a/packages/browser/src/serve.ts b/packages/browser/src/serve.ts index 4c90125..c4d608e 100644 --- a/packages/browser/src/serve.ts +++ b/packages/browser/src/serve.ts @@ -1,5 +1,5 @@ import { type ChildProcessWithoutNullStreams, spawn } from 'node:child_process' -import { validatePort, validateUrl } from './utils' +import { getExecutablePath, validatePort, validateUrl } from './utils' /** * @typedef LightpandaServeOptions * @type {object} @@ -38,6 +38,7 @@ export const serve = (options: LightpandaServeOptions = defaultOptions) => { } return new Promise((resolve, reject) => { + const executablePath = getExecutablePath() const flags = [ { flag: '--host', value: host }, { flag: '--port', value: port }, @@ -52,7 +53,7 @@ export const serve = (options: LightpandaServeOptions = defaultOptions) => { .flatMap(f => (f.value ? [f.flag, !f.flagOnly ? f.value.toString() : ''] : '')) .filter(f => f !== '') - const process = spawn('./lightpanda', ['serve', ...flags]) + const process = spawn(executablePath, ['serve', ...flags]) process.on('spawn', async () => { console.info("🐼 Running Lightpanda's CDP server…", { diff --git a/packages/browser/src/utils.ts b/packages/browser/src/utils.ts index 55a6728..02847c3 100644 --- a/packages/browser/src/utils.ts +++ b/packages/browser/src/utils.ts @@ -1,3 +1,15 @@ +import os from 'node:os' + +export const DEFAULT_CACHE_FOLDER = `${os.homedir()}/.cache/lightpanda-node` +export const BINARY_NAME = 'lightpanda' + +export const USER_EXECUTABLE_PATH = process.env.LIGHTPANDA_EXECUTABLE_PATH +export const DEFAULT_EXECUTABLE_PATH = `${DEFAULT_CACHE_FOLDER}/${BINARY_NAME}` + +/** + * Validate a URL structure + * @param {string} url URL to validate + */ export const validateUrl = (url: string): void => { if (!url || typeof url !== 'string') { throw new Error(`URL is required and must be a string ${url}`) @@ -14,6 +26,10 @@ export const validateUrl = (url: string): void => { } } +/** + * Validate a port number + * @param {number} port Port number to validate + */ export const validatePort = (port: number): void => { if (!port || typeof port !== 'number') { throw new Error(`Port is required and must be a number ${port}`) @@ -23,3 +39,10 @@ export const validatePort = (port: number): void => { throw new Error(`Port should be a positive number ${port}`) } } + +/** + * Get executable path + */ +export const getExecutablePath = () => { + return USER_EXECUTABLE_PATH ?? DEFAULT_EXECUTABLE_PATH +} diff --git a/yarn.lock b/yarn.lock index 891cbfb..c63085b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -613,11 +613,13 @@ __metadata: dependencies: "@biomejs/biome": "npm:^1.9.4" "@types/node": "npm:22.15.32" + "@types/yargs": "npm:^17.0.33" terser: "npm:^5.43.1" tsup: "npm:^8.5.0" typescript: "npm:^5.8.3" + yargs: "npm:^18.0.0" bin: - lightpanda: ./dist/cli/install.js + lightpanda: ./dist/cli/main.js languageName: unknown linkType: soft @@ -876,6 +878,22 @@ __metadata: languageName: node linkType: hard +"@types/yargs-parser@npm:*": + version: 21.0.3 + resolution: "@types/yargs-parser@npm:21.0.3" + checksum: 10c0/e71c3bd9d0b73ca82e10bee2064c384ab70f61034bbfb78e74f5206283fc16a6d85267b606b5c22cb2a3338373586786fed595b2009825d6a9115afba36560a0 + languageName: node + linkType: hard + +"@types/yargs@npm:^17.0.33": + version: 17.0.33 + resolution: "@types/yargs@npm:17.0.33" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: 10c0/d16937d7ac30dff697801c3d6f235be2166df42e4a88bf730fa6dc09201de3727c0a9500c59a672122313341de5f24e45ee0ff579c08ce91928e519090b7906b + languageName: node + linkType: hard + "abbrev@npm:^3.0.0": version: 3.0.1 resolution: "abbrev@npm:3.0.1" @@ -929,7 +947,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0": +"ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c @@ -1068,6 +1086,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^9.0.1": + version: 9.0.1 + resolution: "cliui@npm:9.0.1" + dependencies: + string-width: "npm:^7.2.0" + strip-ansi: "npm:^7.1.0" + wrap-ansi: "npm:^9.0.0" + checksum: 10c0/13441832e9efe7c7a76bd2b8e683555c478d461a9f249dc5db9b17fe8d4b47fa9277b503914b90bd00e4a151abb6b9b02b2288972ffe2e5e3ca40bcb1c2330d3 + languageName: node + linkType: hard + "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -1172,6 +1201,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^10.3.0": + version: 10.4.0 + resolution: "emoji-regex@npm:10.4.0" + checksum: 10c0/a3fcedfc58bfcce21a05a5f36a529d81e88d602100145fcca3dc6f795e3c8acc4fc18fe773fbf9b6d6e9371205edb3afa2668ec3473fa2aa7fd47d2a9d46482d + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -1308,6 +1344,13 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.1.1": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65 + languageName: node + linkType: hard + "esprima@npm:^4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -1467,6 +1510,20 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + +"get-east-asian-width@npm:^1.0.0": + version: 1.3.0 + resolution: "get-east-asian-width@npm:1.3.0" + checksum: 10c0/1a049ba697e0f9a4d5514c4623781c5246982bdb61082da6b5ae6c33d838e52ce6726407df285cdbb27ec1908b333cf2820989bd3e986e37bb20979437fdf34b + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -2482,6 +2539,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^7.0.0, string-width@npm:^7.2.0": + version: 7.2.0 + resolution: "string-width@npm:7.2.0" + dependencies: + emoji-regex: "npm:^10.3.0" + get-east-asian-width: "npm:^1.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9 + languageName: node + linkType: hard + "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -2491,7 +2559,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": version: 7.1.0 resolution: "strip-ansi@npm:7.1.0" dependencies: @@ -2896,6 +2964,24 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^9.0.0": + version: 9.0.0 + resolution: "wrap-ansi@npm:9.0.0" + dependencies: + ansi-styles: "npm:^6.2.1" + string-width: "npm:^7.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10c0/a139b818da9573677548dd463bd626a5a5286271211eb6e4e82f34a4f643191d74e6d4a9bb0a3c26ec90e6f904f679e0569674ac099ea12378a8b98e20706066 + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -2909,3 +2995,24 @@ __metadata: checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 languageName: node linkType: hard + +"yargs-parser@npm:^22.0.0": + version: 22.0.0 + resolution: "yargs-parser@npm:22.0.0" + checksum: 10c0/cb7ef81759c4271cb1d96b9351dbbc9a9ce35d3e1122d2b739bf6c432603824fa02c67cc12dcef6ea80283379d63495686e8f41cc7b06c6576e792aba4d33e1c + languageName: node + linkType: hard + +"yargs@npm:^18.0.0": + version: 18.0.0 + resolution: "yargs@npm:18.0.0" + dependencies: + cliui: "npm:^9.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + string-width: "npm:^7.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^22.0.0" + checksum: 10c0/bf290e4723876ea9c638c786a5c42ac28e03c9ca2325e1424bf43b94e5876456292d3ed905b853ebbba6daf43ed29e772ac2a6b3c5fb1b16533245d6211778f3 + languageName: node + linkType: hard