vzcode 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +47 -5
  2. package/dist/assets/index-41607816.js +89 -0
  3. package/dist/assets/index-8bac68c9.css +1 -0
  4. package/dist/index.html +16 -2
  5. package/package.json +29 -19
  6. package/src/{App.jsx → client/App.tsx} +83 -126
  7. package/src/client/Sidebar/DirectoryArrowSVG.jsx +5 -0
  8. package/src/client/Sidebar/DirectoryListing.tsx +76 -0
  9. package/src/client/Sidebar/FileListing.tsx +42 -0
  10. package/src/client/Sidebar/Item.jsx +43 -0
  11. package/src/client/Sidebar/Listing.tsx +44 -0
  12. package/src/client/Sidebar/index.tsx +80 -0
  13. package/src/client/Sidebar/styles.scss +84 -0
  14. package/src/client/Sidebar/useOpenDirectories.ts +37 -0
  15. package/src/{diff.js → client/diff.js} +1 -1
  16. package/src/client/featureFlags.js +3 -0
  17. package/src/client/getFileTree.ts +52 -0
  18. package/src/{getOrCreateEditor.js → client/getOrCreateEditor.js} +6 -6
  19. package/src/{json1PresenceDisplay.js → client/json1PresenceDisplay.js} +0 -1
  20. package/src/client/settings.jsx +97 -0
  21. package/src/client/sortFileTree.ts +19 -0
  22. package/src/client/style.scss +93 -0
  23. package/src/server/computeInitialDocument.js +69 -0
  24. package/src/server/featureFlags.js +4 -0
  25. package/{cli.js → src/server/index.js} +24 -30
  26. package/src/types.ts +57 -0
  27. package/dist/assets/index-48dbd5c3.js +0 -89
  28. package/dist/assets/index-8d2d4b3b.css +0 -1
  29. package/src/settings.jsx +0 -141
  30. package/src/style.css +0 -215
  31. /package/src/{CodeEditor.jsx → client/CodeEditor.jsx} +0 -0
  32. /package/src/{json1PresenceBroadcast.js → client/json1PresenceBroadcast.js} +0 -0
  33. /package/src/{main.jsx → client/main.jsx} +0 -0
@@ -0,0 +1,80 @@
1
+ import { useMemo } from 'react';
2
+ import { getFileTree } from '../getFileTree';
3
+ import { sortFileTree } from '../sortFileTree';
4
+ import { disableSettings } from '../featureFlags';
5
+ import { Listing } from './Listing';
6
+ import { useOpenDirectories } from './useOpenDirectories';
7
+
8
+ import './styles.scss';
9
+ import { FileId, FileTree, FileTreeFile, Files } from '../../types';
10
+
11
+ export const Sidebar = ({
12
+ createFile,
13
+ files,
14
+ handleRenameFileClick,
15
+ handleDeleteFileClick,
16
+ handleFileClick,
17
+ setIsSettingsOpen,
18
+ isSettingsOpen,
19
+ }: {
20
+ files: Files;
21
+ createFile?: () => void;
22
+ handleRenameFileClick?: (fileId: FileId) => void;
23
+ handleDeleteFileClick?: (fileId: FileId, event: React.MouseEvent) => void;
24
+ handleFileClick?: (fileId: FileId) => void;
25
+ setIsSettingsOpen?: (isSettingsOpen: boolean) => void;
26
+ isSettingsOpen?: boolean;
27
+ }) => {
28
+ const fileTree = useMemo(
29
+ () => (files ? sortFileTree(getFileTree(files)) : null),
30
+ [files]
31
+ );
32
+
33
+ const { openDirectories, toggleDirectory } = useOpenDirectories();
34
+
35
+ return (
36
+ <div className="vz-sidebar">
37
+ <div className="files">
38
+ <div className="full-box">
39
+ <div>
40
+ <a className="link-name" href="#">
41
+ Files
42
+ </a>
43
+ </div>
44
+ <div>
45
+ <i
46
+ className="bx bxs-file-plus new-btn"
47
+ style={{ color: '#dbdde1' }}
48
+ onClick={createFile}
49
+ ></i>
50
+ </div>
51
+ </div>
52
+ {fileTree
53
+ ? fileTree.children.map((entity) => {
54
+ const { fileId } = entity as FileTreeFile;
55
+ const { path } = entity as FileTree;
56
+ return (
57
+ <Listing
58
+ entity={entity}
59
+ key={fileId || path}
60
+ handleRenameFileClick={handleRenameFileClick}
61
+ handleDeleteFileClick={handleDeleteFileClick}
62
+ handleFileClick={handleFileClick}
63
+ openDirectories={openDirectories}
64
+ toggleDirectory={toggleDirectory}
65
+ />
66
+ );
67
+ })
68
+ : null}
69
+ </div>
70
+ {disableSettings ? null : (
71
+ <div
72
+ className="settings"
73
+ onClick={() => setIsSettingsOpen(!isSettingsOpen)}
74
+ >
75
+ Settings
76
+ </div>
77
+ )}
78
+ </div>
79
+ );
80
+ };
@@ -0,0 +1,84 @@
1
+ .vz-sidebar {
2
+ height: 100%;
3
+ width: 200px;
4
+ background: rgb(54, 65, 89);
5
+ display: flex;
6
+ flex-direction: column;
7
+ justify-content: space-between;
8
+
9
+ .full-box {
10
+ padding: 7px;
11
+ display: flex;
12
+ flex-direction: row;
13
+ align-items: center;
14
+ justify-content: space-between;
15
+ }
16
+
17
+ .files {
18
+ overflow: auto;
19
+ }
20
+
21
+ .file-or-directory {
22
+ display: flex;
23
+ flex-direction: row;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ cursor: pointer;
27
+ margin: 0 10px;
28
+ color: rgb(219, 221, 225);
29
+ border-radius: 5px;
30
+ }
31
+
32
+ .link-name {
33
+ margin-top: 30px;
34
+ margin-left: 10px;
35
+ color: rgb(154, 160, 172);
36
+ font-size: 12px;
37
+ text-decoration: none;
38
+ }
39
+
40
+ .new-btn {
41
+ justify-content: right;
42
+ margin-right: 10px;
43
+ cursor: pointer;
44
+ }
45
+
46
+ .utils {
47
+ display: flex;
48
+ }
49
+ .utilities {
50
+ margin-right: 3px;
51
+ }
52
+
53
+ .file-or-directory:hover,
54
+ .settings:hover {
55
+ background-color: rgb(74, 84, 105);
56
+ }
57
+
58
+ .name {
59
+ display: flex;
60
+ text-overflow: ellipsis;
61
+ white-space: nowrap;
62
+ overflow: hidden;
63
+ }
64
+
65
+ .indentation {
66
+ margin-left: 20px;
67
+ }
68
+
69
+ .arrow-wrapper {
70
+ margin-right: 4px;
71
+ }
72
+
73
+ .settings {
74
+ height: 30px;
75
+ color: rgb(219, 221, 225);
76
+ cursor: pointer;
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ padding: 14px 0;
81
+ margin: 10px;
82
+ border-radius: 5px;
83
+ }
84
+ }
@@ -0,0 +1,37 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ // TODO bring this feature back
4
+ // When a page is opened with an active file,
5
+ // make sure all the directories leading to that file
6
+ // are opened automatically.
7
+ //export const initialOpenDirectories = (activeFile) => {
8
+ // const openDirectories = {};
9
+ // if (activeFile) {
10
+ // const path = activeFile.split('/');
11
+ // for (let i = 1; i < path.length; i++) {
12
+ // openDirectories[path.slice(0, i).join('/')] = true;
13
+ // }
14
+ // }
15
+ // return openDirectories;
16
+ //};
17
+
18
+ // Inspired by
19
+ // https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/useOpenDirectories.js
20
+ export const useOpenDirectories = () => {
21
+ const [openDirectories, setOpenDirectories] = useState<{
22
+ [path: string]: boolean;
23
+ }>({});
24
+
25
+ // TODO consider useReducer to avoid event listener churn
26
+ const toggleDirectory = useCallback(
27
+ (path) => {
28
+ setOpenDirectories(
29
+ Object.assign({}, openDirectories, {
30
+ [path]: !openDirectories[path],
31
+ })
32
+ );
33
+ },
34
+ [openDirectories]
35
+ );
36
+ return { openDirectories, toggleDirectory };
37
+ };
@@ -1,6 +1,6 @@
1
1
  import jsondiff from 'json0-ot-diff';
