Skip to content

Instantly share code, notes, and snippets.

@pfrazee
Created October 5, 2020 20:37
Show Gist options
  • Save pfrazee/f453560014259428c9bb1e1d6d89dc55 to your computer and use it in GitHub Desktop.
Save pfrazee/f453560014259428c9bb1e1d6d89dc55 to your computer and use it in GitHub Desktop.
HPM app source (MVP of the app)
<!doctype html>
<html>
<head>
<title>HPM</title>
<style>
body {
max-width: 800px;
margin: 0 auto;
padding: 0 10px;
}
</style>
</head>
<body>
<h1><a href="/">HPM</a></h1>
<form class="search-form">
<p>
Hyper Package Manager
<input type="text" name="q" placeholder="Search for packages">
</p>
</form>
<hpm-app></hpm-app>
<script type="module" src="/ui.js"></script>
</body>
</html>
export const hpm = {
async publish (opts = {}, packageName, packageUrl) {
validation.required('package-name', packageName)
validation.id('package-name', packageName)
validation.required('package-url', packageUrl)
validation.url('package-url', packageUrl)
packageUrl = (new URL(packageUrl)).origin
var profileDrive = beaker.hyperdrive.drive(this.env.get('home'))
await profileDrive.mkdir('/hpm').catch(e => undefined)
await profileDrive.mkdir('/hpm/pkgs').catch(e => undefined)
await profileDrive.writeFile(`/hpm/pkgs/${packageName}.goto`, '', {
metadata: {
href: packageUrl,
title: packageName
}
})
return `${packageName} published`
},
async unpublish (opts = {}, packageName) {
validation.required('package-name', packageName)
validation.id('package-name', packageName)
var profileDrive = beaker.hyperdrive.drive(this.env.get('home'))
try {
var st = await profileDrive.stat(`/hpm/pkgs/${packageName}.goto`)
} catch {
throw new Error(`Package "${packageName}" does not exist`)
}
await profileDrive.unlink(`/hpm/pkgs/${packageName}.goto`)
return `${packageName} unpublished`
},
async ls (opts = {}, userUrl) {
if (userUrl) {
validation.url('user-url', userUrl)
}
var profileDrive = beaker.hyperdrive.drive(userUrl || this.env.get('home'))
var files = await profileDrive.query({path: '/hpm/pkgs/*.goto'})
return files.map(entry => {
let id = entry.path.split('/').pop().split('.').slice(0, -1).join('.')
let {href} = entry.stat.metadata
return `${id} - ${href}`
}).join('\n')
},
async search (opts = {}, query) {
validation.required('query', query)
var {results} = await beaker.index.gql(`
query Search ($search: String!) {
results: records(search: $search, paths: ["/hpm/pkgs/*.goto"]) {
path
metadata
site {
title
}
}
}
`, {search: query})
return results.map(result => {
let id = result.path.split('/').pop().split('.').slice(0, -1).join('.')
let {href} = result.metadata
return `${result.site.title} / ${id} - ${href}`
}).join('\n')
}
}
const validation = {
required (name, v) {
if (!v) {
throw new Error(`${name} is required`)
}
},
id (name, v) {
if (!v) return
var re = /^[a-z0-9-]+$/
if (!re.test(v)) {
throw new Error(`${name} must be all-lowercase alpha-numeric (dashes allowed)`)
}
},
url (name, v) {
if (!v) return
try {
let urlp = new URL(v)
if (urlp.protocol !== 'hyper:') throw new Error()
} catch (e) {
throw new Error(`${name} must be a hyper:// URL`)
}
}
}
{
"title": "HPM",
"description": "Hyper Package Manager",
"commands": [
{
"name": "hpm",
"help": "Hyper package manager",
"subcommands": [
{
"name": "publish",
"help": "Publish a hyper package",
"usage": "hpm publish {name} {url}"
},
{
"name": "unpublish",
"help": "Unpublish a hyper package",
"usage": "hpm publish {name}"
},
{
"name": "ls",
"help": "List packages published by a user (default self)",
"usage": "hpm ls [{user-url}]"
},
{
"name": "search",
"help": "Find a package published by users in your network",
"usage" : "hpm search {query}"
}
]
}
]
}
var searchQuery = (new URLSearchParams(location.search)).get('q')
customElements.define('hpm-app', class extends HTMLElement {
constructor () {
super()
this.session = undefined
this.load()
}
async load () {
this.session = await beaker.session.get()
console.log(this.session)
this.render()
}
render () {
if (this.session) {
if (searchQuery) {
return this.renderLoggedInSearch()
}
return this.renderLoggedIn()
}
this.renderLoggedOut()
}
renderLoggedInSearch () {
this.innerHTML = `
<p><strong>Logged in as ${this.session.user.title}</strong></p>
<hr>
<h2>Search results for "${makeSafe(searchQuery)}"</h2>
<hpm-feed search="${makeSafe(searchQuery)}"></hpm-feed>
`
}
renderLoggedIn () {
this.innerHTML = `
<p><strong>Logged in as ${this.session.user.title}</strong></p>
<hr>
<h2>Feed</h2>
<hpm-feed></hpm-feed>
<hr>
<h2>My Packages</h2>
<hpm-my-packages profile-url="${this.session.user.url}"></hpm-my-packages>
`
}
renderLoggedOut () {
this.innerHTML = `
<p><strong>Log in to get started <button class="login">Log In</button></strong></p>
`
this.attachEventHandlers()
}
attachEventHandlers () {
this.querySelector('.login').addEventListener('click', async e => {
await beaker.session.request({
permissions: {
publicFiles: [
{path: '/hpm/pkgs/*.goto', access: 'write'}
]
}
})
window.location.reload()
})
}
})
customElements.define('hpm-feed', class extends HTMLElement {
constructor () {
super()
this.load()
}
async load () {
var search = this.getAttribute('search')
if (search) {
var {records} = await beaker.index.gql(`
query ($search: String!) {
records (search: $search, paths: ["/hpm/pkgs/*.goto"] sort: crtime reverse: true limit: 10) {
path
ctime
metadata
site { url title }
}
}
`, {search})
} else {
var {records} = await beaker.index.gql(`
query {
records (paths: ["/hpm/pkgs/*.goto"] sort: crtime reverse: true limit: 10) {
path
ctime
metadata
site { url title }
}
}
`)
}
console.log(records)
if (records.length === 0) {
this.innerHTML = `<p>No results</p>`
return
}
var recordHtmls = []
for (let record of records) {
let id = record.path.split('/').pop().split('.').slice(0, -1).join('.')
recordHtmls.push(`
<p>
<a href="${makeSafe(encodeURI(record.metadata.href))}">${makeSafe(id)}</a>
- by <a href="${makeSafe(encodeURI(record.site.url))}">${makeSafe(record.site.title)}</a>
on ${(new Date(record.ctime)).toLocaleDateString()}
</p>
`)
}
this.innerHTML = recordHtmls.join('\n')
}
})
customElements.define('hpm-my-packages', class extends HTMLElement {
constructor () {
super()
this.load()
}
async load () {
var profileUrl = this.getAttribute('profile-url')
var {records} = await beaker.index.gql(`
query ($profileUrl: String!) {
records (paths: ["/hpm/pkgs/*.goto"] origins: [$profileUrl]) {
path
metadata
}
}
`, {profileUrl})
records.sort((a, b) => {
return getId(a.path).localeCompare(getId(b.path))
})
console.log(records)
var recordHtmls = []
for (let record of records) {
let id = getId(record.path)
recordHtmls.push(`
<li>
<a href="${makeSafe(encodeURI(record.metadata.href))}">${makeSafe(id)}</a>
</li>
`)
}
this.innerHTML = `<ul>${recordHtmls.join('\n')}</ul>`
}
})
function makeSafe (str) {
return (str || '').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
}
function getId (path) {
return path.split('/').pop().split('.').slice(0, -1).join('.')
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment