Skip to content

Latest commit

 

History

History
523 lines (499 loc) · 14 KB

bundle-libraries.md

File metadata and controls

523 lines (499 loc) · 14 KB

Bundle Libraries

This takes the downloaded libraries in library-source.md and bundles them using rollup.

codemirror-bundle-source.js

import {
  keymap, highlightSpecialChars, 
  drawSelection, highlightActiveLine, dropCursor,
  rectangularSelection, crosshairCursor,
  lineNumbers, highlightActiveLineGutter,
  EditorView
} from '@codemirror/view'
import { EditorState, Compartment, StateEffect, Prec, Text } from '@codemirror/state'
import {
  defaultHighlightStyle, syntaxHighlighting, indentOnInput, 
  bracketMatching, foldGutter, foldKeymap, LanguageDescription,
  LanguageSupport, HighlightStyle
} 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 {tags} from '@lezer/highlight'
import {lintKeymap} from '@codemirror/lint'
import { javascriptLanguage } from '@codemirror/lang-javascript'
import { cssLanguage } from '@codemirror/lang-css'
import { jsonLanguage } from '@codemirror/lang-json'
import { htmlLanguage } from '@codemirror/lang-html'
import {
  markdown, markdownLanguage, commonmarkLanguage, insertNewlineContinueMarkup, deleteMarkupBackward, markdownKeymap
} from '@codemirror/lang-markdown'
import { globalCompletion, localCompletionSource, pythonLanguage, python } from '@codemirror/lang-python'
import {
  sql, StandardSQL, PostgreSQL, MySQL, MariaSQL, MSSQL, SQLite, Cassandra, PLSQL,
  keywordCompletionSource, schemaCompletionSource
} from '@codemirror/lang-sql'
import { rust, rustLanguage } from '@codemirror/lang-rust'
import { wast, wastLanguage } from '@codemirror/lang-wast'
import { xml, xmlLanguage, completeFromSchema } from '@codemirror/lang-xml'
import { oneDark, oneDarkTheme, oneDarkHighlightStyle, color } from '@codemirror/theme-one-dark'

Macchiato.externalModules = {
  ...Macchiato.externalModules,
  '@codemirror/view': {
    keymap, highlightSpecialChars,
    drawSelection, highlightActiveLine, dropCursor,
    rectangularSelection, crosshairCursor,
    lineNumbers, highlightActiveLineGutter,
    EditorView,
  },
  '@codemirror/state': {
    EditorState, Compartment, StateEffect, Prec, Text
  },
  '@codemirror/language': {
    defaultHighlightStyle,
    syntaxHighlighting,
    indentOnInput,
    bracketMatching,
    foldGutter,
    foldKeymap,
    HighlightStyle,
    LanguageDescription,
    LanguageSupport,
  },
  '@codemirror/commands': {
    defaultKeymap,
    history,
    historyKeymap,
  },
  '@codemirror/search': {
    searchKeymap,
    highlightSelectionMatches,
  },
  '@codemirror/autocomplete': {
    autocompletion,
    completionKeymap,
    closeBrackets,
    closeBracketsKeymap,
  },
  '@lezer/highlight': {
    tags,
  },
  '@codemirror/lint': {
    lintKeymap,
  },
  '@codemirror/lang-javascript': {
    javascriptLanguage,
  },
  '@codemirror/lang-css': {
    cssLanguage,
  },
  '@codemirror/lang-json': {
    jsonLanguage,
  },
  '@codemirror/lang-html': {
    htmlLanguage,
  },
  '@codemirror/lang-markdown': {
    markdown, markdownLanguage, commonmarkLanguage, insertNewlineContinueMarkup, deleteMarkupBackward, markdownKeymap,
  },
  '@codemirror/lang-python': {
    globalCompletion, localCompletionSource, pythonLanguage, python,
  },
  '@codemirror/lang-sql': {
    sql, StandardSQL, PostgreSQL, MySQL, MariaSQL, MSSQL, SQLite, Cassandra, PLSQL,
    keywordCompletionSource, schemaCompletionSource,
  },
  '@codemirror/lang-rust': {
    rust, rustLanguage,
  },
  '@codemirror/lang-wast': {
    wast, wastLanguage,
  },
  '@codemirror/lang-xml': {
    xml, xmlLanguage, completeFromSchema,
  },
  '@codemirror/theme-one-dark': {
    oneDark, oneDarkTheme, oneDarkHighlightStyle, color
  }
}

