From 075984226146ea2645f9bed82827a92b56eaba42 Mon Sep 17 00:00:00 2001 From: Steve Purves Date: Wed, 20 Dec 2023 15:37:39 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=9D=20repo=20providers=20now=20make=20?= =?UTF-8?q?storageKeys=20(#723)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🗝 repo providers now make storageKeys * 💚 tests --- packages/core/src/options.ts | 2 +- packages/core/src/server.ts | 12 ++-- packages/core/src/sessions.ts | 14 ++--- packages/core/src/types.ts | 4 +- packages/core/src/url.ts | 90 ++++++++++++++------------- packages/core/tests/config.spec.ts | 2 +- packages/core/tests/options.spec.ts | 2 +- packages/core/tests/sessions.spec.ts | 4 +- packages/core/tests/url.spec.ts | 91 ++++++++++++++++++++++------ 9 files changed, 141 insertions(+), 80 deletions(-) diff --git a/packages/core/src/options.ts b/packages/core/src/options.ts index 7661267e..854fa3c5 100644 --- a/packages/core/src/options.ts +++ b/packages/core/src/options.ts @@ -24,7 +24,7 @@ export function makeSavedSessionOptions(opts: SavedSessionOptions): Required, url: string) { - window.localStorage.removeItem(makeStorageKey(savedSession.storagePrefix, url)); + window.localStorage.removeItem(makeDefaultStorageKey(savedSession.storagePrefix, url)); } export function updateLastUsedTimestamp(savedSession: Required, url: string) { - const storageKey = makeStorageKey(savedSession.storagePrefix, url); + const storageKey = makeDefaultStorageKey(savedSession.storagePrefix, url); const saved = window.localStorage.getItem(storageKey); if (!saved) return; const obj = JSON.parse(saved); @@ -29,7 +29,7 @@ export function saveServerInfo( // save the current connection url+token to reuse later const { baseUrl, token, wsUrl } = serverSettings; window.localStorage.setItem( - makeStorageKey(savedSession.storagePrefix, url), + makeDefaultStorageKey(savedSession.storagePrefix, url), JSON.stringify({ id, baseUrl, @@ -49,7 +49,7 @@ export async function getExistingServer( url: string, ): Promise { if (!savedSessionOptions.enabled) return null; - const storageKey = makeStorageKey(savedSessionOptions.storagePrefix, url); + const storageKey = makeDefaultStorageKey(savedSessionOptions.storagePrefix, url); const storedInfoJSON = window.localStorage.getItem(storageKey); if (storedInfoJSON == null) { console.debug('thebe:getExistingServer No session saved in ', storageKey); @@ -120,6 +120,6 @@ export function clearAllSavedSessions(storagePrefix = 'thebe-binder') { * @param url */ export function clearSavedSession(storagePrefix = 'thebe-binder', url = '') { - console.debug(`thebe:clearSavedSession - removing ${makeStorageKey(storagePrefix, url)}`); - window.localStorage.removeItem(makeStorageKey(storagePrefix, url)); + console.debug(`thebe:clearSavedSession - removing ${makeDefaultStorageKey(storagePrefix, url)}`); + window.localStorage.removeItem(makeDefaultStorageKey(storagePrefix, url)); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d81d12ae..84fea79b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -4,6 +4,7 @@ import type { IOutput, IError } from '@jupyterlab/nbformat'; import type { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import type ThebeServer from './server'; import type { ServerStatusEvent } from './events'; +import type { Config } from './config'; export type CellKind = 'code' | 'markdown'; @@ -41,12 +42,13 @@ export interface CoreOptions { export interface RepoProviderSpec { name: string; - makeUrls: (opts: BinderOptions) => BinderUrlSet; + makeUrls: (config: Config) => BinderUrlSet; } export interface BinderUrlSet { build: string; launch: string; + storageKey?: string; } export type WellKnownRepoProvider = 'git' | 'github' | 'gitlab' | 'gist'; diff --git a/packages/core/src/url.ts b/packages/core/src/url.ts index b958a2cd..9795d5fd 100644 --- a/packages/core/src/url.ts +++ b/packages/core/src/url.ts @@ -1,4 +1,17 @@ -import type { BinderOptions, BinderUrlSet, RepoProviderSpec } from './types'; +import type { Config } from './config'; +import { makeDefaultStorageKey } from './sessions'; +import type { BinderUrlSet, RepoProviderSpec } from './types'; + +function makeDefaultBuildSpec(storagePrefix: string, binderUrl: string, stub: string) { + const build = `${binderUrl}/build/${stub}`; + const launch = `${binderUrl}/v2/${stub}`; + + return { + build, + launch, + storageKey: makeDefaultStorageKey(storagePrefix, build), + }; +} /** * Make a binder url for git providers @@ -10,16 +23,13 @@ import type { BinderOptions, BinderUrlSet, RepoProviderSpec } from './types'; * @param opts BinderOptions * @returns a binder compatible url */ -function makeGitUrls(opts: BinderOptions) { - if (!opts.repo) throw Error('repo is required for git provider'); - const { repo, binderUrl, ref } = opts; +function makeGitUrls(config: Config) { + if (!config.binder.repo) throw Error('repo is required for git provider'); + const { repo, binderUrl, ref } = config.binder; const encodedRepo = encodeURIComponent(repo.replace(/(^\/)|(\/?$)/g, '')); const base = binderUrl?.replace(/(\/?$)/g, ''); const stub = `git/${encodedRepo}/${ref ?? 'HEAD'}`; - return { - build: `${base}/build/${stub}`, - launch: `${base}/v2/${stub}`, - }; + return makeDefaultBuildSpec(config.savedSessions.storagePrefix, base, stub); } /** @@ -33,17 +43,16 @@ function makeGitUrls(opts: BinderOptions) { * @param opts BinderOptions * @returns a binder compatible url */ -function makeGitLabUrl(opts: BinderOptions) { - if (!opts.repo) throw Error('repo is required for gitlab provider'); - const binderUrl = opts.binderUrl?.replace(/(\/?$)/g, ''); +function makeGitLabUrl(config: Config) { + if (!config.binder.repo) throw Error('repo is required for gitlab provider'); + const binderUrl = config.binder.binderUrl?.replace(/(\/?$)/g, ''); const repo = encodeURIComponent( - (opts.repo ?? '').replace(/^(https?:\/\/)?gitlab.com\//, '').replace(/(^\/)|(\/?$)/g, ''), + (config.binder.repo ?? '') + .replace(/^(https?:\/\/)?gitlab.com\//, '') + .replace(/(^\/)|(\/?$)/g, ''), ); - const stub = `gl/${repo}/${opts.ref ?? 'HEAD'}`; - return { - build: `${binderUrl}/build/${stub}`, - launch: `${binderUrl}/v2/${stub}`, - }; + const stub = `gl/${repo}/${config.binder.ref ?? 'HEAD'}`; + return makeDefaultBuildSpec(config.savedSessions.storagePrefix, binderUrl, stub); } /** @@ -57,26 +66,26 @@ function makeGitLabUrl(opts: BinderOptions) { * @param opts BinderOptions * @returns a binder compatible url */ -function makeGitHubUrl(opts: BinderOptions) { - if (!opts.repo) throw Error('repo is required for github provider'); - const repo = opts.repo.replace(/^(https?:\/\/)?github.com\//, '').replace(/(^\/)|(\/?$)/g, ''); - const binderUrl = opts.binderUrl?.replace(/(\/?$)/g, ''); - const stub = `gh/${repo}/${opts.ref ?? 'HEAD'}`; - return { - build: `${binderUrl}/build/${stub}`, - launch: `${binderUrl}/v2/${stub}`, - }; +function makeGitHubUrl(config: Config) { + if (!config.binder.repo) throw Error('repo is required for github provider'); + const repo = config.binder.repo + .replace(/^(https?:\/\/)?github.com\//, '') + .replace(/(^\/)|(\/?$)/g, ''); + const binderUrl = config.binder.binderUrl?.replace(/(\/?$)/g, ''); + const stub = `gh/${repo}/${config.binder.ref ?? 'HEAD'}`; + + return makeDefaultBuildSpec(config.savedSessions.storagePrefix, binderUrl, stub); } -function makeGistUrl(opts: BinderOptions) { - if (!opts.repo) throw Error('repo is required for gist provider'); - const repo = opts.repo.replace(/^(https?:\/\/)?github.com\//, '').replace(/(^\/)|(\/?$)/g, ''); - const binderUrl = opts.binderUrl?.replace(/(\/?$)/g, ''); - const stub = `gist/${repo}/${opts.ref ?? 'HEAD'}`; - return { - build: `${binderUrl}/build/${stub}`, - launch: `${binderUrl}/v2/${stub}`, - }; +function makeGistUrl(config: Config) { + if (!config.binder.repo) throw Error('repo is required for gist provider'); + const repo = config.binder.repo + .replace(/^(https?:\/\/)?github.com\//, '') + .replace(/(^\/)|(\/?$)/g, ''); + const binderUrl = config.binder.binderUrl?.replace(/(\/?$)/g, ''); + const stub = `gist/${repo}/${config.binder.ref ?? 'HEAD'}`; + + return makeDefaultBuildSpec(config.savedSessions.storagePrefix, binderUrl, stub); } export const GITHUB_SPEC: RepoProviderSpec = { @@ -107,18 +116,15 @@ export const WELL_KNOWN_REPO_PROVIDERS = [GITHUB_SPEC, GITLAB_SPEC, GIT_SPEC, GI * Custom providers are supported by passing in an array of CustomRepoProviderSpecs. * */ -export function makeBinderUrls( - opts: BinderOptions, - repoProviders: RepoProviderSpec[], -): BinderUrlSet { +export function makeBinderUrls(config: Config, repoProviders: RepoProviderSpec[]): BinderUrlSet { const providerMap: Record = repoProviders.reduce((obj, spec) => ({ ...obj, [spec.name]: spec }), {}) ?? {}; - const provider = opts.repoProvider ?? 'github'; + const provider = config.binder.repoProvider ?? 'github'; if (!Object.keys(providerMap).includes(provider)) - throw Error(`Unknown provider ${opts.repoProvider}`); + throw Error(`Unknown provider ${config.binder.repoProvider}`); if (!providerMap[provider].makeUrls) throw Error(`No makeUrls function for ${provider}`); - return providerMap[provider].makeUrls(opts); + return providerMap[provider].makeUrls(config); } diff --git a/packages/core/tests/config.spec.ts b/packages/core/tests/config.spec.ts index 36151556..ba5778b1 100644 --- a/packages/core/tests/config.spec.ts +++ b/packages/core/tests/config.spec.ts @@ -32,7 +32,7 @@ describe('config', () => { expect(config.savedSessions).toEqual({ enabled: true, maxAge: 86400, - storagePrefix: 'thebe-binder-', + storagePrefix: 'thebe-binder', }); }); test('server settings', () => { diff --git a/packages/core/tests/options.spec.ts b/packages/core/tests/options.spec.ts index 0f9a9cee..5db391b8 100644 --- a/packages/core/tests/options.spec.ts +++ b/packages/core/tests/options.spec.ts @@ -38,7 +38,7 @@ describe('options', () => { expect(makeSavedSessionOptions({})).toEqual({ enabled: true, maxAge: 86400, - storagePrefix: 'thebe-binder-', + storagePrefix: 'thebe-binder', }); }); test('overrides', () => { diff --git a/packages/core/tests/sessions.spec.ts b/packages/core/tests/sessions.spec.ts index 9e69fd65..8900bd41 100644 --- a/packages/core/tests/sessions.spec.ts +++ b/packages/core/tests/sessions.spec.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest'; -import { makeStorageKey } from '../src/sessions'; +import { makeDefaultStorageKey } from '../src/sessions'; describe('session saving', () => { describe('make storage key', () => { @@ -19,7 +19,7 @@ describe('session saving', () => { ['https://mybinder.org/', 'prefix-https://mybinder.org/'], ['https://mybinder.org:1234/', 'prefix-https://mybinder.org:1234/'], ])('%s', (url, result) => { - expect(makeStorageKey('prefix', url)).toEqual(result); + expect(makeDefaultStorageKey('prefix', url)).toEqual(result); }); }); }); diff --git a/packages/core/tests/url.spec.ts b/packages/core/tests/url.spec.ts index 1630396a..b48ee9d7 100644 --- a/packages/core/tests/url.spec.ts +++ b/packages/core/tests/url.spec.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from 'vitest'; import { WELL_KNOWN_REPO_PROVIDERS, makeBinderUrls } from '../src/url'; import { makeBinderOptions } from '../src/options'; -import type { BinderOptions } from '../src/types'; +import { Config } from '../src/config'; const binderUrl = 'https://binder.curvenote.dev/'; @@ -10,119 +10,170 @@ describe('building binder urls', () => { test('git', () => { expect( makeBinderUrls( - makeBinderOptions({ binderUrl, repoProvider: 'git' }), + new Config({ + binderOptions: makeBinderOptions({ binderUrl, repoProvider: 'git' }), + }), WELL_KNOWN_REPO_PROVIDERS, ), ).toEqual({ build: 'https://binder.curvenote.dev/build/git/executablebooks%2Fthebe-binder-base/HEAD', launch: 'https://binder.curvenote.dev/v2/git/executablebooks%2Fthebe-binder-base/HEAD', + storageKey: + 'thebe-binder-https://binder.curvenote.dev/build/git/executablebooks%2Fthebe-binder-base/HEAD', }); expect( makeBinderUrls( - makeBinderOptions({ binderUrl, repoProvider: 'git', ref: 'main' }), + new Config({ + binderOptions: makeBinderOptions({ binderUrl, repoProvider: 'git', ref: 'main' }), + }), WELL_KNOWN_REPO_PROVIDERS, ), ).toEqual({ build: 'https://binder.curvenote.dev/build/git/executablebooks%2Fthebe-binder-base/main', launch: 'https://binder.curvenote.dev/v2/git/executablebooks%2Fthebe-binder-base/main', + storageKey: + 'thebe-binder-https://binder.curvenote.dev/build/git/executablebooks%2Fthebe-binder-base/main', }); }); test('gitlab', () => { expect( makeBinderUrls( - makeBinderOptions({ binderUrl, repoProvider: 'gitlab' }), + new Config({ + binderOptions: makeBinderOptions({ binderUrl, repoProvider: 'gitlab' }), + }), WELL_KNOWN_REPO_PROVIDERS, ), ).toEqual({ build: 'https://binder.curvenote.dev/build/gl/executablebooks%2Fthebe-binder-base/HEAD', launch: 'https://binder.curvenote.dev/v2/gl/executablebooks%2Fthebe-binder-base/HEAD', + storageKey: + 'thebe-binder-https://binder.curvenote.dev/build/gl/executablebooks%2Fthebe-binder-base/HEAD', }); expect( makeBinderUrls( - makeBinderOptions({ binderUrl, repoProvider: 'gitlab', ref: 'main' }), + new Config({ + binderOptions: makeBinderOptions({ binderUrl, repoProvider: 'gitlab', ref: 'main' }), + }), WELL_KNOWN_REPO_PROVIDERS, ), ).toEqual({ build: 'https://binder.curvenote.dev/build/gl/executablebooks%2Fthebe-binder-base/main', launch: 'https://binder.curvenote.dev/v2/gl/executablebooks%2Fthebe-binder-base/main', + storageKey: + 'thebe-binder-https://binder.curvenote.dev/build/gl/executablebooks%2Fthebe-binder-base/main', }); }); test('github', () => { expect( makeBinderUrls( - makeBinderOptions({ binderUrl, repoProvider: 'github' }), + new Config({ + binderOptions: makeBinderOptions({ binderUrl, repoProvider: 'github' }), + }), WELL_KNOWN_REPO_PROVIDERS, ), ).toEqual({ build: 'https://binder.curvenote.dev/build/gh/executablebooks/thebe-binder-base/HEAD', launch: 'https://binder.curvenote.dev/v2/gh/executablebooks/thebe-binder-base/HEAD', + storageKey: + 'thebe-binder-https://binder.curvenote.dev/build/gh/executablebooks/thebe-binder-base/HEAD', }); expect( makeBinderUrls( - makeBinderOptions({ binderUrl, repoProvider: 'github', ref: 'main' }), + new Config({ + binderOptions: makeBinderOptions({ binderUrl, repoProvider: 'github', ref: 'main' }), + }), WELL_KNOWN_REPO_PROVIDERS, ), ).toEqual({ build: 'https://binder.curvenote.dev/build/gh/executablebooks/thebe-binder-base/main', launch: 'https://binder.curvenote.dev/v2/gh/executablebooks/thebe-binder-base/main', + storageKey: + 'thebe-binder-https://binder.curvenote.dev/build/gh/executablebooks/thebe-binder-base/main', }); }); test('gist', () => { expect( makeBinderUrls( - makeBinderOptions({ binderUrl, repoProvider: 'gist' }), + new Config({ + binderOptions: makeBinderOptions({ binderUrl, repoProvider: 'gist' }), + }), WELL_KNOWN_REPO_PROVIDERS, ), ).toEqual({ build: 'https://binder.curvenote.dev/build/gist/executablebooks/thebe-binder-base/HEAD', launch: 'https://binder.curvenote.dev/v2/gist/executablebooks/thebe-binder-base/HEAD', + storageKey: + 'thebe-binder-https://binder.curvenote.dev/build/gist/executablebooks/thebe-binder-base/HEAD', }); expect( makeBinderUrls( - makeBinderOptions({ binderUrl, repoProvider: 'gist', ref: 'main' }), + new Config({ + binderOptions: makeBinderOptions({ binderUrl, repoProvider: 'gist', ref: 'main' }), + }), WELL_KNOWN_REPO_PROVIDERS, ), ).toEqual({ build: 'https://binder.curvenote.dev/build/gist/executablebooks/thebe-binder-base/main', launch: 'https://binder.curvenote.dev/v2/gist/executablebooks/thebe-binder-base/main', + storageKey: + 'thebe-binder-https://binder.curvenote.dev/build/gist/executablebooks/thebe-binder-base/main', }); }); }); test('unknown provider throws', () => { expect(() => makeBinderUrls( - makeBinderOptions({ binderUrl, repoProvider: 'unknown' }), + new Config({ + binderOptions: makeBinderOptions({ binderUrl, repoProvider: 'unknown' }), + }), WELL_KNOWN_REPO_PROVIDERS, ), ).toThrow(); }); describe('custom providers', () => { test('known custom provider - no ref', () => { - const opts = { binderUrl, repoProvider: 'custom', repo: 'sunshine' }; + const binderOptions = { binderUrl, repoProvider: 'custom', repo: 'sunshine' }; + const c = new Config({ binderOptions }); const spec = { name: 'custom', - makeUrls: ({ repo, ref }: BinderOptions) => ({ - build: `https://custom.host.com/build/custom/${repo}${ref ? `~~~${ref}` : ''}`, - launch: `https://custom.host.com/v2/custom/${repo}${ref ? `~~~${ref}` : ''}`, + makeUrls: (config: Config) => ({ + build: `https://custom.host.com/build/custom/${config.binder.repo}`, + launch: `https://custom.host.com/v2/custom/${config.binder.repo}`, + storageKey: `thebe-binder-https://custom.host.com/v2/custom/${config.binder.repo}`, }), }; - expect(makeBinderUrls(opts, [spec])).toEqual({ + expect(makeBinderUrls(c, [spec])).toEqual({ build: `https://custom.host.com/build/custom/sunshine`, launch: `https://custom.host.com/v2/custom/sunshine`, + storageKey: 'thebe-binder-https://custom.host.com/v2/custom/sunshine', }); }); test('known custom provider - with ref', () => { - const opts = { binderUrl, repoProvider: 'custom', repo: 'sunshine', ref: 'unicorns' }; + const binderOptions = { + binderUrl, + repoProvider: 'custom', + repo: 'sunshine', + ref: 'unicorns', + }; + const c = new Config({ binderOptions }); const spec = { name: 'custom', - makeUrls: ({ repo, ref }: BinderOptions) => ({ - build: `https://custom.host.com/build/custom/${repo}${ref ? `~~~${ref}` : ''}`, - launch: `https://custom.host.com/v2/custom/${repo}${ref ? `~~~${ref}` : ''}`, + makeUrls: (config: Config) => ({ + build: `https://custom.host.com/build/custom/${config.binder.repo}${ + config.binder.ref ? `~~~${config.binder.ref}` : '' + }`, + launch: `https://custom.host.com/v2/custom/${config.binder.repo}${ + config.binder.ref ? `~~~${config.binder.ref}` : '' + }`, + storageKey: `thebe-binder-https://custom.host.com/v2/custom/${config.binder.repo}${ + config.binder.ref ? `~~~${config.binder.ref}` : '' + }`, }), }; - expect(makeBinderUrls(opts, [spec])).toEqual({ + expect(makeBinderUrls(c, [spec])).toEqual({ build: `https://custom.host.com/build/custom/sunshine~~~unicorns`, launch: `https://custom.host.com/v2/custom/sunshine~~~unicorns`, + storageKey: 'thebe-binder-https://custom.host.com/v2/custom/sunshine~~~unicorns', }); }); });