ardunno-cli-gen
Version:
Generates nice-grpc API for the Arduino CLI
294 lines (293 loc) • 11.3 kB
JavaScript
;
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,
};