diff --git a/package-lock.json b/package-lock.json index 2cfd9d6..e3dbd6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ardunno-cli-gen", - "version": "0.1.5", + "version": "0.1.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ardunno-cli-gen", - "version": "0.1.5", + "version": "0.1.7", "license": "MIT", "dependencies": { "commander": "^10.0.1", diff --git a/package.json b/package.json index ad5d338..08120f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ardunno-cli-gen", - "version": "0.1.5", + "version": "0.1.7", "description": "Generates nice-grpc API for the Arduino CLI", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/__tests__/generate.spec.ts b/src/__tests__/generate.spec.ts index 4a4d0f3..20df5c7 100644 --- a/src/__tests__/generate.spec.ts +++ b/src/__tests__/generate.spec.ts @@ -1,10 +1,11 @@ -import * as assert from 'assert'; +import * as assert from 'assert/strict'; import { describe } from 'mocha'; import { join } from 'node:path'; import { dir as tempDir } from 'tmp-promise'; -import generate, { __test, parseGitHub, parseSemver } from '../generate'; +import generate, { __test } from '../generate'; +import { SemVer } from 'semver'; -const { execa } = __test; +const { parseSemver, parseGitHub, protoLocation, execa } = __test; describe('generate', () => { it("should fail when 'src' is an accessible file", async function () { @@ -141,9 +142,9 @@ describe('generate', () => { 'protobuf', 'empty' )); - assert.notEqual(Empty, undefined); - assert.equal(typeof Empty.fromJSON, 'function'); - assert.deepEqual(Empty.fromJSON(), {}); + assert.notStrictEqual(Empty, undefined); + assert.strictEqual(typeof Empty.fromJSON, 'function'); + assert.deepStrictEqual(Empty.fromJSON(), {}); } finally { await execa('npm', ['unlink', 'protobufjs'], { cwd: path, @@ -154,13 +155,13 @@ describe('generate', () => { describe('parseGitHub', () => { it('should parse valid', () => { - assert.deepEqual(parseGitHub('arduino/arduino-cli'), { + assert.deepStrictEqual(parseGitHub('arduino/arduino-cli'), { owner: 'arduino', repo: 'arduino-cli', }); }); it('should parse valid with commit', () => { - assert.deepEqual(parseGitHub('arduino/arduino-cli#5a4ffe0'), { + assert.deepStrictEqual(parseGitHub('arduino/arduino-cli#5a4ffe0'), { owner: 'arduino', repo: 'arduino-cli', commit: '5a4ffe0', @@ -176,29 +177,125 @@ describe('generate', () => { 'owner/repo#one two', ].forEach((src) => it(`should not parse '${src}'`, () => - assert.equal(parseGitHub(src), undefined)) + assert.strictEqual(parseGitHub(src), undefined)) ); }); describe('parseSemver', () => { it('should parse valid', () => - assert.equal(parseSemver('0.30.0'), '0.30.0')); + assert.strictEqual( + (parseSemver('0.30.0')).version, + '0.30.0' + )); it('should parse valid with rc', () => - assert.equal(parseSemver('0.30.0-rc1'), '0.30.0-rc1')); + assert.strictEqual( + (parseSemver('0.30.0-rc1')).version, + '0.30.0-rc1' + )); it("should parse valid with 'v' prefix", () => - assert.equal(parseSemver('v0.29.1'), '0.29.1')); + assert.strictEqual( + (parseSemver('v0.29.1')).version, + '0.29.1' + )); it("should parse valid semver '>=0.29.0' as a semver [arduino/arduino-cli#1931]", () => - assert.equal(parseSemver('0.29.0'), '0.29.0')); + assert.strictEqual( + (parseSemver('0.29.0')).version, + '0.29.0' + )); it("should parse to GitHub ref when version is not greater than '0.28.0'", () => - assert.deepEqual(parseSemver('0.28.0'), { + assert.deepStrictEqual(parseSemver('0.28.0'), { owner: 'arduino', repo: 'arduino-cli', commit: '0.28.0', })); ['a', '0', '0.30', '0.30.', '0.30.0.'].forEach((src) => it(`should not parse '${src}'`, () => - assert.equal(parseSemver(src), undefined)) + assert.strictEqual(parseSemver(src), undefined)) ); }); + describe('protoLocation', () => { + ( + [ + ['0.28.0', false], + ['0.29.0-rc.1', false], + [ + '0.29.0', + { + endpoint: + 'https://github.com/arduino/arduino-cli/releases/download/0.29.0/arduino-cli_0.29.0_proto.zip', + filename: 'arduino-cli_0.29.0_proto.zip', + }, + ], + [ + 'v0.29.0', + { + endpoint: + 'https://github.com/arduino/arduino-cli/releases/download/0.29.0/arduino-cli_0.29.0_proto.zip', + filename: 'arduino-cli_0.29.0_proto.zip', + }, + ], + [ + 'v0.34.2', + { + endpoint: + 'https://github.com/arduino/arduino-cli/releases/download/0.34.2/arduino-cli_0.34.2_proto.zip', + filename: 'arduino-cli_0.34.2_proto.zip', + }, + ], + [ + 'v0.35.0-rc.0', + { + endpoint: + 'https://github.com/arduino/arduino-cli/releases/download/0.35.0-rc.0/arduino-cli_0.35.0-rc.0_proto.zip', + filename: 'arduino-cli_0.35.0-rc.0_proto.zip', + }, + ], + [ + 'v0.35.0-rc.1', + { + endpoint: + 'https://github.com/arduino/arduino-cli/releases/download/v0.35.0-rc.1/arduino-cli_0.35.0-rc.1_proto.zip', + filename: 'arduino-cli_0.35.0-rc.1_proto.zip', + }, + ], + [ + 'v0.35.0', + { + endpoint: + 'https://github.com/arduino/arduino-cli/releases/download/v0.35.0/arduino-cli_0.35.0_proto.zip', + filename: 'arduino-cli_0.35.0_proto.zip', + }, + ], + [ + 'v0.35.1', + { + endpoint: + 'https://github.com/arduino/arduino-cli/releases/download/v0.35.1/arduino-cli_0.35.1_proto.zip', + filename: 'arduino-cli_0.35.1_proto.zip', + }, + ], + ] as const + ) + .map( + ([raw, expected]) => + [new SemVer(raw, { loose: true }), expected] as [ + SemVer, + { endpoint: string; filename: string } | false + ] + ) + .forEach(([semver, expected]) => + it(`should${ + !expected ? ' not' : '' + } get the GitHub release asset location for the protos (${ + semver.raw + })`, () => { + if (!expected) { + assert.throws(() => protoLocation(semver)); + } else { + assert.deepStrictEqual(protoLocation(semver), expected); + } + }) + ); + }); }); async function dir(test: (path: string) => Promise): Promise { diff --git a/src/generate.ts b/src/generate.ts index cf70012..60a3a55 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -41,7 +41,7 @@ export default async function (options: Options): Promise { if (!semverOrGitHub) { throw new Error(`Invalid : ${src}`); } - const { protoPath, dispose } = await (typeof semverOrGitHub === 'string' + const { protoPath, dispose } = await (semverOrGitHub instanceof SemVer ? download(semverOrGitHub) : clone(semverOrGitHub)); try { @@ -59,7 +59,7 @@ interface Plugin { readonly options: Record; readonly path: string; } -export type PluginName = 'ts_proto'; +type PluginName = 'ts_proto'; const plugins: Record = { // eslint-disable-next-line @typescript-eslint/naming-convention ts_proto: { @@ -161,10 +161,7 @@ interface GitHub { readonly commit?: string | undefined; } -/** - * (non-API) - */ -export function parseGitHub(src: string): GitHub | undefined { +function parseGitHub(src: string): GitHub | undefined { const match: RegExpGroups<['owner', 'repo', 'commit']> = src.match(ghPattern); if (match && match.groups) { @@ -217,9 +214,10 @@ async function clone( }; } -async function download( - semver: string -): Promise<{ protoPath: string; dispose: () => Promise }> { +const { owner, repo } = arduinoGitHub; +const releases = `https://github.com/${owner}/${repo}/releases`; + +function protoLocation(semver: SemVer): { endpoint: string; filename: string } { if (!valid(semver)) { log('attempted to download with invalid semver %s', semver); throw new Error(`invalid semver ${semver}`); @@ -228,11 +226,26 @@ async function download( 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 }; +} - const { owner, repo } = arduinoGitHub; - const filename = `arduino-cli_${semver}_proto.zip`; - const releases = `https://github.com/${owner}/${repo}/releases`; - const endpoint = `${releases}/download/${semver}/${filename}`; +async function download( + semver: SemVer +): Promise<{ protoPath: string; dispose: () => Promise }> { + 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); @@ -304,41 +317,50 @@ async function get(endpoint: string): Promise { } /** - * (non-API) - * * If the `src` argument is `<0.29.0` semver, the function returns with a `GitHub` instance. */ -export function parseSemver(src: string): string | GitHub | undefined { +function parseSemver(src: string): SemVer | GitHub | undefined { log('parse semver %s', src); if (!valid(src)) { log('invalid semver %s', src); return undefined; } - const semver = new SemVer(src, true); - const version = semver.version; + const semver = new SemVer(src, { loose: true }); if (canDownloadProtos(semver)) { - log('parsed semver %s is >=0.29.0', version); - return version; + 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. falling back to GitHub ref %j', - version, + '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)). + * 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: SemVer | string): boolean { return gte(semver, new 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: SemVer | string): boolean { + return gte(semver, new SemVer('0.35.0-rc.1')); +} + // Taken from https://github.com/microsoft/TypeScript/issues/32098#issuecomment-1212501932 type RegExpGroups = | (RegExpMatchArray & { @@ -351,5 +373,8 @@ type RegExpGroups = */ // eslint-disable-next-line @typescript-eslint/naming-convention export const __test = { + parseGitHub, + protoLocation, + parseSemver, execa, } as const;