Skip to content

Commit a080ee3

Browse files
committed
Optimize bundle size + chunked builds + JIT loading
* Replaced AJV with other JSON schema library * Turndown and Vim mode (very large) now only loaded when used * Chunked builds with ESBuild
1 parent 81408ed commit a080ee3

File tree

13 files changed

+349
-397
lines changed

13 files changed

+349
-397
lines changed

build_client.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
1+
import { cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
22
import { fileURLToPath } from "node:url";
33
import { dirname } from "node:path";
44
import * as sass from "sass";
@@ -28,7 +28,7 @@ export async function copyAssets(dist: string) {
2828
const scssContent = await readFile("client/styles/main.scss", "utf-8");
2929
const result = sass.compileString(scssContent, {
3030
loadPaths: ["client/styles"],
31-
style: "expanded",
31+
style: "compressed",
3232
});
3333
await writeFile(`${dist}/main.css`, result.css, "utf-8");
3434

@@ -62,7 +62,10 @@ async function buildCopyBundleAssets() {
6262
sourcemap: "linked",
6363
minify: true,
6464
jsxFactory: "h",
65-
// metafile: true,
65+
metafile: true,
66+
splitting: true,
67+
format: "esm",
68+
chunkNames: ".client/[name]-[hash]",
6669
jsx: "automatic",
6770
jsxFragment: "Fragment",
6871
jsxImportSource: "preact",
@@ -73,16 +76,32 @@ async function buildCopyBundleAssets() {
7376
console.log("Bundle info", text);
7477
}
7578

76-
// Patch the service_worker {{CACHE_NAME}}
79+
await copyAssets("client_bundle/client/.client");
80+
81+
// Scan .client/ directory to build the full precache file list
82+
const clientDir = "client_bundle/client/.client";
83+
const allFiles = await readdir(clientDir);
84+
const precacheFiles = [
85+
"/", // The index page
86+
"/.client/manifest.json", // Dynamically generated by the server, but needed for PWA
87+
...allFiles
88+
.filter((f) =>
89+
!f.endsWith(".map") && f !== "auth.html" && f !== "index.html" &&
90+
f !== "LICENSE.md"
91+
)
92+
.map((f) => `/.client/${f}`),
93+
];
94+
const precacheFilesStr = precacheFiles.join(",");
95+
96+
// Patch the service_worker {{CACHE_NAME}} and {{PRECACHE_FILES}}
7797
let swCode = await readFile(
7898
"client_bundle/client/service_worker.js",
7999
"utf-8",
80100
);
81101
swCode = swCode.replaceAll("{{CACHE_NAME}}", `cache-${Date.now()}`);
102+
swCode = swCode.replaceAll("{{PRECACHE_FILES}}", precacheFilesStr);
82103
await writeFile("client_bundle/client/service_worker.js", swCode, "utf-8");
83104

84-
await copyAssets("client_bundle/client/.client");
85-
86105
console.log("Built!");
87106
}
88107

client/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export class Client {
9696
// CodeMirror editor
9797
editorView!: EditorView;
9898
commandKeyHandlerCompartment?: Compartment;
99+
vimCompartment?: Compartment;
99100
indentUnitCompartment?: Compartment;
100101
undoHistoryCompartment?: Compartment;
101102

client/codemirror/editor_paste.ts

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@ import { syntaxTree } from "@codemirror/language";
22
import { EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
33
import type { Client } from "../client.ts";
44

5-
// We use turndown to convert HTML to Markdown
6-
import TurndownService from "turndown";
7-
8-
// With tables and task notation as well
9-
// @ts-expect-error - No type definitions available for this package
10-
import { tables, taskListItems } from "@joplin/turndown-plugin-gfm";
115
import { lezerToParseTree } from "../markdown_parser/parse_tree.ts";
126
import {
137
addParentPointers,
@@ -21,17 +15,30 @@ import { localDateString } from "@silverbulletmd/silverbullet/lib/dates";
2115
import type { UploadFile } from "@silverbulletmd/silverbullet/type/client";
2216
import { isValidName, isValidPath } from "@silverbulletmd/silverbullet/lib/ref";
2317

24-
const turndownService = new TurndownService({
25-
hr: "---",
26-
codeBlockStyle: "fenced",
27-
headingStyle: "atx",
28-
emDelimiter: "*",
29-
bulletListMarker: "*", // Duh!
30-
strongDelimiter: "**",
31-
linkStyle: "inlined",
32-
});
33-
turndownService.use(taskListItems);
34-
turndownService.use(tables);
18+
// Dynamically load the turndown service (only used for clipboard pasete)
19+
let turndownService: any = null;
20+
async function getTurndownService() {
21+
if (!turndownService) {
22+
const [{ default: TurndownService }, { tables, taskListItems }] =
23+
await Promise.all([
24+
import("turndown"),
25+
// @ts-expect-error - No type definitions available for this package
26+
import("@joplin/turndown-plugin-gfm"),
27+
]);
28+
turndownService = new TurndownService({
29+
hr: "---",
30+
codeBlockStyle: "fenced",
31+
headingStyle: "atx",
32+
emDelimiter: "*",
33+
bulletListMarker: "*", // Duh!
34+
strongDelimiter: "**",
35+
linkStyle: "inlined",
36+
});
37+
turndownService.use(taskListItems);
38+
turndownService.use(tables);
39+
}
40+
return turndownService;
41+
}
3542

3643
function striptHtmlComments(s: string): string {
3744
return s.replace(/<!--[\s\S]*?-->/g, "");
@@ -137,7 +144,7 @@ export function documentExtension(editor: Client) {
137144

138145
// Only do rich text paste if shift is NOT down
139146
if (richText && !shiftDown) {
140-
// Are we in a fencede code block?
147+
// Are we in a fenced code block?
141148
const editorText = editor.editorView.state.sliceDoc();
142149
const tree = lezerToParseTree(
143150
editorText,
@@ -161,23 +168,26 @@ export function documentExtension(editor: Client) {
161168
}
162169
}
163170

164-
const markdown = striptHtmlComments(
165-
turndownService.turndown(richText),
166-
).trim();
171+
// Prevent default immediately, then do async turndown conversion
172+
event.preventDefault();
167173
const view = editor.editorView;
168174
const selection = view.state.selection.main;
169-
view.dispatch({
170-
changes: [
171-
{
172-
from: selection.from,
173-
to: selection.to,
174-
insert: markdown,
175+
safeRun(async () => {
176+
const td = await getTurndownService();
177+
const markdown = striptHtmlComments(td.turndown(richText)).trim();
178+
view.dispatch({
179+
changes: [
180+
{
181+
from: selection.from,
182+
to: selection.to,
183+
insert: markdown,
184+
},
185+
],
186+
selection: {
187+
anchor: selection.from + markdown.length,
175188
},
176-
],
177-
selection: {
178-
anchor: selection.from + markdown.length,
179-
},
180-
scrollIntoView: true,
189+
scrollIntoView: true,
190+
});
181191
});
182192
return true;
183193
}

client/codemirror/editor_state.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ import {
3434
ViewPlugin,
3535
type ViewUpdate,
3636
} from "@codemirror/view";
37-
import { vim } from "@replit/codemirror-vim";
3837
import { markdown } from "@codemirror/lang-markdown";
3938
import type { Client } from "../client.ts";
39+
import { loadVim } from "../vim_loader.ts";
4040
import { inlineContentPlugin } from "./inline_content.ts";
4141
import { cleanModePlugins } from "./clean.ts";
4242
import { lineWrapper } from "./line_wrapper.ts";
@@ -63,6 +63,7 @@ export function createEditorState(
6363
// Ugly: keep the commandKeyHandler compartment in the client, to be replaced
6464
// later once more commands are loaded
6565
client.commandKeyHandlerCompartment = new Compartment();
66+
client.vimCompartment = new Compartment();
6667
const commandKeyBindings = client.commandKeyHandlerCompartment.of(
6768
createCommandKeyBindings(client),
6869
);
@@ -76,6 +77,13 @@ export function createEditorState(
7677
client.undoHistoryCompartment = new Compartment();
7778
const undoHistory = client.undoHistoryCompartment.of([history()]);
7879

80+
const vimMode = client.ui.viewState.uiOptions.vimMode;
81+
82+
// If vim mode is requested, load it async and reconfigure the compartment
83+
if (vimMode) {
84+
void enableVimMode(client);
85+
}
86+
7987
return EditorState.create({
8088
doc: text,
8189
extensions: [
@@ -93,15 +101,8 @@ export function createEditorState(
93101
// bindings wont trigger if they have the same keys.
94102
commandKeyBindings,
95103

96-
// Enable vim mode, or not
97-
[
98-
...(client.ui.viewState.uiOptions.vimMode
99-
? [
100-
vim({ status: true }),
101-
EditorState.allowMultipleSelections.of(true),
102-
]
103-
: []),
104-
],
104+
// Vim mode compartment — starts empty, loaded async if needed
105+
client.vimCompartment.of([]),
105106
[
106107
...(readOnly ||
107108
client.ui.viewState.uiOptions.forcedROMode ||
@@ -425,6 +426,18 @@ export function createRegularKeyBindings(client: Client): Extension {
425426
}
426427
}
427428

429+
async function enableVimMode(client: Client) {
430+
const { vim } = await loadVim();
431+
if (client.editorView && client.vimCompartment) {
432+
client.editorView.dispatch({
433+
effects: client.vimCompartment.reconfigure([
434+
vim({ status: true }),
435+
EditorState.allowMultipleSelections.of(true),
436+
]),
437+
});
438+
}
439+
}
440+
428441
/**
429442
* Checks if the current platform is Mac-like (Mac, iPhone, iPod, iPad).
430443
* @returns A boolean indicating if the platform is Mac-like.

client/components/mini_editor.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
ViewPlugin,
99
type ViewUpdate,
1010
} from "@codemirror/view";
11-
import { getCM as vimGetCm, Vim, vim } from "@replit/codemirror-vim";
11+
import { getVimModule } from "../vim_loader.ts";
1212
import { createCommandKeyBindings } from "../codemirror/editor_state.ts";
1313

1414
type MiniEditorEvents = {
@@ -131,15 +131,16 @@ export function MiniEditor({
131131
// When vim mode is active, we need for CM to have created the new state
132132
// and the subscribe to the vim mode's events
133133
// This needs to happen in the next tick, so we wait a tick with setTimeout
134-
if (vimMode) {
134+
const vimMod = getVimModule();
135+
if (vimMode && vimMod) {
135136
// Only applies to vim mode
136137
setTimeout(() => {
137-
const cm = vimGetCm(editorViewRef.current!)!;
138+
const cm = vimMod.getCM(editorViewRef.current!)!;
138139
cm.on("vim-mode-change", ({ mode }: { mode: string }) => {
139140
vimModeRef.current = mode;
140141
});
141142
if (vimStartInInsertMode) {
142-
Vim.handleKey(cm, "i", "+input");
143+
vimMod.Vim.handleKey(cm, "i", "+input");
143144
}
144145
});
145146
}
@@ -150,8 +151,8 @@ export function MiniEditor({
150151
// Insert command bindings before vim-mode to ensure they're available
151152
// in normal mode. See editor_state.ts for more details.
152153
createCommandKeyBindings(globalThis.client),
153-
// Enable vim mode, or not
154-
[...(vimMode ? [vim()] : [])],
154+
// Enable vim mode, or not (uses already-loaded module if available)
155+
[...(vimMode && vimMod ? [vimMod.vim()] : [])],
155156
[
156157
...(editable
157158
? []

0 commit comments

Comments
 (0)