prosemirror-bundle-source.js

import {
  EditorState,
  Selection,
  SelectionRange,
  TextSelection,
  NodeSelection,
  AllSelection,
  Transaction,
  Plugin,
  PluginKey
} from 'prosemirror-state'
import {
  EditorView,
  Decoration,
  DecorationSet
} from 'prosemirror-view'
import {
  Node,
  ResolvedPos,
  NodeRange,
  Fragment,
  Slice,
  ReplaceError,
  Mark,
  Schema,
  NodeType,
  MarkType,
  ContentMatch,
  DOMParser,
  DOMSerializer
} from 'prosemirror-model'
import {schema} from 'prosemirror-schema-basic'
import {addListNodes} from 'prosemirror-schema-list'
import {exampleSetup} from 'prosemirror-example-setup'
import {
  history,
  undo,
  redo,
  undoNoScroll,
  redoNoScroll,
  undoDepth,
  redoDepth,
  closeHistory
} from 'prosemirror-history'
import {
  keymap,
  keydownHandler
} from 'prosemirror-keymap'
import {
  deleteSelection,
  joinBackward,
  joinTextblockBackward,
  joinTextblockForward,
  selectNodeBackward,
  joinForward,
  selectNodeForward,
  joinUp,
  joinDown,
  lift,
  newlineInCode,
  exitCode,
  createParagraphNear,
  liftEmptyBlock,
  splitBlockAs,
  splitBlock,
  splitBlockKeepMarks,
  selectParentNode,
  selectAll,
  selectTextblockStart,
  selectTextblockEnd,
  wrapIn,
  setBlockType,
  toggleMark,
  autoJoin,
  chainCommands,
  pcBaseKeymap,
  macBaseKeymap,
  baseKeymap
} from 'prosemirror-commands'

Macchiato.externalModules = {
  ...Macchiato.externalModules,
  'prosemirror-state': {
    EditorState,
    Selection,
    SelectionRange,
    TextSelection,
    NodeSelection,
    AllSelection,
    Transaction,
    Plugin,
    PluginKey
  },
  'prosemirror-view': {
    EditorView,
    Decoration,
    DecorationSet
  },
  'prosemirror-model': {
    Node,
    ResolvedPos,
    NodeRange,
    Fragment,
    Slice,
    ReplaceError,
    Mark,
    Schema,
    NodeType,
    MarkType,
    ContentMatch,
    DOMParser,
    DOMSerializer
  },
  'prosemirror-schema-basic': {
    schema
  },
  'prosemirror-schema-list': {
    addListNodes
  },
  'prosemirror-example-setup': {
    exampleSetup
  },
  'prosemirror-history': {
    history,
    undo,
    redo,
    undoNoScroll,
    redoNoScroll,
    undoDepth,
    redoDepth,
    closeHistory
  },
  'prosemirror-keymap': {
    keymap,
    keydownHandler
  },
  'prosemirror-commands': {
    deleteSelection,
    joinBackward,
    joinTextblockBackward,
    joinTextblockForward,
    selectNodeBackward,
    joinForward,
    selectNodeForward,
    joinUp,
    joinDown,
    lift,
    newlineInCode,
    exitCode,
    createParagraphNear,
    liftEmptyBlock,
    splitBlockAs,
    splitBlock,
    splitBlockKeepMarks,
    selectParentNode,
    selectAll,
    selectTextblockStart,
    selectTextblockEnd,
    wrapIn,
    setBlockType,
    toggleMark,
    autoJoin,
    chainCommands,
    pcBaseKeymap,
    macBaseKeymap,
    baseKeymap
  }
}

