UNPKG

ardunno-cli-gen

Version:

Generates nice-grpc API for the Arduino CLI

294 lines (293 loc) 11.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.__test = void 0; const debug_1 = __importDefault(require("debug")); const https_proxy_agent_1 = require("https-proxy-agent"); const node_fs_1 = require("node:fs"); const node_https_1 = require("node:https"); const node_path_1 = require("node:path"); const node_url_1 = require("node:url"); const rimraf_1 = require("rimraf"); const semver_1 = require("semver"); const tmp_promise_1 = require("tmp-promise"); const unzipper_1 = require("unzipper"); const log = (0, debug_1.default)('ardunno-cli-gen'); async function default_1(options) { const { src, out, force } = options; log('generating with options %j', options); const [outExists, protos] = await Promise.all([ node_fs_1.promises.access(out).then(() => true, () => false), globProtos(src), ]); if (!force && outExists) { throw new Error(`${out} already exists. Use '--force' to override output`); } if (protos && protos.length) { log('found protos %j', protos); return generate(src, protos, out); } const semverOrGitHub = parseSemver(src) || parseGitHub(src); if (!semverOrGitHub) { throw new Error(`Invalid <src>: ${src}`); } const { protoPath, dispose } = await (semverOrGitHub instanceof semver_1.SemVer ? download(semverOrGitHub) : clone(semverOrGitHub)); try { const protos = await globProtos(protoPath); if (!protos) { throw new Error(`Failed to glob in ${protoPath}`); } await generate(protoPath, protos, out); } finally { await dispose(); } } exports.default = default_1; const plugins = { // eslint-disable-next-line @typescript-eslint/naming-convention ts_proto: { path: require.resolve('ts-proto/protoc-gen-ts_proto'), options: { outputServices: ['nice-grpc', 'generic-definitions'], oneof: 'unions', useExactTypes: false, paths: 'source_relative', esModuleInterop: true, exportCommonSymbols: false, useOptionals: 'none', }, }, }; function createArgs(tuple, src, out) { const [name, plugin] = tuple; const { options, path } = plugin; const opt = Object.entries(options) .reduce((acc, [key, value]) => acc.concat((Array.isArray(value) ? value : [value]).map((v) => `${key}=${v}`)), []) .join(','); return [ `--plugin=${path}`, `--proto_path=${src}`, `--${name}_opt=${opt}`, `--${name}_out=${out}`, ]; } async function generate(src, protos, out, name = 'ts_proto') { try { await node_fs_1.promises.mkdir(out, { recursive: true }); } catch (err) { log('failed to create --out %s %O', out, err); throw new Error(`Failed to create '--out' ${out}: ${err}`); } // Credit: https://github.com/arduino/arduino-ide/pull/2457/commits/f842badea8b0272f697db9c06ad31da732e62f45 // eslint-disable-next-line @typescript-eslint/no-var-requires const protoc = require('@pingghost/protoc/protoc'); // TODO: add support for external protoc const plugin = plugins[name]; const args = [...createArgs([name, plugin], src, out), ...protos]; log('executing %s with args %j', protoc, args); await execa(protoc, args); } async function execa(file, args, options) { const { execa } = await import('execa'); await execa(file, args, options); } async function globProtos(cwd) { log('glob %s', cwd); try { const { globby } = await import('globby'); const protos = await globby('**/*.proto', { cwd }); return protos; } catch (err) { log('glob failed %O', err); return undefined; } } // Constraint does not match with all GitHub rules, they're only a subset of them // owner name can contain only hyphens // repo name can contain dots and underscores // commit can be a branch, a hash, tag, etc, anything that git can `checkout` TODO: use https://git-scm.com/docs/git-check-ref-format? const ghPattern = /^(?<owner>([0-9a-zA-Z-]+))\/(?<repo>([0-9a-zA-Z-_\.]+))(#(?<commit>([^\s]+)))?$/; const arduinoGitHub = { owner: 'arduino', repo: 'arduino-cli', }; function parseGitHub(src) { const match = src.match(ghPattern); if (match && match.groups) { const { groups: { owner, repo, commit }, } = match; const gh = { owner, repo, ...(commit && { commit }), }; log('match GitHub %s, %j', src, gh); return gh; } log('no match GitHub %s', src); return undefined; } async function clone(gh) { const { owner, repo, commit = 'HEAD' } = gh; const { path } = await (0, tmp_promise_1.dir)({ prefix: repo }); log('clone %j', gh); log('clone dir %s', path); const url = `https://github.com/${owner}/${repo}.git`; try { await execa('git', ['clone', url, path]); log('cloned from %s to %s', url, path); } catch (err) { log('could not clone repository %s', url); throw new Error(`Could not clone GitHub repository from ${url}\n\nReason: ${err}`); } await execa('git', ['-C', path, 'fetch', '--all', '--tags']); log('fetched all from %s', url); try { await execa('git', ['-C', path, 'checkout', commit]); log('checked out %s from %s', commit, url); } catch (err) { log('could not checkout commit %s', commit); throw new Error(`Could not checkout commit '${commit}' in ${owner}/${repo}\n\nReason: ${err}`); } return { protoPath: (0, node_path_1.join)(path, 'rpc'), dispose: () => (0, rimraf_1.rimraf)(path), }; } const { owner, repo } = arduinoGitHub; const releases = `https://github.com/${owner}/${repo}/releases`; function protoLocation(semver) { if (!(0, semver_1.valid)(semver)) { log('attempted to download with invalid semver %s', semver); throw new Error(`invalid semver ${semver}`); } if (!canDownloadProtos(semver)) { log('attempted to download the asset file with semver %s', semver); throw new Error(`semver must be '>=0.29.0' it was ${semver}`); } const filenameVersion = semver.version; const ghReleaseVersion = hasSemverPrefix(semver) ? semver.raw : semver.version; const filename = `arduino-cli_${filenameVersion}_proto.zip`; const endpoint = `${releases}/download/${ghReleaseVersion}/${filename}`; log('semver: %s (raw: %s), filename: %s, endpoint: %s', semver.version, semver.raw, filename, endpoint); return { endpoint, filename }; } async function download(semver) { const { endpoint, filename } = protoLocation(semver); log('accessing protos from public endpoint %s', endpoint); // asset GET will result in a HTTP 302 (Redirect) const getLocationResp = await get(endpoint); if (getLocationResp.statusCode === 404) { log('release is not available for semver %s', semver); throw new Error(`Could not found release for version '${semver}'. Check the release page of the Arduino CLI for available versions: ${releases}`); } assertStatusCode(getLocationResp.statusCode, 302); const location = getLocationResp.headers.location; if (!location) { log('no location header was found: %j'); throw new Error(`no location header was found: ${JSON.stringify(getLocationResp.headers)}`); } const getAssetResp = await get(location); assertStatusCode(getAssetResp.statusCode, 200); const { path } = await (0, tmp_promise_1.dir)({ prefix: repo }); const zipPath = await new Promise((resolve, reject) => { const out = (0, node_path_1.join)(path, filename); const file = (0, node_fs_1.createWriteStream)(out); getAssetResp.pipe(file); file.on('finish', () => file.close((err) => (err ? reject(err) : resolve(out)))); file.on('error', (err) => { node_fs_1.promises.unlink(out); reject(err); }); }); const archive = await unzipper_1.Open.file(zipPath); const protoPath = (0, node_path_1.join)(path, 'rpc'); await archive.extract({ path: protoPath }); // Patch for https://github.com/arduino/arduino-cli/issues/2755 // Download the 1.0.4 version and use the missing google/rpc/status.proto if (semver.version !== '1.0.4') { const { protoPath: v104ProtoPath, dispose: v104Dispose } = await download(new semver_1.SemVer('v1.0.4')); await node_fs_1.promises.cp((0, node_path_1.join)(v104ProtoPath, 'google'), (0, node_path_1.join)(protoPath, 'google'), { recursive: true, }); v104Dispose(); } return { protoPath, dispose: () => (0, rimraf_1.rimraf)(path), }; } function assertStatusCode(actual, expected) { if (actual !== expected) { log('unexpected status code. was %s, expected %s', actual, expected); throw new Error(`unexpected status code. was ${actual}, expected ${expected}`); } } async function get(endpoint) { const url = new node_url_1.URL(endpoint); const proxy = process.env.https_proxy; if (proxy) { log('using proxy %s', proxy); // eslint-disable-next-line @typescript-eslint/no-explicit-any url.agent = new https_proxy_agent_1.HttpsProxyAgent(proxy); } log('GET %s', url.toString()); return new Promise((resolve) => { (0, node_https_1.get)(url, (resp) => { log('response %s, %s, %s', resp.statusCode, resp.method, resp.url); resolve(resp); }); }); } /** * If the `src` argument is `<0.29.0` semver, the function returns with a `GitHub` instance. */ function parseSemver(src) { log('parse semver %s', src); if (!(0, semver_1.valid)(src)) { log('invalid semver %s', src); return undefined; } const semver = new semver_1.SemVer(src, { loose: true }); if (canDownloadProtos(semver)) { log('parsed semver %s is >=0.29.0 (raw: %s)', semver.version, semver.raw); return semver; } const github = { ...arduinoGitHub, commit: semver.version, }; log('parsed semver %s is <0.29.0 (raw: %s). falling back to GitHub ref %j', semver.version, semver.raw, github); return github; } /** * The `.proto` files were not part of the Arduino CLI release before version `0.29.0` ([`arduino/arduino-cli#1931`](https://github.com/arduino/arduino-cli/pull/1931)). */ function canDownloadProtos(semver) { return (0, semver_1.gte)(semver, new semver_1.SemVer('0.29.0')); } /** * The Arduino CLI GitHub release has the `'v'` prefix from version `>=v0.35.0-rc.1` ([`arduino/arduino-cli#2374`](https://github.com/arduino/arduino-cli/pull/2374)). */ function hasSemverPrefix(semver) { return (0, semver_1.gte)(semver, new semver_1.SemVer('0.35.0-rc.1')); } /** * (non-API) */ // eslint-disable-next-line @typescript-eslint/naming-convention exports.__test = { parseGitHub, protoLocation, parseSemver, execa, };