2
2
  import diffMatchPatch from 'diff-match-patch';
3
- import { json1Presence, textUnicode } from './ot';
3
+ import { json1Presence, textUnicode } from '../ot';
4
4
 
5
5
  export const diff = (a, b) =>
6
6
  jsondiff(a, b, diffMatchPatch, json1Presence, textUnicode);
@@ -0,0 +1,3 @@
1
+ // A feature flag to disable settings for release, until it's completed.
2
+ // See https://vitejs.dev/guide/env-and-mode.html
3
+ export const disableSettings = import.meta.env.VITE_DISABLE_SETTINGS === 'true';
@@ -0,0 +1,52 @@
1
+ // Inspired by
2
+
3
+ import { Files, FileTree } from '../types';
4
+
5
+ // https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/FileTree/getFileTree.js
6
+ export const getFileTree = (files: Files): FileTree => {
7
+ const tree: FileTree = { name: 'files' };
8
+ for (const fileId of Object.keys(files)) {
9
+ const file = files[fileId];
10
+ const path = file.name.split('/');
11
+ const n = path.length;
12
+ let node = tree;
13
+
14
+ // Walk the path.
15
+ for (let i = 0; i < n - 1; i++) {
16
+ // Search for an existing child.
17
+ let child: FileTree | undefined;
18
+ const name = path[i];
19
+ if (node.children) {
20
+ for (let j = 0; j < node.children.length; j++) {
21
+ const nodeChild = node.children[j];
22
+ if (nodeChild.name === name) {
23
+ child = nodeChild;
24
+ break;
25
+ }
26
+ }
27
+ }
28
+
29
+ // Create a child if none with matching name exists.
30
+ if (!child) {
31
+ child = {
32
+ name: path[i],
33
+ path: path.slice(0, i + 1).join('/'),
34
+ };
35
+ (node.children || (node.children = [])).push(child);
36
+ }
37
+
38
+ node = child;
39
+ }
40
+
41
+ // file.text being `null` signifies
42
+ // that this is a directory.
43
+ if (file.text !== null) {
44
+ (node.children || (node.children = [])).push({
45
+ name: path[n - 1],
46
+ file,
47
+ fileId,
48
+ });
49
+ }
50
+ }
51
+ return tree;
52
+ };
@@ -1,12 +1,11 @@
1
1
  import { EditorView, basicSetup } from 'codemirror';
2
- import { EditorState } from '@codemirror/state';
2
+ import { Compartment, EditorState } from '@codemirror/state';
3
3
  import { javascript } from '@codemirror/lang-javascript';
4
4
  import { markdown } from '@codemirror/lang-markdown';
5
5
  import { html } from '@codemirror/lang-html';
6
6
  import { css } from '@codemirror/lang-css';
7
- import { oneDark } from '@codemirror/theme-one-dark';
8
7
  import { json1Sync } from 'codemirror-ot';
9
- import { json1Presence, textUnicode } from './ot';
8
+ import { json1Presence, textUnicode } from '../ot';
10
9
  import { json1PresenceBroadcast } from './json1PresenceBroadcast';
