// 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, EditorView } 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' console.log('loading file') 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 ]) ])() window.editorLib = { basicSetup, EditorView }` class Builder { // any URL that supports pkg/path or pkg@version/path baseUrl = 'https://unpkg.com' 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 pkgResp = await fetch( `${this.baseUrl}/${dep}/package.json` ) this.checkOk(pkgResp) this.log(dep) const pkg = await pkgResp.json() this.scripts[dep].version = pkg.version 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) { const hash = await crypto.subtle.digest( "SHA-256", ab ) return 'sha256-' + btoa( 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) { throw new Error( 'failed integrity check: ' + `${checkValue} !== ${script.sha}` ) } return ab } async getScript(name) { this.log('[downloading] ' + name, 'green') const script = this.scripts[name] if (script.text) { return script.text } const url = ( `${this.baseUrl}/${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() script.sha = resp.integrity } this.log( '[downloaded] ' + url + ` [${script.text.length}]`, 'green' ) 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( 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; } ` } class Editor extends HTMLElement { constructor() { super() this.attachShadow({mode: 'open'}) this.shadowRoot.adoptedStyleSheets = [ this.constructor.styleSheet ] } connectedCallback() { const {EditorView, basicSetup} = window.editorLib this.view = new EditorView({ doc: '', extensions: [ basicSetup, ], root: this.shadowRoot, }) this.shadowRoot.append(this.view.dom) } 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; height: 33vh; background-color: #fff; border: 5px solid red } :host > * { flex-grow: 1; } ` } customElements.define('m-build-view', BuildView) customElements.define('m-editor', Editor) 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.run() } async run() { await this.build() await this.display() } async build() { try { await this.builder.build() const s = document.createElement('script') s.type = 'module' s.textContent = this.builder.code document.head.append(s) this.buildView.log('skipped appending code') //this.buildView.log(this.builder.code) } catch (e) { this.buildView.log(`${e}`) } } async display() { await new Promise(r => setTimeout(() => r(), 500)) this.editor = document.createElement('m-editor') document.body.append(this.editor) } } new App()