Full Code of motifland/yfs for AI

main 46248a771784 cached
42 files
50.6 KB
14.9k tokens
29 symbols
1 requests
Download .txt
Repository: motifland/yfs
Branch: main
Commit: 46248a771784
Files: 42
Total size: 50.6 KB

Directory structure:
gitextract_w42u4ncg/

├── .eslintrc
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── examples/
│   └── single-file-sync/
│       ├── .eslintrc.js
│       ├── .gitignore
│       ├── README.md
│       ├── components/
│       │   ├── Editor/
│       │   │   ├── Editor.module.css
│       │   │   ├── GitHub.tsx
│       │   │   ├── index.tsx
│       │   │   ├── options.ts
│       │   │   └── theme.ts
│       │   ├── Footer/
│       │   │   ├── Footer.module.css
│       │   │   └── index.tsx
│       │   ├── Head/
│       │   │   └── index.tsx
│       │   ├── Header/
│       │   │   ├── Header.module.css
│       │   │   └── index.tsx
│       │   └── Navbar/
│       │       ├── Navbar.module.css
│       │       └── index.tsx
│       ├── lib/
│       │   ├── constants.ts
│       │   ├── use-interval.ts
│       │   ├── y-config.ts
│       │   └── y-monaco.ts
│       ├── next-env.d.ts
│       ├── next.config.js
│       ├── package.json
│       ├── pages/
│       │   ├── _app.js
│       │   └── index.tsx
│       ├── styles/
│       │   ├── Shared.module.css
│       │   ├── globals.css
│       │   └── yjs.css
│       ├── tsconfig.json
│       └── types.d.ts
├── package.json
├── src/
│   ├── cache.ts
│   ├── constants.ts
│   ├── helpers.ts
│   ├── index.ts
│   └── yjs.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc
================================================
// next is loading eslintrc from root directory, adding this to avoid eslint rules been overridden
{}


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port
.vscode

================================================
FILE: .prettierignore
================================================
node_modules
.next
build
dist
*.tsbuildinfo
*.gitignore
*.svg
*.lock
*.npmignore
*.sql
*.png
*.jpg
*.jpeg
*.gif
*.ico
*.sh
Dockerfile
Dockerfile.*
.env
.env.*
LICENSE
*.log
.DS_Store
.dockerignore
*.patch
*.toml
*.prisma

================================================
FILE: .prettierrc.json
================================================
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "useTabs": false
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 Motif

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# YFS