run-bundle-libraries.js

const files = ['codemirror-bundle.md', 'prosemirror-bundle.md']

const commands = {
  async getLibrarySource() {
    return await Deno.readTextFile('./build/build-libraries/library-source.md')
  },
  async loadBundle(filename) {
    if (!files.includes(filename)) {
      throw new Error('Unknown bundle')
    }
    return await Deno.readTextFile(filename)
  },
  async saveBundle(filename, text) {
    if (!files.includes(filename)) {
      throw new Error('Unknown bundle')
    }
    await Deno.writeTextFile(filename, text)
  },
}

async function handleMessage(e) {
  const [cmd, ...args] = e.data
  const port = e.ports[0]
  try {
    const result = await commands[cmd](...args)
    port.postMessage(result)
  } catch (err) {
    console.error(`Error running \`${cmd}\``, err)
    port.postMessage({error: true})
  }
  port.close()
}

const re = /(?:^|\n)\s*\n`bundle-libraries-entry.js`\n\s*\n```.*?\n(.*?)```\s*(?:\n|$)/s
const runEntry = `
const re = new RegExp(${JSON.stringify(re.source)}, ${JSON.stringify(re.flags)})
addEventListener('message', async e => {
  if (e.data[0] === 'notebook') {
    globalThis.__source = new TextDecoder().decode(e.data[1])
    const entrySrc = globalThis.__source.match(re)[1]
    await import(\`data:text/javascript;base64,\${btoa(entrySrc)}\`)
  }
}, {once: true})
`
const worker = new Worker(`data:text/javascript;base64,${btoa(runEntry)}`, {
  type: 'module',
  permissions: 'none',
})
worker.addEventListener('message', handleMessage)
const data = await Deno.readFile('./bundle-libraries.md')
worker.postMessage(['notebook', data], [data.buffer])

bundle-libraries.js

async function parentRequest(...data) {
  const channel = new MessageChannel()
  const result = await new Promise((resolve, _) => {
    channel.port1.onmessage = (message) => {
      channel.port1.close()
      resolve(message.data)
    }
    postMessage(data, [channel.port2])
  })
  if (result === false) {
    throw new Error(
      `Received false from parent request ${JSON.stringify(data[0])} in worker`
    )
  }
  return result
}

// https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
async function toBase64(bytes) {
  return await new Promise((resolve, reject) => {
    const reader = Object.assign(new FileReader(), {
      onload() { resolve(reader.result.split(',')[1]) },
      onerror() { reject(reader.error) }
    })
    reader.readAsDataURL(new File([bytes], "", { type: 'application/octet-stream' }))
  })
}