11
10
  import { json1PresenceDisplay } from './json1PresenceDisplay';
12
11
 
@@ -23,7 +22,7 @@ export const getOrCreateEditor = ({
23
22
  shareDBDoc,
24
23
  localPresence,
25
24
  docPresence,
26
- theme,
25
+ theme
27
26
  }) => {
28
27
  const data = shareDBDoc.data;
29
28
 
@@ -32,6 +31,8 @@ export const getOrCreateEditor = ({
32
31
  // The path for this file in the ShareDB document.
33
32
  const path = [fileId, 'text'];
34
33
 
34
+ let themeSet = new Compartment();
35
+
35
36
  const extensions = [
36
37
  // This plugin implements multiplayer editing,
37
38
  // real-time synchronozation of changes across clients.
@@ -50,7 +51,7 @@ export const getOrCreateEditor = ({
50
51
  json1PresenceDisplay({ path, docPresence }),
51
52
 
52
53
  basicSetup,
53
- oneDark,
54
+ themeSet.of(theme),
54
55
  ];
55
56
 
56
57
  if (
@@ -70,7 +71,6 @@ export const getOrCreateEditor = ({
70
71
 
71
72
  let editor = editorCache.get(fileId);
72
73
  if (!editor) {
73
- // Cache miss --> mint a new editor.
74
74
  editor = new EditorView({
75
75
  state: EditorState.create({
76
76
  doc: data[fileId].text,
@@ -5,7 +5,6 @@ import {
5
5
  Decoration,
6
6
  } from '@codemirror/view';
7
7
  import { Annotation, RangeSet } from '@codemirror/state';
8
- import { randomId } from './randomId';
9
8
 
10
9
  // Deals with receiving the broadcasted presence cursor locations
11
10
  // from other clients and displaying them.
@@ -0,0 +1,97 @@
1
+ import { useCallback } from 'react';
2
+ import Modal from 'react-bootstrap/Modal';
3
+ import Form from 'react-bootstrap/Form';
4
+ import Button from 'react-bootstrap/Button';
5
+ import Select from 'react-select';
6
+ import { oneDark } from '@codemirror/theme-one-dark';
7
+ import { okaidia } from '@uiw/codemirror-theme-okaidia';
8
+ import { abcdef } from '@uiw/codemirror-theme-abcdef';
9
+ import { dracula } from '@uiw/codemirror-theme-dracula';
10
+ import { eclipse } from '@uiw/codemirror-theme-eclipse';
11
+ import { githubDark } from '@uiw/codemirror-theme-github';
12
+ import { material } from '@uiw/codemirror-theme-material';
13
+ import { nord } from '@uiw/codemirror-theme-nord';
14
+ import { xcodeLight } from '@uiw/codemirror-theme-xcode';
15
+
16
+ // TODO document where this list came from
17
+ // TODO research which themes are available for CodeMirror 6
18
+ const themes = [
19
+ { value: abcdef, label: 'abcdef' },
20
+ { value: dracula, label: 'darcula' },
21
+ { value: eclipse, label: 'eclipse' },
22
+ { value: material, label: 'material' },
23
+ { value: nord, label: 'nord' },
24
+ { value: oneDark, label: 'One Dark' },
25
+ { value: okaidia, label: 'okaidia' },
26
+ { value: githubDark, label: 'github' },
27
+ { value: xcodeLight, label: 'xcode' },
28
+ ];
29
+
30
+ const saveTimes = [
31
+ { value: 1, label: '1 second' },
32
+ { value: 5, label: '5 seconds' },
33
+ { value: 10, label: '10 seconds' },
34
+ { value: 30, label: '30 seconds' },
35
+ ];
36
+
37
+ export const Settings = ({ show, onClose, setTheme, editor }) => {
38
+ const handleChange = useCallback((selectedOption) => {
39
+ setTheme(selectedOption.value);
40
+ editor.dispatch({
41
+ effects: themeSet.reconfigure(selectedOption.value),
42
+ });
43
+ }, []);
44
+
45
+ const handleSaveTimeChange = useCallback((selectedOption) => {
46
+ const time = [{ value: selectedOption.value }];
47
+ fetch('/saveTime', {
48
+ method: 'POST',
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ },
52
+ body: JSON.stringify(time),
53
+ });
54
+ }, []);
55
+
56
+ return show ? (
57
+ <Modal show={show} onHide={onClose} animation={false}>
58
+ <Modal.Header closeButton>
59
+ <Modal.Title>Settings</Modal.Title>
60
+ </Modal.Header>
61
+ <Modal.Body>
62
+ <Form.Group className="mb-3" controlId="formFork">
63
+ <Form.Label>Username</Form.Label>
64
+ <Form.Control type="text" placeholder="Enter username" />
65
+ <Form.Text className="text-muted">
66
+ Enter a username to be displayed on your cursor
67
+ </Form.Text>
68
+ </Form.Group>
69
+ <Form.Group className="mb-3" controlId="formFork">
70
+ <Form.Label>Theme</Form.Label>
71
+ <Select
72
+ classvalue="themes"
73
+ options={themes}
74
+ onChange={handleChange}
75
+ />
76
+ <Form.Text className="text-muted">
77
+ Select a color theme for the editor
78
+ </Form.Text>
79
+ </Form.Group>
80
+ <Form.Group className="mb-3" controlId="formFork">
81
+ <Form.Label>Auto-Save Time</Form.Label>
82
+ <Select
83
+ classvalue="themes"
84
+ options={saveTimes}
85
+ onChange={handleSaveTimeChange}
86
+ />
87
+ <Form.Text className="text-muted">Select an auto save time</Form.Text>
88
+ </Form.Group>
89
+ </Modal.Body>
90
+ <Modal.Footer>
91
+ <Button variant="primary" onClick={onClose}>
92
+ Done
93
+ </Button>
94
+ </Modal.Footer>
95
+ </Modal>
96
+ ) : null;
97
+ };
@@ -0,0 +1,19 @@
1
+ // Recursively sorts the file tree in-place.
2
+ // Sorts first to group directories before files.
3
+ // Sorts second alphabetically.
4
+
5
+ import { FileTree, FileTreeFile } from '../types';
6
+
7
+ // From https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/FileTree/sortFileTree.js
8
+ export const sortFileTree = (fileTree: FileTree) => {
9
+ if (fileTree.children) {
10
+ fileTree.children.sort((a, b) => {
11
+ const aIsFile = (a as FileTreeFile).file ? 1 : 0;
12
+ const bIsFile = (b as FileTreeFile).file ? 1 : 0;
13
+ // Directories before files.
14
+ return aIsFile - bIsFile || a.name.localeCompare(b.name);
15
+ });
16
+ fileTree.children.forEach(sortFileTree);
17
+ }
18
+ return fileTree;
19
+ };
@@ -0,0 +1,93 @@
1
+ :root {
2
+ --vzcode-font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
3
+ Liberation Mono, monospace;
4
+ }
5
+
6
+ /* width */
7
+ ::-webkit-scrollbar {
8
+ width: 10px;
9
+ height: 10px;
10
+ }
11
+
12
+ /* Track */
13
+ ::-webkit-scrollbar-track {
14
+ background: rgb(41, 44, 52);
15
+ }
16
+
17
+ /* Handle */
18
+ ::-webkit-scrollbar-thumb {
19
+ background: rgb(37, 37, 38);
20
+ }
21
+
22
+ /* Handle on hover */
23
+ ::-webkit-scrollbar-thumb:hover {
24
+ background: #555;
25
+ }
26
+
27
+ html,
28
+ body,
29
+ #root,
30
+ .app {
31
+ height: 100%;
32
+ }
33
+ .app {
34
+ /* TODO use a variable for this */
35
+ background-color: rgb(41, 44, 52);
36
+
37
+ font-family: 'Poppins', sans-serif;
38
+ display: flex;
39
+
40
+ .left {
41
+ display: flex;
42
+ }
43
+ .right {
44
+ display: flex;
45
+ flex: 1;
46
+ flex-direction: column;
47
+ min-width: 0;
48
+
49
+ .tab-list {
50
+ display: flex;
51
+ background-color: rgb(37, 37, 38);
52
+
53
+ .tab {
54
+ background: rgb(45, 45, 45);
55
+ color: #fff;
56
+ font-family: var(--vzcode-font-family);
57
+ align-items: center;
58
+ display: flex;
59
+ padding-top: 2px;
60
+ padding-left: 10px;
61
+ padding-right: 10px;
62
+ border-top-left-radius: 10px;
63
+ border-top-right-radius: 10px;
64
+ margin-top: 5px;
65
+ margin-left: 2px;
66
+ margin-right: 2px;
67
+ cursor: pointer;
68
+
69
+ .tab-close {
70
+ margin-left: 5px;
71
+ }
72
+ .tab-close:hover {
73
+ border-radius: 30%;
74
+ background-color: rgb(37, 37, 38);
75
+ }
76
+ }
77
+ .tab.active {
78
+ background-color: rgb(41, 44, 52);
79
+ }
80
+ }
81
+
82
+ .editor {
83
+ flex: 1;
84
+ display: flex;
85
+ font-family: var(--vzcode-font-family);
86
+ overflow-x: auto;
87
+ }
88
+ }
89
+ }
90
+
91
+ .themes {
92
+ color: rgb(37, 37, 38);
93
+ }
@@ -0,0 +1,69 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { randomId } from '../randomId.js';
4
+ import { enableDirectories, debugDirectories } from './featureFlags.js';
5
+
6
+ const isDirectory = (file) => file.endsWith('/');
7
+
8
+ // Lists files from the file system,
9
+ // converts them into the VZCode internal
10
+ // ShareDB-compatible data structure.
11
+ export const computeInitialDocument = ({ fullPath }) => {
12
+ // Isolate files, not directories.
13
+ // Inspired by https://stackoverflow.com/questions/41472161/fs-readdir-ignore-directories
14
+
15
+ const unsearchedDirectories = [''];
16
+
17
+ // Initialize the document using our data structure for representing files.
18
+ // * Keys are file ids, which are random numbers.
19
+ // * Values are objects with properties:
20
+ // * text - the text content of the file
21
+ // * name - the file name
22
+ const initialDocument = {};
23
+
24
+ // Stack for recursively traversing directories.
25
+ let files = [];
26
+
27
+ while (unsearchedDirectories.length !== 0) {
28
+ const currentDirectory = unsearchedDirectories.pop();
29
+ const currentDirectoryPath = path.join(fullPath, currentDirectory);
30
+ const newFiles = fs
31
+ .readdirSync(currentDirectoryPath, { withFileTypes: true })
32
+ .filter((dirent) => (enableDirectories ? true : dirent.isFile()))
33
+
34
+ // Add a trailing slash for directories
35
+ .map((dirent) => {
36
+ const relativePath =
37
+ currentDirectory === ''
38
+ ? dirent.name
39
+ : currentDirectory + '/' + dirent.name;
40
+
41
+ if (dirent.isFile()) {
42
+ return relativePath;
43
+ }
44
+ unsearchedDirectories.push(relativePath);
45
+ return relativePath + '/';
46
+ });
47
+ // console.log(currentDirectory);
48
+
49
+ files = [...files, ...newFiles];
50
+ }
51
+
52
+ files.forEach((file) => {
53
+ const id = randomId();
54
+ initialDocument[id] = {
55
+ text: isDirectory(file)
56
+ ? null
57
+ : fs.readFileSync(path.join(fullPath, file), 'utf-8'),
58
+ name: file,
59
+ };
60
+ });
61
+
62
+ if (debugDirectories) {
63
+ console.log('files:');
64
+ console.log(files);
65
+ console.log('initialDocument:');
66
+ console.log(JSON.stringify(initialDocument, null, 2));
67
+ }
68
+ return initialDocument;
69
+ };
@@ -0,0 +1,4 @@
1
+ // Feature flag for directories, disabled until it's fully working.
2
+ export const enableDirectories = true;
3
+
4
+ export const debugDirectories = false;
@@ -7,9 +7,13 @@ import WebSocketJSONStream from '@teamwork/websocket-json-stream';
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
9
  import { fileURLToPath } from 'url';
10
- import { json1Presence } from './src/ot.js';
11
- import { randomId } from './src/randomId.js';
12
10
  import open from 'open';
11
+ import ngrok from 'ngrok';
12
+ import dotenv from 'dotenv';
13
+ import { computeInitialDocument } from './computeInitialDocument.js';
14
+ import { json1Presence } from '../ot.js';
15
+
16
+ dotenv.config({ path: '../../.env' });
13
17
 
14
18
  // The time in milliseconds by which auto-saving is debounced.
15
19
  const autoSaveDebounceTimeMS = 800;
@@ -17,28 +21,9 @@ const autoSaveDebounceTimeMS = 800;
17
21
  // Server port.
18
22
  const port = 3030;
19
23
 
20
- // Use the current working directory to look for files.
21
- const fullPath = process.cwd();
22
-
23
- // Isolate files, not directories.
24
- // Inspired by https://stackoverflow.com/questions/41472161/fs-readdir-ignore-directories
25
- const files = fs
26
- .readdirSync(fullPath, { withFileTypes: true })
27
- .filter((dirent) => dirent.isFile())
28
- .map((dirent) => dirent.name);
29
-
30
- // Initialize the document using our data structure for representing files.
31
- // * Keys are file ids, which are random numbers.
32
- // * Values are objects with properties:
33
- // * text - the text content of the file
34
- // * name - the file name
35
- const initialDocument = {};
36
- files.forEach((file) => {
37
- const id = randomId();
38
- initialDocument[id] = {
39
- text: fs.readFileSync(file, 'utf-8'),
40
- name: file,
41
- };
24
+ const initialDocument = computeInitialDocument({
25
+ // Use the current working directory to look for files.
26
+ fullPath: process.cwd(),
42
27
  });
43
28
 
44
29
  // Register our custom OT type,
@@ -66,9 +51,9 @@ wss.on('connection', (ws) => {
66
51
  });
67
52
 
68
53
  // Serve static files
69
- const __filename = fileURLToPath(import.meta.url);
70
- const __dirname = path.dirname(__filename);
71
- const dir = path.join(__dirname, '/dist');
54
+ const filename = fileURLToPath(import.meta.url);
55
+ const dirname = path.dirname(filename);
56
+ const dir = path.join(dirname, '..', '..', 'dist');
72
57
  app.use(express.static(dir));
73
58
 
74
59
  // Create the initial "document",
@@ -127,7 +112,16 @@ shareDBDoc.subscribe(() => {
127
112
  });
128
113
  });
129
114
 
130
- server.listen(port, () => {
131
- console.log(`Editor is live at http://localhost:${port}`);
132
- open(`http://localhost:${port}`);
115
+ server.listen(port, async () => {
116
+ if (process.env.NGROK_TOKEN) {
117
+ (async function () {
118
+ await ngrok.authtoken(process.env.NGROK_TOKEN);
119
+ const url = await ngrok.connect(port);
120
+ console.log(`Editor is live at ${url}`);
121
+ open(url);
122
+ })();
123
+ } else {
124
+ console.log(`Editor is live at http://localhost:${port}`);
125
+ open(`http://localhost:${port}`);
126
+ }
133
127
  });