Synchronize text files between the browser and the file system using the
[File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)
and [Yjs](https://yjs.dev/).


Blog post explaining the underlying concepts: [Syncing text files between browser and disk using Yjs and the File System Access API](https://motif.land/blog/syncing-text-files-using-yjs-and-the-file-system-access-api).

<br />
<p align="center">
  <a aria-label="NPM version" href="https://www.npmjs.com/package/@yfs/react">
    <img alt="" src="https://badgen.net/npm/v/@yfs/react">
  </a>
  <a aria-label="License" href="https://github.com/motifland/yfs/blob/main/LICENSE">
    <img alt="" src="https://badgen.net/npm/license/@yfs/react">
  </a>
</p>

## Installation

To get started, install the `@yfs/react` package via npm or yarn:

```shell
# npm
npm install @yfs/react

# Yarn
yarn add @yfs/react
```

## Usage

Example:

<!-- prettier-ignore -->
```jsx
import React, { useState } from 'react'
import * as Y from 'yjs'
import useYFS from '@yfs/react'

function Editor () {
  const { setRootDirectory, directoryName, syncDoc } = useYFS()
  const [doc] = useState<Y.Doc>(new Y.Doc())

  return (
    <div>
      <button
        onClick={() => {
          if (!directoryName) {
            setRootDirectory(true)
          } else {
            syncDoc('my-file.md', doc)
          }
        }}
      >
        Sync
      </button>
      {/* Editor code... */}
    </div>
  )
}
```

## Authors

This library is created by the team behind [Motif](https://motif.land)
([@motifland](https://twitter.com/motifland)).

- Michael Fester ([@michaelfester](https://twitter.com/michaelfester))

It is based on the great work by [Kevin Jahns](https://twitter.com/kevin_jahns)
on [Yjs](https://yjs.dev/).

## License

MIT


================================================
FILE: examples/single-file-sync/.eslintrc.js
================================================
/* eslint-env node */
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: ['eslint:recommended', 'plugin:react/recommended', 'next'],
  ignorePatterns: ['public/**/*'],
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: ['react', 'react-hooks'],
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': [
      'error',
      { additionalHooks: '(useDeepCompareEffect)' },
    ],
    '@next/next/no-img-element': 'off',
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
  overrides: [
    {
      files: ['*.ts', '*.tsx'],
      parser: '@typescript-eslint/parser',
      plugins: ['@typescript-eslint'],
      extends: ['plugin:@typescript-eslint/recommended'],
      rules: {
        '@typescript-eslint/no-unused-vars': 'error',
        '@typescript-eslint/no-explicit-any': 'off',
        '@typescript-eslint/explicit-module-boundary-types': 'off',
      },
    },
  ],
}


================================================
FILE: examples/single-file-sync/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel


================================================
FILE: examples/single-file-sync/README.md
================================================
This is a sample app showcasing a browser-based editor
([Monaco](https://microsoft.github.io/monaco-editor/)) syncing its content with
the file system. It is using the
[File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API),
in conjunction with the [Yjs](https://yjs.dev/) CRDT implementation.

## Getting Started

First, install the dependencies:

```bash
yarn
```

Then, run the development server:

```bash
yarn dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the
result.


================================================
FILE: examples/single-file-sync/components/Editor/Editor.module.css
================================================
.editor {
  width: 100%;
  height: 100%;
}


================================================
FILE: examples/single-file-sync/components/Editor/GitHub.tsx
================================================
import * as React from 'react'

const GitHub = (props: any) => {
  return (
    <svg
      width={24}
      height={24}
      fill="none"
      viewBox="0 0 24 24"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585C20.565 22.347 24 17.733 24 12.303 24 5.506 18.63 0 12 0z"
        fill="#1C1917"
      />
    </svg>
  )
}

export default GitHub


================================================
FILE: examples/single-file-sync/components/Editor/index.tsx
================================================
import React, { useCallback } from 'react'
import MonacoEditor from '@monaco-editor/react'
import theme from './theme'
import options from './options'

export default function Editor ({
  onDidMount,
}: {
  onDidMount: (monaco: any, editor: any) => void
}) {
  const onEditorDidMount = useCallback(
    (editor: any, monaco: any) => {
      monaco.editor.defineTheme('custom', theme)
      monaco.editor.setTheme('custom')
      onDidMount(monaco, editor)
    },
    [onDidMount],
  )

  return (
    <MonacoEditor
      height="100%"
      defaultLanguage="markdown"
      defaultValue="# Welcome to Yfs"
      onMount={onEditorDidMount}
      loading={<></>}
      options={options}
    />
  )
}


================================================
FILE: examples/single-file-sync/components/Editor/options.ts
================================================
const options = {
  codeLens: false,
  fontSize: 14,
  lineHeight: 24,
  lineNumbers: 'off',
  padding: {
    top: 30,
    bottom: 30
  },
  selectionHighlight: false,
  wordWrap: 'on',
  folding: false,
  fontFamily:
    'Menlo, ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
  hideCursorInOverviewRuler: true,
  glyphMargin: false,
  lightbulb: { enabled: false },
  lineDecorationsWidth: 20,
  minimap: { enabled: false },
  renderLineHighlight: 'none',
  roundedSelection: true,
  scrollbar: {
    alwaysConsumeMouseWheel: false,
    horizontal: 'hidden',
    vertical: 'hidden',
    useShadows: false,
    verticalHasArrows: false,
    verticalScrollbarSize: 0
  },
  scrollBeyondLastLine: false,
  smoothScrolling: true,
  snippetSuggestions: 'none',
  suggest: {
    showIcons: false
  },
  tabCompletion: 'off',
  tabSize: 2,
  wordBasedSuggestions: false,
  wrappingStrategy: 'advanced'
}

export default options


================================================
FILE: examples/single-file-sync/components/Editor/theme.ts
================================================
const theme = {
  base: 'vs',
  inherit: true,
  rules: [
    { token: '', foreground: '0F172A' },
    { token: 'invalid', foreground: '0F172A' },
    { token: 'emphasis', fontStyle: 'italic' },
    { token: 'strong', fontStyle: 'bold' },
    { token: 'variable', foreground: '5c6773' },
    { token: 'variable.predefined', foreground: '5c6773' },
    { token: 'constant', foreground: '854D0E' },
    { token: 'comment', foreground: 'abb0b6', fontStyle: 'italic' },
    { token: 'number', foreground: '854D0E' },
    { token: 'number.hex', foreground: '854D0E' },
    { token: 'regexp', foreground: '4dbf99' },
    { token: 'annotation', foreground: '701A75' },
    { token: 'type', foreground: '701A75' },
    { token: 'delimiter', foreground: '5c6773' },
    { token: 'delimiter.html', foreground: '5c6773' },
    { token: 'delimiter.xml', foreground: '5c6773' },
    { token: 'tag', foreground: '701A75' },
    { token: 'tag.id.jade', foreground: 'C026D3' },
    { token: 'tag.class.jade', foreground: 'C026D3' },
    { token: 'meta.scss', foreground: 'C026D3' },
    { token: 'metatag', foreground: 'C026D3' },
    { token: 'metatag.content.html', foreground: '0E7490' },
    { token: 'metatag.html', foreground: 'C026D3' },
    { token: 'metatag.xml', foreground: 'C026D3' },
    { token: 'metatag.php', fontStyle: 'bold' },
    { token: 'key', foreground: '701A75' },
    { token: 'string.key.json', foreground: '701A75' },
    { token: 'string.value.json', foreground: '0E7490' },
    { token: 'attribute.name', foreground: '854D0E' },
    { token: 'attribute.value', foreground: '0451A5' },
    { token: 'attribute.value.number', foreground: 'abb0b6' },
    { token: 'attribute.value.unit', foreground: '0E7490' },
    { token: 'attribute.value.html', foreground: '0E7490' },
    { token: 'attribute.value.xml', foreground: '0E7490' },
    { token: 'string', foreground: '0F172A' },
    { token: 'string.html', foreground: '0E7490' },
    { token: 'string.sql', foreground: '0E7490' },
    { token: 'string.yaml', foreground: '0E7490' },
    { token: 'string.js', foreground: '0E7490' },
    { token: 'string.mdx', foreground: '0E7490' },
    { token: 'string.link', foreground: '0369A1' },
    { token: 'string.heading', fontStyle: 'bold' },
    { token: 'string.target', foreground: 'A1A1AA' },
    { token: 'string.codedelimiter.mdx', foreground: 'A1A1AA' },
    { token: 'keyword', foreground: '854d0e' },
    { token: 'keyword.json', foreground: '854d0e' },
    { token: 'keyword.flow', foreground: '854d0e' },
    { token: 'keyword.flow.scss', foreground: '854d0e' },
    { token: 'operator.scss', foreground: '666666' },
    { token: 'operator.sql', foreground: '778899' },
    { token: 'operator.swift', foreground: '666666' },
    { token: 'predefined.sql', foreground: 'FF00FF' }
  ],
  // Cf. https://github.com/Microsoft/monaco-editor/blob/main/test/playground.generated/customizing-the-appearence-exposed-colors.html
  colors: {
    'editor.background': '#FFFFFF',
    'editor.foreground': '#1c1917',
    'editorIndentGuide.background': '#E4E4E7',
    'editorIndentGuide.activeBackground': '#A1A1AA',
    'editorCursor.foreground': '#51A7F8',
    'editor.selectionBackground': '#CFDFEC',
    'editor.inactiveSelectionBackground': '#F5F5F5',
    'editorLineNumber.foreground': '#D1D5DB'
  }
}

export default theme


================================================
FILE: examples/single-file-sync/components/Footer/Footer.module.css
================================================
.syncFooter {
  display: flex;
  flex-direction: row;
  gap: 10px;
  align-items: center;
  align-self: flex-start;
  padding: 0 6px;
  font-size: var(--font-size-sm);
  color: var(--text-light);
}

.syncFooter p {
  margin: 0;
  padding: 0;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 9999px;
}

.dotBlue {
  background-color: #15e3ff;
}

.dotOrange {
  background-color: #f7c444;
}


================================================
FILE: examples/single-file-sync/components/Footer/index.tsx
================================================
import React from 'react'
import styles from './Footer.module.css'
import sharedStyles from '../../styles/Shared.module.css'

export default function Footer ({
  directoryName,
  isWritePermissionGranted,
  unsetRootDirectory,
  grantWritePermission,
}: {
  directoryName: string | undefined
  isWritePermissionGranted: boolean
  unsetRootDirectory: () => void
  grantWritePermission: () => void
}) {
  if (!directoryName) {
    return <></>
  }

  return (
    <div className={styles.syncFooter}>
      <div
        className={`${styles.dot} ${
          isWritePermissionGranted ? styles.dotBlue : styles.dotOrange
        }`}
      />
      {isWritePermissionGranted && (
        <>
          <p>
            Syncing with folder{' '}
            <span style={{ fontStyle: 'italic' }}>{directoryName}</span>.
          </p>
          <a className={sharedStyles.subtleLink} onClick={unsetRootDirectory}>
            Disconnect
          </a>
        </>
      )}
      {!isWritePermissionGranted && (
        <>
          <p>
            Syncing with folder{' '}
            <span style={{ fontStyle: 'italic' }}>{directoryName}</span> is
            paused.
          </p>
          <a className={sharedStyles.subtleLink} onClick={grantWritePermission}>
            Grant permissions
          </a>
        </>
      )}
    </div>
  )
}


================================================
FILE: examples/single-file-sync/components/Head/index.tsx
================================================
import React from 'react'
import NextHead from 'next/head'

export default function Head () {
  return (
    <NextHead>
      <title>File Sync Demo</title>
      <meta
        name='description'
        content='Synchronize text files between the browser and the file system'
      />
      <link rel='icon' href='/favicon.ico' />
    </NextHead>
  )
}


================================================
FILE: examples/single-file-sync/components/Header/Header.module.css
================================================
.cta {
  display: inline-block;
  color: white;
  background-color: var(--button-dark);
  outline: 0 !important;
  outline-offset: 2px;
  box-shadow: none;
  font-weight: 500;
  font-size: var(--font-size-base);
  padding: 10px 20px;
  border-radius: 5px;
  cursor: pointer;
  -webkit-appearance: button;
  border-style: solid;
  border-width: 0;
  transition-duration: 150ms;
}

.cta:hover {
  opacity: 0.9;
  box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}

.warning {
  color: var(--text-warning);
  text-align: center;
}


================================================
FILE: examples/single-file-sync/components/Header/index.tsx
================================================
import React from 'react'
import styles from './Header.module.css'

export default function Header ({
  isFSAPISupported,
  directoryName,
  setRootDirectory,
}: {
  isFSAPISupported: boolean
  directoryName: string | undefined
  setRootDirectory: (writable: boolean) => void
}) {
  return (
    <div style={{ position: 'absolute', top: -60 }}>
      {isFSAPISupported && !directoryName && (
        <button
          className={styles.cta}
          onClick={() => {
            setRootDirectory(true)
          }}
        >
          Select folder
        </button>
      )}
      {!isFSAPISupported && (
        <p className={styles.warning}>
          The File System Access API is currently not supported in this browser.
        </p>
      )}
    </div>
  )
}


================================================
FILE: examples/single-file-sync/components/Navbar/Navbar.module.css
================================================
.nav {
  display: flex;
  flex-direction: row;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding: 1.5rem;
}

.nav div:first-child {
  flex: 1;
  display: flex;
  gap: 5px;
  flex-direction: column;
}

.nav div:first-child h1,
.nav div:first-child p {
  margin: 0;
}

.nav div:first-child h1 {
  font-size: 18px;
  color: var(--text-dark);
}

.nav div:first-child p {
  font-size: var(--font-size-sm);
  color: var(--text-light);
}

.nav div:last-child {
  display: flex;
  gap: 25px;
  flex-direction: row;
  align-items: center;
}

.nav div:last-child a {
  font-size: var(--font-size-sm);
  color: var(--text-dark);
}


================================================
FILE: examples/single-file-sync/components/Navbar/index.tsx
================================================
import React from 'react'
import GitHub from 'components/Editor/GitHub'
import styles from './Navbar.module.css'
import sharedStyles from '../../styles/Shared.module.css'

export default function Navbar () {
  return (
    <nav className={styles.nav}>
      <div>
        <h1>File Sync Demo</h1>
        <p>
          Using the{' '}
          <a
            className={sharedStyles.subtleLink}
            href='https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API'
            target='_blank'
            rel='noreferrer'
          >
            File System Access API
          </a>{' '}
          +{' '}
          <a
            className={sharedStyles.subtleLink}
            href='https://yjs.dev/'
            target='_blank'
            rel='noreferrer'
          >
            Yjs
          </a>
        </p>
      </div>
      <div>
        <a
          href='https://motif.land/blog/syncing-text-files-using-yjs-and-the-file-system-access-api'
          target='_blank'
          rel='noreferrer'
        >
          Read blog post ↗
        </a>
        <a
          href='https://github.com/motifland/yfs'
          target='_blank'
          rel='noreferrer'
        >
          <GitHub />
        </a>
      </div>
    </nav>
  )
}


================================================
FILE: examples/single-file-sync/lib/constants.ts
================================================
export const YDOC_UPDATE_ORIGIN_CURRENT_EDITOR =
  'YDOC_UPDATE_ORIGIN_CURRENT_EDITOR'

export const ROOM_ID = 'yfs-sample-room'
export const TEST_FILE_NAME = 'yfs-test.md'

export const WEBRTC_SIGNALING_SERVERS = [
  'wss://motif-signaling.fly.dev/',
]


================================================
FILE: examples/single-file-sync/lib/use-interval.ts
================================================
import { useEffect, useRef } from 'react'

export const useInterval = (callback: () => void, delay: number | null) => {
  const savedCallback = useRef<any>()

  useEffect(() => {
    savedCallback.current = callback
  }, [callback])

  useEffect(() => {
    const tick = () => {
      savedCallback.current()
    }
    if (delay !== null) {
      const id = setInterval(tick, delay)
      return () => clearInterval(id)
    }
  }, [delay])
}


================================================
FILE: examples/single-file-sync/lib/y-config.ts
================================================
import * as Y from 'yjs'
import * as awarenessProtocol from 'y-protocols/awareness.js'
import { WebrtcProvider } from 'y-webrtc'
import { IndexeddbPersistence } from 'y-indexeddb'
import * as mutex from 'lib0/mutex.js'
import { MonacoBinding } from './y-monaco'
import { ROOM_ID, WEBRTC_SIGNALING_SERVERS } from './constants'

export const YDOC_UPDATE_ORIGIN_CURRENT_EDITOR =
  'YDOC_UPDATE_ORIGIN_CURRENT_EDITOR'
export const YDOC_UPDATE_ORIGIN_PROGRAMMATIC = 'YDOC_UPDATE_ORIGIN_PROGRAMMATIC'

export class YConfig {
  roomId: string
  monaco: any
  model: any
  editor: any
  userId: number
  doc: Y.Doc | undefined
  initialEncodedYDoc: string | undefined
  awareness: awarenessProtocol.Awareness | undefined
  webRTCProvider: WebrtcProvider | undefined
  indexedDBPersistence: IndexeddbPersistence | undefined
  monacoBinding: MonacoBinding | undefined
  mux: mutex.mutex
  updateTimeoutId: ReturnType<typeof setTimeout> | null

  constructor (
    monaco: any,
    editor: any,
    userId: number
  ) {
    this.roomId = ROOM_ID
    // this.onNonProgrammaticDocUpdateImmediate = onNonProgrammaticDocUpdateImmediate
    // this.onLocalEditorUpdateDebounced = onLocalEditorUpdateDebounced
    this.monaco = monaco
    this.editor = editor
    this.model = editor.getModel()
    this.userId = userId
    this.mux = mutex.createMutex()
    this.updateTimeoutId = null
    this.initYDoc()
  }

  initYDoc (): void {
    this.doc = new Y.Doc()

    this.awareness = new awarenessProtocol.Awareness(this.doc)

    this.webRTCProvider = new WebrtcProvider(this.roomId, this.doc, {
      signaling: WEBRTC_SIGNALING_SERVERS,
      password: null,
      awareness: this.awareness,
      maxConns: 50 + Math.floor(Math.random() * 15),
      filterBcConns: true,
      peerOpts: {}
    })

    this.monacoBinding = new MonacoBinding(
      this.monaco,
      this.doc.getText(),
      this.model,
      this.editor,
      this.awareness,
      this.userId
    )

    this.indexedDBPersistence = new IndexeddbPersistence(
      'y-indexeddb',
      this.doc
    )

    if (!this.webRTCProvider.connected) {
      this.webRTCProvider.connect()
    }
  }

  destroy (): void {
    this.awareness?.destroy()
    this.monacoBinding?.destroy()
    this.webRTCProvider?.disconnect()
    this.webRTCProvider?.destroy()
    this.doc?.destroy()
  }
}


================================================
FILE: examples/single-file-sync/lib/y-monaco.ts
================================================
import * as Y from 'yjs'
import * as error from 'lib0/error.js'
import { createMutex, mutex } from 'lib0/mutex.js'
import * as awarenessProtocol from 'y-protocols/awareness.js'
import { YDOC_UPDATE_ORIGIN_CURRENT_EDITOR } from './constants'

type Direction = any
type Selection = {
  start: Y.RelativePosition
  end: Y.RelativePosition
  direction: Direction
}

class RelativeSelection {
  start: Y.RelativePosition
  end: Y.RelativePosition
  direction: Direction

  constructor (
    start: Y.RelativePosition,
    end: Y.RelativePosition,
    direction: Direction,
  ) {
    this.start = start
    this.end = end
    this.direction = direction
  }
}

const createRelativeSelection = (
  editor: any,
  monacoModel: any,
  type: Y.AbstractType<any>,
) => {
  const sel = editor.getSelection()
  if (sel !== null) {
    const startPos = sel.getStartPosition()
    const endPos = sel.getEndPosition()
    const start = Y.createRelativePositionFromTypeIndex(
      type,
      monacoModel.getOffsetAt(startPos),
    )
    const end = Y.createRelativePositionFromTypeIndex(
      type,
      monacoModel.getOffsetAt(endPos),
    )
    return new RelativeSelection(start, end, sel.getDirection())
  }
  return null
}

const createMonacoSelectionFromRelativeSelection = (
  monaco: any,
  editor: any,
  type: Y.AbstractType<any>,
  relSel: Selection,
  doc: Y.Doc,
) => {
  const start = Y.createAbsolutePositionFromRelativePosition(relSel.start, doc)
  const end = Y.createAbsolutePositionFromRelativePosition(relSel.end, doc)
  if (
    start !== null &&
    end !== null &&
    start.type === type &&
    end.type === type
  ) {
    const model = editor.getModel()
    if (!model) {
      return null
    }
    const startPos = model.getPositionAt(start.index)
    const endPos = model.getPositionAt(end.index)
    return monaco.Selection.createWithDirection(
      startPos.lineNumber,
      startPos.column,
      endPos.lineNumber,
      endPos.column,
      relSel.direction,
    )
  }
  return null
}

export class MonacoBinding {
  doc: Y.Doc
  ytext: Y.Text
  monacoModel: any
  editor: any
  mux: mutex
  color: number
  awareness: awarenessProtocol.Awareness | undefined
  _savedSelections: Map<any, Selection>
  _decorations: any
  _rerenderDecorations: any
  _monacoChangeHandler: any
  _beforeTransaction: () => void
  _ytextObserver: (event: Y.YTextEvent) => void

  constructor (
    monaco: any,
    ytext: Y.Text,
    monacoModel: any,
    editor: any,
    awareness: awarenessProtocol.Awareness,
    userId: number,
  ) {
    this.doc = ytext.doc as Y.Doc
    this.ytext = ytext
    this.monacoModel = monacoModel
    this.editor = editor
    this.mux = createMutex()
    this._savedSelections = new Map()
    this.color = userId % 8

    this._beforeTransaction = () => {
      this.mux(() => {
        this._savedSelections = new Map()
        if (editor.getModel() === monacoModel) {
          const rsel = createRelativeSelection(editor, monacoModel, ytext)
          if (rsel !== null) {
            this._savedSelections.set(editor, rsel)
          }
        }
      })
    }

    this.doc.on('beforeAllTransactions', this._beforeTransaction)

    this._decorations = new Map()

    this._rerenderDecorations = () => {
      if (awareness && editor.getModel() === monacoModel) {
        // render decorations
        const currentDecorations = this._decorations.get(editor) || []
        const newDecorations: any = []
        awareness.getStates().forEach((state, clientID) => {
          if (
            clientID !== this.doc.clientID &&
            state.selection != null &&
            state.selection.anchor != null &&
            state.selection.head != null
          ) {
            const anchorAbs = Y.createAbsolutePositionFromRelativePosition(
              state.selection.anchor,
              this.doc,
            )
            const headAbs = Y.createAbsolutePositionFromRelativePosition(
              state.selection.head,
              this.doc,
            )
            if (
              anchorAbs !== null &&
              headAbs !== null &&
              anchorAbs.type === ytext &&
              headAbs.type === ytext
            ) {
              let start, end, afterContentClassName, beforeContentClassName
              if (anchorAbs.index < headAbs.index) {
                start = monacoModel.getPositionAt(anchorAbs.index)
                end = monacoModel.getPositionAt(headAbs.index)
                afterContentClassName = `ySelectionHead yBorder-${this.color}`
                beforeContentClassName = null
              } else {
                start = monacoModel.getPositionAt(headAbs.index)
                end = monacoModel.getPositionAt(anchorAbs.index)
                afterContentClassName = null
                beforeContentClassName = `ySelectionHead yBorder-${this.color}`
              }
              newDecorations.push({
                range: new monaco.Range(
                  start.lineNumber,
                  start.column,
                  end.lineNumber,
                  end.column,
                ),
                options: {
                  className: `ySelection-${this.color}`,
                  afterContentClassName,
                  beforeContentClassName,
                },
              })
            }
          }
        })
        this._decorations.set(
          editor,
          editor.deltaDecorations(currentDecorations, newDecorations),
        )
      } else {
        // ignore decorations
        this._decorations.delete(editor)
      }
    }

    this._ytextObserver = event => {
      this.mux(() => {
        let index = 0
        event.delta.forEach(op => {
          if (op.retain !== undefined) {
            index += op.retain
          } else if (op.insert !== undefined) {
            const pos = monacoModel.getPositionAt(index)
            const range = new monaco.Selection(
              pos.lineNumber,
              pos.column,
              pos.lineNumber,
              pos.column,
            )
            monacoModel.applyEdits([{ range, text: op.insert as string }])
            index += (op.insert as any).length
          } else if (op.delete !== undefined) {
            const pos = monacoModel.getPositionAt(index)
            const endPos = monacoModel.getPositionAt(index + op.delete)
            const range = new monaco.Selection(
              pos.lineNumber,
              pos.column,
              endPos.lineNumber,
              endPos.column,
            )
            monacoModel.applyEdits([{ range, text: '' }])
          } else {
            throw error.unexpectedCase()
          }
        })
        this._savedSelections.forEach((rsel, editor) => {
          const sel = createMonacoSelectionFromRelativeSelection(
            monaco,
            editor,
            ytext,
            rsel,
            this.doc,
          )
          if (sel !== null) {
            editor.setSelection(sel)
          }
        })
      })
      this._rerenderDecorations()
    }

    ytext.observe(this._ytextObserver)
    monacoModel.setValue(ytext.toString())

    this._monacoChangeHandler = monacoModel.onDidChangeContent((event: any) => {
      // Apply changes from right to left
      this.mux(() => {
        this.doc.transact(() => {
          event.changes
            .sort(
              (change1: any, change2: any) =>
                change2.rangeOffset - change1.rangeOffset,
            )
            .forEach((change: any) => {
              ytext.delete(change.rangeOffset, change.rangeLength)
              ytext.insert(change.rangeOffset, change.text)
            })
        }, YDOC_UPDATE_ORIGIN_CURRENT_EDITOR)
      })
    })

    monacoModel.onWillDispose(() => {
      this.destroy()
    })

    if (awareness) {
      editor.onDidChangeCursorSelection(() => {
        if (editor.getModel() === monacoModel) {
          const sel = editor.getSelection()
          if (sel === null) {
            return
          }
          let anchor = monacoModel.getOffsetAt(sel.getStartPosition())
          let head = monacoModel.getOffsetAt(sel.getEndPosition())
          if (sel.getDirection() === monaco.SelectionDirection.RTL) {
            const tmp = anchor
            anchor = head
            head = tmp
          }
          awareness.setLocalStateField('selection', {
            anchor: Y.createRelativePositionFromTypeIndex(ytext, anchor),
            head: Y.createRelativePositionFromTypeIndex(ytext, head),
          })
        }
      })
      awareness.on('change', this._rerenderDecorations)
      this.awareness = awareness
    }
  }

  destroy (): void {
    this._monacoChangeHandler.dispose()
    this.ytext.unobserve(this._ytextObserver)
    this.doc.off('beforeAllTransactions', this._beforeTransaction)
    if (this.awareness) {
      this.awareness.off('change', this._rerenderDecorations)
    }
  }
}


================================================
FILE: examples/single-file-sync/next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.


================================================
FILE: examples/single-file-sync/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
}

module.exports = nextConfig


================================================
FILE: examples/single-file-sync/package.json
================================================
{
  "name": "yfs-single-file-sync-example",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@monaco-editor/react": "^4.4.4",
    "@yfs/react": "^0.0.15",
    "lib0": "^0.2.51",
    "next": "12.1.5",
    "react": "18.1.0",
    "react-dom": "18.1.0",
    "y-indexeddb": "^9.0.7",
    "y-webrtc": "^10.2.3",
    "yjs": "^13.5.35"
  },
  "devDependencies": {
    "@types/node": "^17.0.30",
    "@types/react": "^18.0.8",
    "@typescript-eslint/eslint-plugin": "^5.22.0",
    "@typescript-eslint/parser": "^5.22.0",
    "eslint": "8.14.0",
    "eslint-config-next": "12.1.5",
    "typescript": "^4.6.4"
  }
}


================================================
FILE: examples/single-file-sync/pages/_app.js
================================================
import '../styles/globals.css'
import '../styles/yjs.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp


================================================
FILE: examples/single-file-sync/pages/index.tsx
================================================
import React, { useCallback, useRef } from 'react'
import useFileSync from '@yfs/react'
import Editor from 'components/Editor'
import Head from 'components/Head'
import Navbar from 'components/Navbar'
import Footer from 'components/Footer'
import Header from 'components/Header'
import { useInterval } from 'lib/use-interval'
import styles from '../styles/Shared.module.css'
import { YConfig } from 'lib/y-config'
import { TEST_FILE_NAME } from 'lib/constants'

export default function Home () {
  const config = useRef<YConfig | undefined>(undefined)
  const {
    isSupported,
    setRootDirectory,
    unsetRootDirectory,
    grantWritePermission,
    directoryName,
    isWritePermissionGranted,
    syncDoc
  } = useFileSync()

  const onEditorDidMount = useCallback((monaco: any, editor: any) => {
    config.current?.destroy()
    config.current = new YConfig(monaco, editor, Math.floor(Math.random() * 8))
  }, [])

  const sync = useCallback(() => {
    if (!config.current?.doc) {
      return
    }
    syncDoc(TEST_FILE_NAME, config.current.doc)
  }, [syncDoc])

  useInterval(sync, isWritePermissionGranted ? 10000 : null)

  return (
    <div className={styles.container}>
      <Head />
      <Navbar />
      <main className={styles.main}>
        <Header
          isFSAPISupported={isSupported}
          directoryName={directoryName}
          setRootDirectory={setRootDirectory}
        />
        <div className={styles.editorContainer}>
          <Editor onDidMount={onEditorDidMount} />
        </div>
        <Footer
          directoryName={directoryName}
          isWritePermissionGranted={isWritePermissionGranted}
          unsetRootDirectory={unsetRootDirectory}
          grantWritePermission={grantWritePermission}
        />
      </main>
    </div>
  )
}


================================================
FILE: examples/single-file-sync/styles/Shared.module.css
================================================
.container {
  min-height: 100vh;
  padding: 2rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  color: var(--text-dark);
}

.container {
  padding: 0;
}

.subtleLink {
  text-decoration: underline dotted var(--text-extralight);
  cursor: pointer;
}

.subtleLink:hover {
  opacity: 0.9;
}

.main {
  position: relative;
  padding: 0 1.5rem 2rem 1.5rem;
  width: 100%;
  max-width: 980px;
  margin-top: 50px;
  display: flex;
  gap: 10px;
  flex-direction: column;
  align-items: center;
}

.editorContainer {
  width: 100%;
  height: 100vh;
  max-height: 480px;
  padding: 0 1rem;
  border-radius: 5px;
  border-width: 1px;
  overflow: hidden;
  background-color: white;
  border-color: var(--border-light);
  border-style: solid;
}


================================================
FILE: examples/single-file-sync/styles/globals.css
================================================
@import url('https://rsms.me/inter/inter.css');

:root {
  --background: #eeede9;
  --border-light: #e3e3e3;
  --text-dark: #1c1917;
  --text-light: #78716c;
  --text-extralight: #a8a29e;
  --text-warning: #f97316;
  --font-size-base: 16px;
  --font-size-sm: 14px;
  --button-dark: #0f172a;
  --orange-light: #ffedd5;
  --green-light: #dcfce7;
  --sky-light: #e0f2fe;
  --violet-light: #ede9fe;
  --rose-light: #ffe4e6;
  --amber-light: #fef3c7;
  --lime-light: #ecfccb;
  --fuchsia-light: #fae8ff;
  --orange: #fb923c;
  --green: #4ade80;
  --sky: #38bdf8;
  --violet: #a78bfa;
  --rose: #fb7185;
  --amber: #fbbf24;
  --lime: #a3e635;
  --fuchsia: #e879f9;
}

html,
body {
  padding: 0;
  margin: 0;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
    Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  background-color: var(--background);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}


================================================
FILE: examples/single-file-sync/styles/yjs.css
================================================
/* Selection */

.ySelection-0 {
  background-color: var(--orange-light);
}
.ySelection-1 {
  background-color: var(--green-light);
}
.ySelection-2 {
  background-color: var(--sky-light);
}
.ySelection-3 {
  background-color: var(--violet-light);
}
.ySelection-4 {
  background-color: var(--rose-light);
}
.ySelection-5 {
  background-color: var(--amber-light);
}
.ySelection-6 {
  background-color: var(--lime-light);
}
.ySelection-7 {
  background-color: var(--fuchsia-light);
}

/* Background */

.yBackground-0 {
  background-color: var(--orange);
}
.yBackground-1 {
  background-color: var(--green);
}
.yBackground-2 {
  background-color: var(--sky);
}
.yBackground-3 {
  background-color: var(--violet);
}
.yBackground-4 {
  background-color: var(--rose);
}
.yBackground-5 {
  background-color: var(--amber);
}
.yBackground-6 {
  background-color: var(--lime);
}
.yBackground-7 {
  background-color: var(--fuchsia);
}

/* Border */

.yBorder-0 {
  border-color: var(--orange);
}
.yBorder-1 {
  border-color: var(--green);
}
.yBorder-2 {
  border-color: var(--sky);
}
.yBorder-3 {
  border-color: var(--violet);
}
.yBorder-4 {
  border-color: var(--rose);
}
.yBorder-5 {
  border-color: var(--amber);
}
.yBorder-6 {
  border-color: var(--lime);
}
.yBorder-7 {
  border-color: var(--fuchsia);
}

/* Selection head */

.ySelectionHead {
  position: absolute;
  box-sizing: border-box;
  height: 100%;
  border-left-width: 2px;
  border-top-width: 1px;
  border-bottom-width: 1px;
}


================================================
FILE: examples/single-file-sync/tsconfig.json
================================================
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./",
    "target": "esnext",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext",
      "webworker"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "downlevelIteration": true,
    "incremental": true
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ]
}


================================================
FILE: examples/single-file-sync/types.d.ts
================================================
declare module 'y-monaco'


================================================
FILE: package.json
================================================
{
  "name": "@yfs/react",
  "version": "0.1.0",
  "description": "Synchronize text files between the browser and the file system",
  "license": "MIT",
  "repository": "git@github.com:motifland/yfs.git",
  "author": {
    "name": "Michael Fester",
    "email": "michael.fester@gmail.com",
    "url": "https://motif.land"
  },
  "sideEffects": false,
  "type": "module",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "rimraf ./dist && tsc",
    "prepublish": "npm run tsc",
    "clean": "rimraf ./dist"
  },
  "dependencies": {
    "diff": "^5.0.0",
    "idb-keyval": "^6.1.0",
    "react": "18.1.0",
    "yjs": "^13.5.35"
  },
  "devDependencies": {
    "@types/diff": "^5.0.2",
    "@types/node": "^17.0.30",
    "@types/react": "^18.0.8",
    "typescript": "^4.6.4"
  }
}


================================================
FILE: src/cache.ts
================================================
import { set as idbSet, get as idbGet } from 'idb-keyval'
import { STORE_KEY_CACHED_FS_FILE } from './constants'

type LastWriteCacheData = {
  name: string
  content: string
  lastModified: number
}

const getLastWriteCacheKey = (name: string) => {
  return `${STORE_KEY_CACHED_FS_FILE}-${name}`
}

export const setLastWriteCacheData = async (
  name: string,
  content: string,
  lastModified: number
) => {
  await idbSet(
    getLastWriteCacheKey(name),
    JSON.stringify({
      name,
      content,
      lastModified
    })
  )
}

export const getLastWriteCacheData = async (
  name: string
): Promise<LastWriteCacheData | undefined> => {
  const jsonFile = await idbGet(getLastWriteCacheKey(name))
  if (jsonFile) {
    return JSON.parse(jsonFile)
  }
  return undefined
}


================================================
FILE: src/constants.ts
================================================
export const STORE_KEY_DIRECTORY_HANDLE = 'STORE_KEY_DIRECTORY_HANDLE'
export const STORE_KEY_CACHED_FS_FILE = 'STORE_KEY_CACHED_FS_FILE'


================================================
FILE: src/helpers.ts
================================================
export type HandleWithPath = {
  handle: FileSystemHandle
  path: string[]
  type: 'file' | 'directory'
}

const readWriteOptions = { mode: 'readwrite' }

export const isReadWritePermissionGranted = async (
  handle: FileSystemFileHandle | FileSystemDirectoryHandle
) => {
  return (await (handle as any).queryPermission(readWriteOptions)) === 'granted'
}

export const askReadWritePermissionsIfNeeded = async (
  handle: FileSystemFileHandle | FileSystemDirectoryHandle
) => {
  if (await isReadWritePermissionGranted(handle)) {
    return true
  }

  const permission = await (handle as any).requestPermission(readWriteOptions)
  return permission === 'granted'
}

const createEmptyFileInFolder = async (
  parentDirectoryHandle: FileSystemDirectoryHandle,
  name: string
): Promise<FileSystemFileHandle> => {
  return await parentDirectoryHandle.getFileHandle(name, { create: true })
}

export const createFolderInFolder = async (
  parentDirectoryHandle: FileSystemDirectoryHandle,
  name: string
): Promise<FileSystemDirectoryHandle> => {
  return await parentDirectoryHandle.getDirectoryHandle(name, { create: true })
}

const writeContentToFile = async (
  fileHandle: FileSystemFileHandle,
  content: string
) => {
  const writable = await (fileHandle as any).createWritable()
  await writable.write(content)
  await writable.close()
}

export const writeContentToFileIfChanged = async (
  fsFile: globalThis.File,
  fileHandle: FileSystemFileHandle,
  content: string
) => {
  const fsFileContent = await fsFile.text()
  if (fsFileContent === content) {
    return
  }
  await writeContentToFile(fileHandle, content)
}

export const renameFile = async (
  fsFile: globalThis.File,
  parentDirectoryHandle: FileSystemDirectoryHandle,
  name: string
) => {
  // Move and rename is not currently supported by the FileSystem
  // Access API so we need to do this they manual way by creating
  // a new file and deleting the old one.
  const content = await fsFile.text()
  await createFile(parentDirectoryHandle, name, content)
  await deleteFile(parentDirectoryHandle, fsFile.name)
}

export const moveFile = async (
  fsFile: globalThis.File,
  sourceDirectoryHandle: FileSystemDirectoryHandle,
  destinationDirectoryHandle: FileSystemDirectoryHandle
) => {
  // Same comment as renameFile
  const content = await fsFile.text()
  await createFile(destinationDirectoryHandle, fsFile.name, content)
  await deleteFile(sourceDirectoryHandle, fsFile.name)
}

export const moveFolderContent = async (
  sourceFolderHandle: FileSystemDirectoryHandle,
  destinationFolderHandle: FileSystemDirectoryHandle
) => {
  for await (const handle of (sourceFolderHandle as any).values()) {
    if (handle.kind === 'file') {
      const fsFile = await (handle as FileSystemFileHandle).getFile()
      await moveFile(fsFile, sourceFolderHandle, destinationFolderHandle)
    } else if (handle.kind === 'directory') {
      await moveFolder(handle, sourceFolderHandle, destinationFolderHandle)
    }
  }
}

export const moveFolder = async (
  folderHandle: FileSystemDirectoryHandle,
  parentDirectoryHandle: FileSystemDirectoryHandle,
  destinationFolderHandle: FileSystemDirectoryHandle
) => {
  const newFolderHandle = await createFolderInFolder(
    destinationFolderHandle,
    folderHandle.name
  )
  await moveFolderContent(folderHandle, newFolderHandle)
  await deleteFolder(folderHandle.name, parentDirectoryHandle)
}

export const renameFolder = async (
  folderHandle: FileSystemDirectoryHandle,
  parentDirectoryHandle: FileSystemDirectoryHandle,
  newName: string
) => {
  const newFolderHandle = await createFolderInFolder(
    parentDirectoryHandle,
    newName
  )
  try {
    await moveFolderContent(folderHandle, newFolderHandle)
    await deleteFolder(folderHandle.name, parentDirectoryHandle)
  } catch {
    // Do nothing
  }
}

export const createFile = async (
  parentDirectoryHandle: FileSystemDirectoryHandle,
  name: string,
  content: string
): Promise<FileSystemFileHandle> => {
  const newFileHandle = await createEmptyFileInFolder(
    parentDirectoryHandle,
    name
  )
  await writeContentToFile(newFileHandle, content)
  return newFileHandle
}

export const deleteFile = async (
  parentDirectoryHandle: FileSystemDirectoryHandle,
  name: string
) => {
  await parentDirectoryHandle.removeEntry(name)
}

export const deleteFolder = async (
  name: string,
  parentDirectoryHandle: FileSystemDirectoryHandle
) => {
  await parentDirectoryHandle.removeEntry(name, {
    recursive: true
  })
}

export const getFSFileHandle = async (
  name: string,
  directoryHandle: FileSystemDirectoryHandle
): Promise<FileSystemFileHandle | undefined> => {
  for await (const handle of (directoryHandle as any).values()) {
    const relativePath = (await directoryHandle.resolve(handle)) || []
    if (relativePath?.length === 1 && relativePath[0] === name) {
      return handle
    }
  }
  return undefined
}

export const isHandlesEqual = async (
  handle: FileSystemHandle | undefined,
  otherHandle: FileSystemHandle | undefined
) => {
  if (!handle && !otherHandle) {
    return true
  }

  if (handle && otherHandle) {
    return await (handle as any)?.isSameEntry(otherHandle)
  }

  return false
}

export const isIgnoredPath = (path: string[]): boolean => {
  // Return true if the file at the given path should be ignored for
  // syncing. This is the case currently if the path contains a component
  // that starts with a period, e.g. ".git" or ".DS_Store".
  return !!path.find(p => p.startsWith('.') || p.endsWith('.crswap'))
}

export const isTextMimeType = (file: globalThis.File) => {
  // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
  return file.type.startsWith('text/') || !file.type
}


================================================
FILE: src/index.ts
================================================
import { useCallback, useEffect, useMemo, useState } from 'react'
import { set as idbSet, get as idbGet, del as idbDel } from 'idb-keyval'
import * as Y from 'yjs'
import { STORE_KEY_DIRECTORY_HANDLE } from './constants'
import {
  askReadWritePermissionsIfNeeded,
  createFile,
  getFSFileHandle,
  writeContentToFileIfChanged
} from './helpers'
import { getLastWriteCacheData, setLastWriteCacheData } from './cache'
import { getDeltaOperations } from './yjs'

const useFileSync = () => {
  const [isSupported, setSupported] = useState(false)
  const [isWritePermissionGranted, setWritePermissionGranted] = useState(false)
  const [directoryHandle, setDirectoryHandle] = useState<
    FileSystemDirectoryHandle | undefined
  >(undefined)

  useEffect(() => {
    setSupported(typeof (window as any).showDirectoryPicker === 'function')
  }, [])

  useEffect(() => {
    const loadHandle = async () => {
      const handle = await idbGet(STORE_KEY_DIRECTORY_HANDLE)
      if (handle) {
        setDirectoryHandle(handle)
      }
    }
    loadHandle()
  }, [])

  useEffect(() => {
    if (directoryHandle) {
      idbSet(STORE_KEY_DIRECTORY_HANDLE, directoryHandle)
    }
  }, [directoryHandle])

  const grantWritePermission = useCallback(async () => {
    if (!isSupported || !directoryHandle) {
      return
    }
    try {
      const granted = await askReadWritePermissionsIfNeeded(directoryHandle)
      setWritePermissionGranted(granted)
    } catch {}
  }, [isSupported, directoryHandle])

  const setRootDirectory = useCallback(
    async (withWritePermission: boolean) => {
      if (!isSupported) {
        return
      }
      try {
        const handle = await (window as any).showDirectoryPicker()
        if (handle) {
          setDirectoryHandle(handle)
          if (withWritePermission) {
            const granted = await askReadWritePermissionsIfNeeded(handle)
            setWritePermissionGranted(granted)
          }
        }
      } catch {}
    },
    [isSupported, grantWritePermission]
  )

  const unsetRootDirectory = useCallback(async () => {
    setDirectoryHandle(undefined)
    idbDel(STORE_KEY_DIRECTORY_HANDLE)
  }, [])

  const syncDoc = useCallback(
    async (name: string, doc: Y.Doc) => {
      if (!directoryHandle) {
        return
      }

      const updateFileContent = async (
        file: globalThis.File,
        fileHandle: FileSystemFileHandle,
        newContent: string
      ) => {
        // When we write to the file system, we also save a version
        // in cache in order to be able to watch for subsequent changes
        // to the file.
        await writeContentToFileIfChanged(file, fileHandle, newContent)
        await setLastWriteCacheData(name, newContent, file.lastModified)
      }

      let fileHandle = await getFSFileHandle(name, directoryHandle)
      const docContent = doc.getText().toString()

      if (!fileHandle) {
        // File is not present in the file system, so create it.
        const newFileHandle = await createFile(directoryHandle, name, '')
        const newFile = await newFileHandle.getFile()
        await updateFileContent(newFile, newFileHandle, docContent)
        return
      }

      const file = await fileHandle.getFile()

      // File exists, so compare it with the last-write-cache.
      const lastWriteCacheData = await getLastWriteCacheData(name)

      if (!lastWriteCacheData) {
        // Cached version does not exist. This should never happen. Indeed,
        // even if the user clears the app data, the directory handle will
        // be cleared as well, so the user will be asked to select a directory
        // again, in which case a hard overwrite will happen, and the
        // last-write-cache will be populated. So in case `lastWriteCacheData`
        // does not exist, we can consider this situation as similar to the
        // initial file dump situation and simply overwrite the FS file.
        await updateFileContent(file, fileHandle, docContent)
        return
      }

      // Cached version exists. This allows us to see the changes in the
      // local file, and compute the diff which in turn gives us as
      // state update vector for our CRDT. We can then apply it
      // to the app file for a seamless merging of the two versions.

      if (file.lastModified === lastWriteCacheData.lastModified) {
        // File has not changed in the file system. Since the FS file cache
        // is only set when a project file is synced, this means that the
        // only option is that the app file has changed, in which
        // case it should be written to the FS file.
        await updateFileContent(file, fileHandle, docContent)
        return
      }

      // File has changed in the file system.

      const fileContent = await file.text()
      const lastWriteFileContent = lastWriteCacheData.content
      const deltas = getDeltaOperations(lastWriteFileContent, fileContent)

      if (deltas.length === 0) {
        // Same comment as above: no difference between FS file and
        // and last-write-cache, so just write the app file to FS.
        await updateFileContent(file, fileHandle, docContent)
        return
      }

      // A change has happened in the file, since it differs
      // from the cached version. So we merge it with the app doc.
      doc.getText().applyDelta(deltas)

      const mergedContent = doc.getText().toString()
      await updateFileContent(file, fileHandle, mergedContent)
    },
    [directoryHandle]
  )

  const directoryName = useMemo(() => {
    return directoryHandle?.name
  }, [directoryHandle])

  return {
    isSupported,
    setRootDirectory,
    unsetRootDirectory,
    grantWritePermission,
    isWritePermissionGranted,
    directoryName,
    syncDoc
  }
}

export default useFileSync


================================================
FILE: src/yjs.ts
================================================
import * as Diff from 'diff'

type YDelta = { retain: number } | { delete: number } | { insert: string }

// Compute the set of Yjs delta operations (that is, `insert` and
// `delete`) required to go from initialText to finalText.
// Based on https://github.com/kpdecker/jsdiff.
export const getDeltaOperations = (
  initialText: string,
  finalText: string
): YDelta[] => {
  if (initialText === finalText) {
    return []
  }

  const edits = Diff.diffChars(initialText, finalText)
  let prevOffset = 0
  let deltas: YDelta[] = []

  for (const edit of edits) {
    if (edit.removed && edit.value) {
      deltas = [
        ...deltas,
        ...[
          ...(prevOffset > 0 ? [{ retain: prevOffset }] : []),
          { delete: edit.value.length }
        ]
      ]
      prevOffset = 0
    } else if (edit.added && edit.value) {
      deltas = [...deltas, ...[{ retain: prevOffset }, { insert: edit.value }]]
      prevOffset = edit.value.length
    } else {
      prevOffset = edit.value.length
    }
  }
  return deltas
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "declaration": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ES2020",
    "moduleResolution": "node",
    "outDir": "./dist",
    "strict": true,
    "target": "ES2020"
  },
  "include": ["./src"]
}
Download .txt
gitextract_w42u4ncg/

├── .eslintrc
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── examples/
│   └── single-file-sync/
│       ├── .eslintrc.js
│       ├── .gitignore
│       ├── README.md
│       ├── components/
│       │   ├── Editor/
│       │   │   ├── Editor.module.css
│       │   │   ├── GitHub.tsx
│       │   │   ├── index.tsx
│       │   │   ├── options.ts
│       │   │   └── theme.ts
│       │   ├── Footer/
│       │   │   ├── Footer.module.css
│       │   │   └── index.tsx
│       │   ├── Head/
│       │   │   └── index.tsx
│       │   ├── Header/
│       │   │   ├── Header.module.css
│       │   │   └── index.tsx
│       │   └── Navbar/
│       │       ├── Navbar.module.css
│       │       └── index.tsx
│       ├── lib/
│       │   ├── constants.ts
│       │   ├── use-interval.ts
│       │   ├── y-config.ts
│       │   └── y-monaco.ts
│       ├── next-env.d.ts
│       ├── next.config.js
│       ├── package.json
│       ├── pages/
│       │   ├── _app.js
│       │   └── index.tsx
│       ├── styles/
│       │   ├── Shared.module.css
│       │   ├── globals.css
│       │   └── yjs.css
│       ├── tsconfig.json
│       └── types.d.ts
├── package.json
├── src/
│   ├── cache.ts
│   ├── constants.ts
│   ├── helpers.ts
│   ├── index.ts
│   └── yjs.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (29 symbols across 14 files)

FILE: examples/single-file-sync/components/Editor/index.tsx
  function Editor (line 6) | function Editor ({

FILE: examples/single-file-sync/components/Footer/index.tsx
  function Footer (line 5) | function Footer ({

FILE: examples/single-file-sync/components/Head/index.tsx
  function Head (line 4) | function Head () {

FILE: examples/single-file-sync/components/Header/index.tsx
  function Header (line 4) | function Header ({

FILE: examples/single-file-sync/components/Navbar/index.tsx
  function Navbar (line 6) | function Navbar () {

FILE: examples/single-file-sync/lib/constants.ts
  constant YDOC_UPDATE_ORIGIN_CURRENT_EDITOR (line 1) | const YDOC_UPDATE_ORIGIN_CURRENT_EDITOR =
  constant ROOM_ID (line 4) | const ROOM_ID = 'yfs-sample-room'
  constant TEST_FILE_NAME (line 5) | const TEST_FILE_NAME = 'yfs-test.md'
  constant WEBRTC_SIGNALING_SERVERS (line 7) | const WEBRTC_SIGNALING_SERVERS = [

FILE: examples/single-file-sync/lib/y-config.ts
  constant YDOC_UPDATE_ORIGIN_CURRENT_EDITOR (line 9) | const YDOC_UPDATE_ORIGIN_CURRENT_EDITOR =
  constant YDOC_UPDATE_ORIGIN_PROGRAMMATIC (line 11) | const YDOC_UPDATE_ORIGIN_PROGRAMMATIC = 'YDOC_UPDATE_ORIGIN_PROGRAMMATIC'
  class YConfig (line 13) | class YConfig {
    method constructor (line 28) | constructor (
    method initYDoc (line 45) | initYDoc (): void {
    method destroy (line 78) | destroy (): void {

FILE: examples/single-file-sync/lib/y-monaco.ts
  type Direction (line 7) | type Direction = any
  type Selection (line 8) | type Selection = {
  class RelativeSelection (line 14) | class RelativeSelection {
    method constructor (line 19) | constructor (
  class MonacoBinding (line 84) | class MonacoBinding {
    method constructor (line 99) | constructor (
    method destroy (line 290) | destroy (): void {

FILE: examples/single-file-sync/pages/_app.js
  function MyApp (line 4) | function MyApp({ Component, pageProps }) {

FILE: examples/single-file-sync/pages/index.tsx
  function Home (line 13) | function Home () {

FILE: src/cache.ts
  type LastWriteCacheData (line 4) | type LastWriteCacheData = {

FILE: src/constants.ts
  constant STORE_KEY_DIRECTORY_HANDLE (line 1) | const STORE_KEY_DIRECTORY_HANDLE = 'STORE_KEY_DIRECTORY_HANDLE'
  constant STORE_KEY_CACHED_FS_FILE (line 2) | const STORE_KEY_CACHED_FS_FILE = 'STORE_KEY_CACHED_FS_FILE'

FILE: src/helpers.ts
  type HandleWithPath (line 1) | type HandleWithPath = {

FILE: src/yjs.ts
  type YDelta (line 3) | type YDelta = { retain: number } | { delete: number } | { insert: string }
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (57K chars).
[
  {
    "path": ".eslintrc",
    "chars": 102,
    "preview": "// next is loading eslintrc from root directory, adding this to avoid eslint rules been overridden\n{}\n"
  },
  {
    "path": ".gitignore",
    "chars": 1617,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
  },
  {
    "path": ".prettierignore",
    "chars": 220,
    "preview": "node_modules\n.next\nbuild\ndist\n*.tsbuildinfo\n*.gitignore\n*.svg\n*.lock\n*.npmignore\n*.sql\n*.png\n*.jpg\n*.jpeg\n*.gif\n*.ico\n*."
  },
  {
    "path": ".prettierrc.json",
    "chars": 80,
    "preview": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"tabWidth\": 2,\n  \"useTabs\": false\n}\n"
  },
  {
    "path": "LICENSE",
    "chars": 1062,
    "preview": "MIT License\n\nCopyright (c) 2022 Motif\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
  },
  {
    "path": "README.md",
    "chars": 1823,
    "preview": "# YFS\n\nSynchronize text files between the browser and the file system using the\n[File System Access API](https://develop"
  },
  {
    "path": "examples/single-file-sync/.eslintrc.js",
    "chars": 1003,
    "preview": "/* eslint-env node */\nmodule.exports = {\n  env: {\n    browser: true,\n    es2021: true,\n    node: true,\n  },\n  extends: ["
  },
  {
    "path": "examples/single-file-sync/.gitignore",
    "chars": 343,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "examples/single-file-sync/README.md",
    "chars": 555,
    "preview": "This is a sample app showcasing a browser-based editor\n([Monaco](https://microsoft.github.io/monaco-editor/)) syncing it"
  },
  {
    "path": "examples/single-file-sync/components/Editor/Editor.module.css",
    "chars": 43,
    "preview": ".editor {\n  width: 100%;\n  height: 100%;\n}\n"
  },
  {
    "path": "examples/single-file-sync/components/Editor/GitHub.tsx",
    "chars": 1081,
    "preview": "import * as React from 'react'\n\nconst GitHub = (props: any) => {\n  return (\n    <svg\n      width={24}\n      height={24}\n"
  },
  {
    "path": "examples/single-file-sync/components/Editor/index.tsx",
    "chars": 698,
    "preview": "import React, { useCallback } from 'react'\nimport MonacoEditor from '@monaco-editor/react'\nimport theme from './theme'\ni"
  },
  {
    "path": "examples/single-file-sync/components/Editor/options.ts",
    "chars": 969,
    "preview": "const options = {\n  codeLens: false,\n  fontSize: 14,\n  lineHeight: 24,\n  lineNumbers: 'off',\n  padding: {\n    top: 30,\n "
  },
  {
    "path": "examples/single-file-sync/components/Editor/theme.ts",
    "chars": 3335,
    "preview": "const theme = {\n  base: 'vs',\n  inherit: true,\n  rules: [\n    { token: '', foreground: '0F172A' },\n    { token: 'invalid"
  },
  {
    "path": "examples/single-file-sync/components/Footer/Footer.module.css",
    "chars": 396,
    "preview": ".syncFooter {\n  display: flex;\n  flex-direction: row;\n  gap: 10px;\n  align-items: center;\n  align-self: flex-start;\n  pa"
  },
  {
    "path": "examples/single-file-sync/components/Footer/index.tsx",
    "chars": 1339,
    "preview": "import React from 'react'\nimport styles from './Footer.module.css'\nimport sharedStyles from '../../styles/Shared.module."
  },
  {
    "path": "examples/single-file-sync/components/Head/index.tsx",
    "chars": 353,
    "preview": "import React from 'react'\nimport NextHead from 'next/head'\n\nexport default function Head () {\n  return (\n    <NextHead>\n"
  },
  {
    "path": "examples/single-file-sync/components/Header/Header.module.css",
    "chars": 559,
    "preview": ".cta {\n  display: inline-block;\n  color: white;\n  background-color: var(--button-dark);\n  outline: 0 !important;\n  outli"
  },
  {
    "path": "examples/single-file-sync/components/Header/index.tsx",
    "chars": 766,
    "preview": "import React from 'react'\nimport styles from './Header.module.css'\n\nexport default function Header ({\n  isFSAPISupported"
  },
  {
    "path": "examples/single-file-sync/components/Navbar/Navbar.module.css",
    "chars": 631,
    "preview": ".nav {\n  display: flex;\n  flex-direction: row;\n  gap: 10px;\n  align-items: center;\n  width: 100%;\n  padding: 1.5rem;\n}\n\n"
  },
  {
    "path": "examples/single-file-sync/components/Navbar/index.tsx",
    "chars": 1264,
    "preview": "import React from 'react'\nimport GitHub from 'components/Editor/GitHub'\nimport styles from './Navbar.module.css'\nimport "
  },
  {
    "path": "examples/single-file-sync/lib/constants.ts",
    "chars": 254,
    "preview": "export const YDOC_UPDATE_ORIGIN_CURRENT_EDITOR =\n  'YDOC_UPDATE_ORIGIN_CURRENT_EDITOR'\n\nexport const ROOM_ID = 'yfs-samp"
  },
  {
    "path": "examples/single-file-sync/lib/use-interval.ts",
    "chars": 442,
    "preview": "import { useEffect, useRef } from 'react'\n\nexport const useInterval = (callback: () => void, delay: number | null) => {\n"
  },
  {
    "path": "examples/single-file-sync/lib/y-config.ts",
    "chars": 2335,
    "preview": "import * as Y from 'yjs'\nimport * as awarenessProtocol from 'y-protocols/awareness.js'\nimport { WebrtcProvider } from 'y"
  },
  {
    "path": "examples/single-file-sync/lib/y-monaco.ts",
    "chars": 8891,
    "preview": "import * as Y from 'yjs'\nimport * as error from 'lib0/error.js'\nimport { createMutex, mutex } from 'lib0/mutex.js'\nimpor"
  },
  {
    "path": "examples/single-file-sync/next-env.d.ts",
    "chars": 201,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
  },
  {
    "path": "examples/single-file-sync/next.config.js",
    "chars": 118,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "examples/single-file-sync/package.json",
    "chars": 744,
    "preview": "{\n  \"name\": \"yfs-single-file-sync-example\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev"
  },
  {
    "path": "examples/single-file-sync/pages/_app.js",
    "chars": 164,
    "preview": "import '../styles/globals.css'\nimport '../styles/yjs.css'\n\nfunction MyApp({ Component, pageProps }) {\n  return <Componen"
  },
  {
    "path": "examples/single-file-sync/pages/index.tsx",
    "chars": 1789,
    "preview": "import React, { useCallback, useRef } from 'react'\nimport useFileSync from '@yfs/react'\nimport Editor from 'components/E"
  },
  {
    "path": "examples/single-file-sync/styles/Shared.module.css",
    "chars": 768,
    "preview": ".container {\n  min-height: 100vh;\n  padding: 2rem 0;\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  align-items"
  },
  {
    "path": "examples/single-file-sync/styles/globals.css",
    "chars": 1062,
    "preview": "@import url('https://rsms.me/inter/inter.css');\n\n:root {\n  --background: #eeede9;\n  --border-light: #e3e3e3;\n  --text-da"
  },
  {
    "path": "examples/single-file-sync/styles/yjs.css",
    "chars": 1485,
    "preview": "/* Selection */\n\n.ySelection-0 {\n  background-color: var(--orange-light);\n}\n.ySelection-1 {\n  background-color: var(--gr"
  },
  {
    "path": "examples/single-file-sync/tsconfig.json",
    "chars": 674,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"baseUrl\": \"./\",\n    \"target\": \"esnext\",\n    \"lib"
  },
  {
    "path": "examples/single-file-sync/types.d.ts",
    "chars": 26,
    "preview": "declare module 'y-monaco'\n"
  },
  {
    "path": "package.json",
    "chars": 839,
    "preview": "{\n  \"name\": \"@yfs/react\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Synchronize text files between the browser and the fil"
  },
  {
    "path": "src/cache.ts",
    "chars": 782,
    "preview": "import { set as idbSet, get as idbGet } from 'idb-keyval'\nimport { STORE_KEY_CACHED_FS_FILE } from './constants'\n\ntype L"
  },
  {
    "path": "src/constants.ts",
    "chars": 138,
    "preview": "export const STORE_KEY_DIRECTORY_HANDLE = 'STORE_KEY_DIRECTORY_HANDLE'\nexport const STORE_KEY_CACHED_FS_FILE = 'STORE_KE"
  },
  {
    "path": "src/helpers.ts",
    "chars": 5757,
    "preview": "export type HandleWithPath = {\n  handle: FileSystemHandle\n  path: string[]\n  type: 'file' | 'directory'\n}\n\nconst readWri"
  },
  {
    "path": "src/index.ts",
    "chars": 5801,
    "preview": "import { useCallback, useEffect, useMemo, useState } from 'react'\nimport { set as idbSet, get as idbGet, del as idbDel }"
  },
  {
    "path": "src/yjs.ts",
    "chars": 1031,
    "preview": "import * as Diff from 'diff'\n\ntype YDelta = { retain: number } | { delete: number } | { insert: string }\n\n// Compute the"
  },
  {
    "path": "tsconfig.json",
    "chars": 278,
    "preview": "{\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": t"
  }
]

About this extraction

This page contains the full source code of the motifland/yfs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (50.6 KB), approximately 14.9k tokens, and a symbol index with 29 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!