async function bundle(librarySource, bundleSourceFilename) {
  const blocks = await Array.fromAsync(readBlocksWithNames(librarySource))
  const localBlocks = await Array.fromAsync(readBlocksWithNames(__source))
  const nodeModules = Object.fromEntries(
    blocks
    .filter(block => (block.name ?? '').startsWith('node_modules/'))
    .map(block => ([block.name.replace(/^node_modules\//, ''), block]))
  )
  const otherBlocks = Object.fromEntries(
    blocks
    .filter(block => !(block.name ?? '').startsWith('node_modules/'))
    .map(block => ([block.name, block]))
  )
  const pkgLock = JSON.parse(librarySource.slice(...otherBlocks['package-lock.json'].contentRange))
  const rollupBlock = nodeModules['@rollup/browser/dist/es/rollup.browser.js']
  const rollupContent = await toBase64(librarySource.slice(...rollupBlock.contentRange))
  const bundleSourceBlock = localBlocks.find(({name}) => name === bundleSourceFilename)
  const bundleSourceContent = __source.slice(...bundleSourceBlock.contentRange)
  const scripts = {'bundle-source.js': bundleSourceContent}
  for (const key of Object.keys(pkgLock.packages).filter(name => name.startsWith('node_modules'))) {
    const name = key.replace(/^node_modules\//, '')
    const packageJsonPath = `${name}/package.json`
    const packageBlock = nodeModules[packageJsonPath]
    const pkg = JSON.parse(librarySource.slice(...packageBlock.contentRange))
    const path = pkg.exports?.import ?? pkg.module
    const mainPath = `${name}/${path.replace(/\.\//, '')}`
    const mainBlock = nodeModules[mainPath]
    if (mainPath?.length && mainBlock) {
      const mainContent = librarySource.slice(...mainBlock.contentRange)
      scripts[name] = mainContent
    } else {
      console.error(`Missing block: ${packageJsonPath} - ${path} - ${mainPath}`)
    }
  }
  const input = 'bundle-source.js'
  const loaderPlugin = {
    name: 'loader',
    resolveId: async source => {
      if (source in scripts) {
        return source
      } else {
        console.log(`Missing script: ${source}`)
      }
    },
    load: async id => {
      if (id in scripts) {
        return scripts[id]
      }
    }
  }
  const plugins = [loaderPlugin]
  const {rollup} = await import(`data:text/javascript;base64,${rollupContent}`)
  const bundle = await rollup({input, plugins})
  const {output} = await bundle.generate({format: 'es'})
  return output[0].code
}

async function updateBundle(bundleNotebook, filename, bundleSource) {
  const blocks = await Array.fromAsync(readBlocksWithNames(bundleNotebook))
  const range = blocks.find(({name}) => name === filename).contentRange
  return bundleNotebook.slice(0, range[0]) + bundleSource.replace(/\n$/, '') + bundleNotebook.slice(range[1])
}

async function build() {
  try {
    const librarySource = await parentRequest('getLibrarySource')
    for (const library of ['codemirror', 'prosemirror']) {
      const mdFilename = `${library}-bundle.md`
      const jsFilename = `${library}-bundle.js`
      const bundleSourceFilename = `${library}-bundle-source.js`
      const output = await bundle(librarySource, bundleSourceFilename)
      const bundleNotebookInput = await parentRequest('loadBundle', mdFilename)
      const bundleNotebookOutput = await updateBundle(bundleNotebookInput, jsFilename, output)
      await parentRequest('saveBundle', mdFilename, bundleNotebookOutput)
    }
    close()
  } catch (err) {
    console.error(err)
    close()
  }
}

await build()

bundle-libraries-entry.js

function* readBlocks(input) {
  const re = /(?:^|\n)([ \t]*)(`{3,}|~{3,})([^\n]*\n)/
  let index = 0
  while (index < input.length) {
    const open = input.substring(index).match(re)
    if (!open) {
      break
    } else if (open[1].length > 0 || open[2][0] === '~') {
      throw new Error(`Invalid open fence at ${index + open.index}`)
    }
    const contentStart = index + open.index + open[0].length
    const close = input.substring(contentStart).match(
      new RegExp(`\n([ ]{0,3})${open[2]}(\`*)[ \t]*\r?(?:\n|$)`)
    )
    if (!(close && close[1] === '')) {
      throw new Error(`Missing or invalid close fence at ${index + open.index}`)
    }
    const contentRange = [contentStart, contentStart + close.index]
    const blockRange = [index + open.index, contentRange.at(-1) + close[0].length]
    yield { blockRange, contentRange, info: open[3].trim() }
    index = blockRange.at(-1)
  }
}

function* readBlocksWithNames(input) {
  for (const block of readBlocks(input)) {
    const match = input.slice(0, block.blockRange[0]).match(
      new RegExp('\\n\\s*\\n\\s*`([^`]+)`\\s*\\n\\s*$')
    )
    yield ({...block, ...(match ? {name: match[1]} : undefined)})
  }
}

async function run(src) {
  globalThis.readBlocks = readBlocks
  globalThis.readBlocksWithNames = readBlocksWithNames
  for (const block of readBlocksWithNames(src)) {
    if (block.name === 'bundle-libraries.js') {
      const blockSrc = src.slice(...block.contentRange)
      await import(`data:text/javascript;base64,${btoa(blockSrc)}`)
    }
  }
}

run(__source)