You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

281 lines
6.7 KiB
JavaScript

// based on basicSetup https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
const editorSrc = `import {keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor,
rectangularSelection, crosshairCursor,
lineNumbers, highlightActiveLineGutter} from '@codemirror/view'
import {EditorState} from '@codemirror/state'
import {defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching,
foldGutter, foldKeymap} from '@codemirror/language'
import {defaultKeymap, history, historyKeymap} from '@codemirror/commands'
import {searchKeymap, highlightSelectionMatches} from '@codemirror/search'
import {autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap} from '@codemirror/autocomplete'
import {lintKeymap} from '@codemirror/lint'
/* */export const basicSetup = (() => [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
])
])()`
class Builder {
3 years ago
jApiBaseUrl = 'https://data.jsdelivr.com/v1/'
jCdnBaseUrl = 'https://cdn.jsdelivr.net/npm/'
scripts = {
'@rollup/browser': {
version: '3.20.4',
path: 'dist/rollup.browser.js',
sha: 'sha256-GgOznxZmgghx1a7CH09B+VmDKtziPO5tAnC5gC+/5Kw=',
},
}
topLevelDeps = [
"@codemirror/autocomplete",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@codemirror/lang-javascript",
]
constructor() {
this.downloads = []
}
checkOk(resp) {
if (!resp.ok) {
throw new Error(`HTTP request failed: ${resp.status}`)
}
}
async loadDep(dep) {
this.scripts[dep] = {}
const dataResp = await fetch(
this.jApiBaseUrl +
`packages/npm/${dep}/resolved`,
{
headers: {
'User-Agent':
'https://codeberg.org/macchiato',
},
},
)
this.checkOk(dataResp)
const {version} = await dataResp.json()
this.scripts[dep].version = version
const pkgResp = await fetch(
this.jCdnBaseUrl +
`${dep}@${version}/package.json`
)
this.checkOk(pkgResp)
this.log(dep)
3 years ago
const pkg = await pkgResp.json()
this.scripts[dep].path = (
pkg.module ?? pkg.main
)
this.downloads.push(this.getScript(dep))
const deps = Object.keys(
pkg.dependencies || {}
).filter(dep => !(dep in this.scripts))
await Promise.allSettled(deps.map(dep => (
this.loadDep(dep)
)))
}
async sha(ab) {
3 years ago
const hash = await crypto.subtle.digest(
"SHA-256", ab
)
return 'sha256-' + btoa(
3 years ago
String.fromCharCode(
...new Uint8Array(hash)
)
)
}
async checkIntegrity(resp, name, script) {
const blob = await resp.blob()
const ab = await blob.arrayBuffer()
const sha = await this.sha(ab)
if (sha !== script.sha) {
3 years ago
throw new Error(
'failed integrity check: ' +
`${checkValue} !== ${script.sha}`
)
}
return ab
}
async getScript(name) {
this.log('[downloading] ' + name, 'green')
3 years ago
const script = this.scripts[name]
if (script.text) {
return script.text
}
const url = (
this.jCdnBaseUrl +
`${name}@${script.version}/${script.path}`
)
const resp = await fetch(url)
this.checkOk(resp)
if (script.sha) {
const ab = await this.checkIntegrity(
resp, name, script
)
script.text = new TextDecoder().decode(ab)
} else {
script.text = await resp.text()
3 years ago
script.sha = resp.integrity
}
this.log(
'[downloaded] ' + url + ` [${script.text.length}]`,
'green'
)
3 years ago
return script.text
}
async loadScript(name) {
const text = await this.getScript(name)
const s = document.createElement('script')
s.text = text
document.head.append(s)
}
get loaderPlugin() {
return {
name: 'loader',
resolveId: async source => {
if (source === 'editor.js' || source in this.scripts) {
return source
}
},
load: async id => {
if (id === 'editor.js') {
this.log(`[found] editor.js`)
return editorSrc
} else if (id in this.scripts) {
this.log(`[found] ${id}`)
return this.scripts[id].text
}
},
}
}
async build() {
const result = await Promise.all(
3 years ago
this.topLevelDeps.map(dep => (
this.loadDep(dep)
))
)
await Promise.all(this.downloads)
await this.loadScript('@rollup/browser')
const { rollup } = window.rollup
const input = 'editor.js'
const plugins = [this.loaderPlugin]
const bundle = await rollup({input, plugins})
const {output} = await bundle.generate({format: 'es'})
this.code = output[0].code
this.sha = await this.sha(
new TextEncoder().encode(this.code)
)
this.log(`built ${this.sha}`)
}
}
class BuildView extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'})
this.shadowRoot.adoptedStyleSheets = [
this.constructor.styleSheet
]
}
log(message, cls = 'cyan') {
const el = document.createElement('pre')
el.classList.add(cls)
el.innerText = message
this.shadowRoot.append(el)
;(el.scrollIntoViewIfNeeded ?? el.scrollIntoView).call(el)
}
static get styleSheet() {
if (this._styleSheet === undefined) {
this._styleSheet = new CSSStyleSheet()
this._styleSheet.replaceSync(this.css)
}
return this._styleSheet
}
static css = `
:host {
display: flex;
flex-direction: column;
align-items: stretch;
margin: 10px;
gap: 10px;
}
pre {
padding: 8px;
border-radius: 5px;
margin: 0;
overflow-x: auto;
}
pre.cyan {
background: cyan;
}
pre.green {
background: lightgreen;
}
`
}
customElements.define('m-build-view', BuildView)
class App {
constructor() {
this.builder = new Builder()
this.buildView = document.createElement('m-build-view')
document.body.append(this.buildView)
this.builder.log = this.buildView.log.bind(this.buildView)
this.build()
}
async build() {
try {
await this.builder.build()
} catch (e) {
this.buildView.log(`${e}`)
}
3 years ago
}
}
new App()