Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
151 commits
Select commit Hold shift + click to select a range
f72dcef
Add markdown-it and make Tiptap core a runtime dependency
XrxcGH Sep 30, 2026
208796b
Add the text block schema for the Phase 4 editor
XrxcGH Sep 30, 2026
a2a821c
Convert OpenNote Markdown to and from the text block document
XrxcGH Sep 30, 2026
2684753
Carry text element IDs, tags, styles, and checked to-dos
XrxcGH Sep 30, 2026
6b98912
Add an ID generator for text elements, rows, and columns
XrxcGH Sep 30, 2026
90fbb5d
Share the document outline helper between editor tests
XrxcGH Sep 30, 2026
e90a138
Add the paste pipeline's sanitizer for Word, OneNote, and the web
XrxcGH Sep 30, 2026
3278fb9
Merge remote-tracking branch 'origin/phase-3' into phase-4
XrxcGH Oct 3, 2026
4adcaf9
Add Phase 4's decision records 0020, 0021, 0022, 0024, and 0026
XrxcGH Oct 3, 2026
65c9119
Pin Tiptap at 3.31.4 and add Phase 4's editor dependencies
XrxcGH Oct 3, 2026
aeb2081
Keep editor/ a library layer with an ESLint boundary rule
XrxcGH Oct 3, 2026
94a16eb
Move the editor core to the plan's paths and add the editor skeleton
XrxcGH Oct 3, 2026
03aae67
Add the page service contract and Phase 4's platform clients
XrxcGH Oct 3, 2026
e974085
Add the page skeleton that typing reaches the core through
XrxcGH Oct 3, 2026
c5f23cc
Add the page test helpers and smoke tests on the stubs
XrxcGH Oct 3, 2026
097a700
Bridge the page commands to Phase 3's core and stub Phase 4's commands
XrxcGH Oct 3, 2026
0028e99
Load the page's editor in its own chunk and budget Phase 4's chunks
XrxcGH Oct 3, 2026
fd7f7d7
List the text box and object keys among the keys that always work
XrxcGH Oct 3, 2026
8410abe
Add the perf-typing job with the in-page typing benchmark
XrxcGH Oct 3, 2026
59397d9
Check in the real app that typed text survives a restart
XrxcGH Oct 3, 2026
0bf7d38
Give typed notes their colors: highlighters, pens, lists, and callouts
XrxcGH Oct 3, 2026
1e385db
Cache Markdown at every node and serialize any schema by name
XrxcGH Oct 3, 2026
a68ada1
Map table block data to and from the table editor's document
XrxcGH Oct 3, 2026
7cdccae
Give the page a real viewport with zoom and a remembered view
XrxcGH Oct 3, 2026
bcaf424
Run the page service contract against the memory service
XrxcGH Oct 3, 2026
c8de285
Give the editor its own task items, callouts, math, and image chips
XrxcGH Oct 3, 2026
1914fa7
Re-parse only the range of a block that changed
XrxcGH Oct 3, 2026
5e5134e
Parse large blocks in a module worker
XrxcGH Oct 3, 2026
b0e2bfd
Probe, check, and import images in the shell
XrxcGH Oct 3, 2026
d392f98
Download pasted web images with limits and address checks
XrxcGH Oct 3, 2026
9729b67
Read clipboard facts and issue tokens for Word's images
XrxcGH Oct 3, 2026
06d9787
Route the page's pointers and pan and pinch from them
XrxcGH Oct 3, 2026
6e6568c
Check spelling through the Windows spelling API
XrxcGH Oct 3, 2026
22cf61b
Keep the page view working without ResizeObserver
XrxcGH Oct 3, 2026
7aaa587
Keep each row's cells in column order when adding a column
XrxcGH Oct 3, 2026
a5e2f03
Add the table editor's cell keys, grid normalizer, and name
XrxcGH Oct 3, 2026
bc8fb1b
Expose the shown block layer and give Move row its keys
XrxcGH Oct 3, 2026
993d904
Show and edit table blocks, with their commands and toolbar
XrxcGH Oct 3, 2026
a222341
Show spelling errors on typed notes, with the menu and F7
XrxcGH Oct 3, 2026
c13f468
Add the reading order function and the pinned reorder
XrxcGH Oct 3, 2026
d037e92
Render text statically and mount editors in place from a pool
XrxcGH Oct 3, 2026
d5c17d0
Write a callout's first paragraph on its head line
XrxcGH Oct 3, 2026
53e36b8
Escape fixture text that follows other markup on its line
XrxcGH Oct 3, 2026
7c8f025
Check the shared Markdown fixtures through the neutral tree
XrxcGH Oct 3, 2026
48f54ed
Keep links of every scheme and test hostile and CommonMark input
XrxcGH Oct 3, 2026
2b114bc
Count UTF-8 offsets without encoding the text
XrxcGH Oct 3, 2026
3bfc05a
Register the formatting and block commands in both shortcut sets
XrxcGH Oct 3, 2026
e03929f
Batch typing, split automatic changes, and undo through the core
XrxcGH Oct 3, 2026
4aae590
Split the paste normalizers by source and map document colors
XrxcGH Oct 3, 2026
5106865
Write pasted table cells canonically and keep nested tables
XrxcGH Oct 3, 2026
cd62dd1
Convert Markdown shortcuts as typed, each as its own undo step
XrxcGH Oct 3, 2026
0162a5e
Fix the pinned reorder test's tuple cast
XrxcGH Oct 3, 2026
108d671
Add the title band, page layouts, new text boxes, and Reading view
XrxcGH Oct 3, 2026
c46f918
Move the DOM side of spelling squiggles out of the editor plugin
XrxcGH Oct 3, 2026
1f06cab
Read pages aloud with local Windows voices
XrxcGH Oct 3, 2026
0239876
Run each Markdown property 1,000 times in CI
XrxcGH Oct 3, 2026
3d978fb
Add the page diff that compare versions shows
XrxcGH Oct 3, 2026
a136d4e
Keep Tab inside text boxes and cross flowing blocks with the keys
XrxcGH Oct 3, 2026
29454c5
Show image blocks with resize, crop, and alt text
XrxcGH Oct 3, 2026
8b7cce8
Paste, drop, and copy on the page through one pipeline
XrxcGH Oct 3, 2026
c9dd669
Add the clipboard corpus, clipset, and the paste and image E2E specs
XrxcGH Oct 3, 2026
e6032bf
Record ADR 0025: paste, clipboard facts, and image import
XrxcGH Oct 3, 2026
773aca3
Open a slash menu of blocks from "/" at the start of a line
XrxcGH Oct 3, 2026
0893456
Clean math, image, and link text before writing it
XrxcGH Oct 3, 2026
04cd847
Test the == and $ flanking rules of the parser
XrxcGH Oct 3, 2026
0f0fab0
Add page history: versions, compare, and restoring parts
XrxcGH Oct 3, 2026
ce58106
Load WP5's registrations after start-up and trim unused strings
XrxcGH Oct 3, 2026
086c482
Make blocks objects: selection, keys, chrome, drags, and menus
XrxcGH Oct 3, 2026
e339ef0
Add the real-app specs for spelling, read aloud, and history
XrxcGH Oct 3, 2026
2a90172
Register table commands in idle time to keep start-up small
XrxcGH Oct 3, 2026
98677f6
Add code blocks' language button, picker, and keys
XrxcGH Oct 3, 2026
4eeed8e
Highlight code in idle time, behind page.codeHighlight
XrxcGH Oct 3, 2026
f1cc67c
Add the page.tables E2E spec
XrxcGH Oct 3, 2026
dfc5b65
Turn off WebView2's own spell checker in editors
XrxcGH Oct 3, 2026
e8db89c
Record spikes S1 and S2 in ADR 0023
XrxcGH Oct 3, 2026
caddee9
Move outline items and fold headings and list items
XrxcGH Oct 3, 2026
d7512eb
Move the blocks below a text box when an edit changes its height
XrxcGH Oct 3, 2026
c391a4b
Correct words as they end and date new pages
XrxcGH Oct 3, 2026
9666130
Name blocks by reading order and add the Reading order pane
XrxcGH Oct 3, 2026
eb1e7c4
Let notebooks restyle headings and text through style variables
XrxcGH Oct 3, 2026
21f96ef
Give Phase 5 real text geometry and layout notices
XrxcGH Oct 3, 2026
fa6de5a
Add the page view's end-to-end specs
XrxcGH Oct 3, 2026
9705cff
Record how the viewport was built and what spike S5 still needs
XrxcGH Oct 3, 2026
bb87797
Map page history and core events through the Tauri adapter
XrxcGH Oct 3, 2026
38e4662
Prove composition, opening, and crashes, and run the contract on the …
XrxcGH Oct 3, 2026
4e6d5c0
Keep one title band element from the tree's Enter to the world
XrxcGH Oct 3, 2026
ee0583c
Add the formatting bar and replay keyboard layouts through the dispat…
XrxcGH Oct 3, 2026
cbfa0ed
Rebuild only the fold buttons an edit touches, and benchmark the plugins
XrxcGH Oct 3, 2026
f79a1ae
Move the plugin benchmark out of editor/, which can't read fixtures
XrxcGH Oct 3, 2026
93f765d
Add the Canvas switch and trim the page view's start-up strings
XrxcGH Oct 3, 2026
ff72f6c
Give the idle rendering test time when the machine is busy
XrxcGH Oct 3, 2026
e73ae03
Keep Phase 2's tree menu as designed and expect Link on Ctrl+K
XrxcGH Oct 3, 2026
1c7f07e
Merge branch 'p4-wp1-markdown' into phase-4
XrxcGH Oct 3, 2026
0af212f
Merge branch 'p4-wp2-pages' into phase-4
XrxcGH Oct 3, 2026
740ffd9
Merge branch 'p4-wp3-view' into phase-4
XrxcGH Oct 3, 2026
8665e95
Merge branch 'p4-wp4-keys' into phase-4
XrxcGH Oct 3, 2026
5d2cc5a
Merge branch 'p4-wp5-images' into phase-4
XrxcGH Oct 3, 2026
493263e
Merge branch 'p4-wp6-tables' into phase-4
XrxcGH Oct 3, 2026
7bef021
Merge branch 'p4-wp7-spelling' into phase-4
XrxcGH Oct 3, 2026
367e409
Cross into text blocks that have no editor yet
XrxcGH Oct 3, 2026
78043e9
Destroy block views before the editor pool
XrxcGH Oct 3, 2026
cd3a52a
Bundle every imported package before component tests
XrxcGH Oct 3, 2026
ee68549
Keep a heading in place when its editor mounts
XrxcGH Oct 3, 2026
e50f8a7
Check static text for spelling whenever it renders
XrxcGH Oct 3, 2026
b81407b
Update tests to the merged Markdown, sync, and pool
XrxcGH Oct 3, 2026
e5b752d
Keep ProseMirror out of the start-up bundle
XrxcGH Oct 3, 2026
a589f92
Budget the merged page chunks as they load
XrxcGH Oct 3, 2026
42e876e
Load the formatting commands before the first shortcut
XrxcGH Oct 3, 2026
fd91792
Clear the block selection when another page is shown
XrxcGH Oct 3, 2026
ec9fb4d
Read the held page in the typing spec
XrxcGH Oct 3, 2026
2d0b2ef
Show the editing settings in Settings
XrxcGH Oct 3, 2026
d845a45
Color code in static text blocks
XrxcGH Oct 3, 2026
08d4450
Undo an automatic change back to the text as typed
XrxcGH Oct 3, 2026
e6943e7
Read every pasted attribute through its own parse rule
XrxcGH Oct 3, 2026
897e306
Number a page's edits only when the core accepts them
XrxcGH Oct 3, 2026
9e00b8e
Keep a refused text change unsent until it reaches the core
XrxcGH Oct 3, 2026
f1bc893
Read a link's scheme as a browser does before checking it
XrxcGH Oct 3, 2026
276acd8
Encode the page and asset IDs in an image's address
XrxcGH Oct 3, 2026
894c919
Shorten comments and split tests the code checks flagged
XrxcGH Oct 3, 2026
d9b98b2
Keep a clicked link on the page from navigating the window
XrxcGH Oct 3, 2026
8c74489
Never replace an unreadable page map, and check page arguments
XrxcGH Oct 3, 2026
80184bb
Contain paint per textblock and drop a costly first-child rule
XrxcGH Oct 3, 2026
b1967bc
Keep fold buttons while their heading or item is typed in
XrxcGH Oct 3, 2026
8a12b5e
Import the test formats' helpers only where they are built
XrxcGH Oct 3, 2026
f364955
Keep squiggles that typing only moved, so the page isn't repainted
XrxcGH Oct 3, 2026
74ec7ad
Measure typing in every Phase 4 condition and the motion gestures
XrxcGH Oct 3, 2026
7165b0a
Add the typing path's microbenchmarks
XrxcGH Oct 3, 2026
b1a559d
Add plain-text paste cases: prose with blank lines, and one URL
XrxcGH Oct 3, 2026
fc58752
Give the link field the keys typed while it opens
XrxcGH Oct 3, 2026
c0832ff
Show math in static text before an editor mounts
XrxcGH Oct 3, 2026
f7d9005
Add page screens to the axe sweep and a walk through typed notes
XrxcGH Oct 3, 2026
4bf48e7
Put held keys into the link field before React renders again
XrxcGH Oct 3, 2026
5b698ac
Write and format a page from the keyboard in the UI suite
XrxcGH Oct 3, 2026
4c9f7ee
Keep a touch pan going when the pressed element loses its capture
XrxcGH Oct 3, 2026
fd5e84b
Check touch and pen on a freeform page in the UI suite
XrxcGH Oct 3, 2026
7469f12
Add the help page on moving between blocks and the manual checklist
XrxcGH Oct 3, 2026
d1f6398
Merge main into phase-4: Phase 3 and the beta smoke fixes
XrxcGH Oct 3, 2026
bfd2291
Record Phase 4's typing, motion, and microbenchmark numbers
XrxcGH Oct 3, 2026
ad8bff2
Wait for the table cell's flush in the page walk
XrxcGH Oct 3, 2026
3b69571
Merge branch 'p4-review-fixes' into phase-4
XrxcGH Oct 3, 2026
ba55885
Update the READMEs for Phase 4
XrxcGH Oct 3, 2026
8910f8c
Skip the WebP check where Windows has no WebP codec
XrxcGH Oct 3, 2026
6481bfa
Load the link popover before Ctrl+K and mount it at once
XrxcGH Oct 3, 2026
6dd8d15
Wait for the inserted image before the walk goes on
XrxcGH Oct 3, 2026
4a73ac7
Wait for the new paragraph in the typing benchmark
XrxcGH Oct 3, 2026
2c54f43
Escape backslashes with the pipe in the corpus table helper
XrxcGH Oct 3, 2026
36b4673
Close the app by its window in the restart smoke spec
XrxcGH Oct 3, 2026
2921798
Stop two test flakes: caret placement and the picker's typing
XrxcGH Oct 3, 2026
1d16455
Let the page say when its command chunks are loaded
XrxcGH Oct 3, 2026
c422288
Wait for the note to be drawn before the typing benchmark places a caret
XrxcGH Oct 3, 2026
62a1339
Handle a failed idle load in later() instead of leaving it unhandled
XrxcGH Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,32 @@ jobs:
tests/ui/results
retention-days: 7

perf-typing:
# Phase 4's typing benchmark, in-page measure only (ARCHITECTURE.md section 24.5): every typing condition and
# the motion gestures, recorded as an artifact. Hosted runners are too noisy for the 16 ms gate itself, which
# the reference laptop's run checks with OPENNOTE_TYPING_GATE=1 (docs/perf/phase-4.md).
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install tools
run: npm ci
- name: Typing benchmark
run: npm run perf:typing
- name: Keep the numbers
if: always()
uses: actions/upload-artifact@v4
with:
name: perf-typing
path: tests/perf/typing/results
retention-days: 30

windows-app:
# Rust checks and a test build of the desktop app on Windows, the first target platform.
runs-on: windows-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ tests/e2e/.drivers/
tests/e2e/logs/
tests/ui/report/
tests/ui/results/
tests/perf/typing/results/
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 42 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,26 @@ OpenNote is an open-source note-taking app that combines your favorite writing,

OpenNote is in early development. Each phase of the [development plan](docs/DEVELOPMENT.md#5-phases) gets its own branch and merges into `main` when it meets its exit gate.

| Phase | Focus | Status |
| --------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------- |
| 0 | Project foundation | Done and merged |
| 1 | Spikes that test ink, text, PDF export, and audio | Done and merged |
| 2 | [App shell and navigation](https://github.com/XrxcGH/OpenNote/pull/8) | Done and merged 2026-10-02 |
| 3 | [Document model and storage](https://github.com/XrxcGH/OpenNote/pull/9) | Done and merged 2026-10-03 |
| 4 | Typed notes (text, Markdown, tables, code) | In progress |
| [5 to 14](docs/DEVELOPMENT.md#5-phases) | Ink, export, charts, search, audio, math, import, intelligence, hardening, and release | Core in progress; beta build being assembled |
| Phase | Focus | Status |
| --------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| 0 | Project foundation | Done and merged |
| 1 | Spikes that test ink, text, PDF export, and audio | Done and merged |
| 2 | [App shell and navigation](https://github.com/XrxcGH/OpenNote/pull/8) | Done and merged 2026-10-02 |
| 3 | [Document model and storage](https://github.com/XrxcGH/OpenNote/pull/9) | Done and merged 2026-10-03 |
| 4 | [Typed notes (text, Markdown, tables, code)](https://github.com/XrxcGH/OpenNote/pull/15) | Done and merged 2026-10-03 |
| [5 to 14](docs/DEVELOPMENT.md#5-phases) | Ink, export, charts, search, audio, math, import, intelligence, hardening, and release | In progress, being wired into the app; a first beta build exists on branch `beta` |

## What the app does today

OpenNote runs as a Windows desktop app. You can organize notebooks, sections, and pages, and write typed notes on a page, either in a simple document flow or in text boxes you place anywhere. Pages save on their own and recover after a crash. Typed notes support:

- Text boxes, headings, lists, checkboxes, bold and italic, links, quotes, and callouts
- Markdown shortcuts as you type (for example `# ` for a heading), and Markdown paste
- Images by paste, drag, or file picker, with resize and crop
- Tables, and code blocks with syntax highlighting
- Spell check through the Windows spelling service

Ink, export, charts, search, audio, math, and import are still being wired into the app.

## Downloads

Expand All @@ -39,28 +51,46 @@ cd OpenNote
npm install && npm start
```

That opens the OpenNote window in development mode. Run `npm run setup-hooks` once, so CHECKS runs before every commit. [CONTRIBUTING.md](CONTRIBUTING.md) has the full setup, the npm scripts, and the workflow for branches and pull requests.
That opens the OpenNote window in development mode. Run `npm run setup-hooks` once, so CHECKS runs before every commit. To test your changes, run:

```sh
npm test # CHECKS, lint rule, and unit tests
npm run test:components # component tests in the installed Edge or Chrome
npm run test:ui # screen and accessibility tests in Playwright
npm run test:e2e # real-app tests, which need tauri-driver and msedgedriver
npm run perf:typing # the typing latency benchmark
cargo test --workspace # Rust tests
```

[CONTRIBUTING.md](CONTRIBUTING.md) has the full setup, every npm script, and the workflow for branches and pull requests.

## Repository map

```text
app/ The desktop app (Phase 2)
app/ The desktop app (Phases 2 and 4)
src/ TypeScript and React interface
editor/ The typed notes editor: schema, Markdown, tables, and highlighting
features/
page/ The page view: text boxes, images, paste, and spell check
services/
pages/ The page service contract, with its Tauri and in-memory versions
src-tauri/ Rust shell, built on Tauri 2
brand/ Design tokens, logo, and app icon
checks/ The CHECKS quality gate
crates/ Rust libraries (Phase 3)
core/ Document model, file format, storage, undo, and search
updater/ Self-updater (fetch, verify, install, rollback)
docs/ Plans, specs, and guides
adr/ Architecture decision records
adr/ Architecture decision records, including Phase 4's 0020 to 0026
design/ Screen wireframes and the generator that draws them
format/ Note file format specification and tools
perf/ Performance records, starting with Phase 4's typing numbers
spikes/ Throwaway Phase 1 experiments and their results
tests/ End-to-end, UI, performance, and crash tests (Phase 3)
crash/ Crash safety tests that kill the app during saves
e2e/ Smoke set of real-app tests (keyboard, screen reader)
e2e/ Real-app tests: the smoke set (keyboard, screen reader) and typed notes
perf/ Performance benchmarks for every budget
typing/ The typing latency benchmark for the 20-page note (Phase 4)
ui/ Screenshot tests of every screen and size
.github/ Issue and pull request templates, and the CI and release workflows
```
Expand Down
13 changes: 11 additions & 2 deletions app/bundle-budget.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
{
"startup": { "jsKb": 160, "cssKb": 15 },
"startup": { "jsKb": 180, "cssKb": 15 },
"after": { "within": "features/page/", "entry": "features/page/PageBody" },
"lazyKb": {
"features/page": 150,
"editor/markdown/worker": 45,
"editor/highlight": 20,
"highlight.js/lib/languages": 5,
"features/page/history": 25,
"features/page/readAloud": 10,
"features/page/styles": 15,
"features/page/readingOrder": 15,
"features/page/PageBody": 225,
"features/page": 40,
"features/palette": 40,
"features/settings": 40,
"features/setup": 40,
Expand Down
32 changes: 32 additions & 0 deletions app/scripts/check-bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,38 @@ describe('check-bundle', () => {
]);
});

it('counts a group that loads after another one without what that one loads', () => {
const manifest: Manifest = {
'index.html': { file: 'assets/index.js', isEntry: true },
'src/features/page/PageBody.tsx': {
file: 'assets/page.js',
src: 'src/features/page/PageBody.tsx',
isDynamicEntry: true,
imports: ['_editor.js'],
},
'_editor.js': { file: 'assets/editor.js' },
'src/features/page/history/chunk.ts': {
file: 'assets/history.js',
src: 'src/features/page/history/chunk.ts',
isDynamicEntry: true,
imports: ['_editor.js', '_diff.js'],
},
'_diff.js': { file: 'assets/diff.js' },
'src/features/palette/Palette.tsx': {
file: 'assets/palette.js',
src: 'src/features/palette/Palette.tsx',
isDynamicEntry: true,
imports: ['_editor.js'],
},
};
const budget: Budget = { ...BUDGET, after: { within: 'features/page/', entry: 'features/page/PageBody' } };
expect(groups(manifest, budget).slice(2)).toEqual([
{ name: 'src/features/page/PageBody.tsx', files: ['assets/page.js', 'assets/editor.js'], limitKb: 3 },
{ name: 'src/features/page/history/chunk.ts', files: ['assets/history.js', 'assets/diff.js'], limitKb: 3 },
{ name: 'src/features/palette/Palette.tsx', files: ['assets/palette.js', 'assets/editor.js'], limitKb: 2 },
]);
});

it('measures gzipped sizes and reports groups over their limit', () => {
const dist = mkdtempSync(join(tmpdir(), 'bundle-'));
writeFileSync(join(dist, 'small.js'), 'export {};');
Expand Down
17 changes: 14 additions & 3 deletions app/scripts/check-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// gzips each group of files, and fails when a group passes its limit in app/bundle-budget.json. The groups are:
// - start-up JavaScript: the entry and everything it imports statically
// - start-up CSS: the style sheets of those chunks
// - one lazy group per dynamic import, without what start-up already loads
// - one lazy group per dynamic import, without what start-up already loads. A group that only ever loads after
// another one (the page's on-demand chunks load after the page) also leaves out what that one loads.
//
// Run after `npm run app:build`:
// node app/scripts/check-bundle.ts [--dist app/dist] [--compare-with <old report.json>] [--report <out.json>]
Expand All @@ -29,6 +30,11 @@ export interface Budget {
/** Limits for lazy groups whose source path contains the key, such as "features/palette". */
lazyKb: Record<string, number>;
lazyDefaultKb: number;
/**
* Lazy groups that load only after another one: a group whose source path contains `within` counts without the
* files of the group whose path contains `entry`, which is loaded by then.
*/
after?: { within: string; entry: string };
}

export interface Group {
Expand Down Expand Up @@ -68,8 +74,13 @@ export function groups(manifest: Manifest, budget: Budget): Group[] {
limitKb: budget.startup.cssKb,
},
];
for (const key of keys.filter((k) => manifest[k].isDynamicEntry)) {
const own = [...staticClosure(manifest, [key])].filter((k) => !startup.has(k)).map((k) => manifest[k]);
const lazyKeys = keys.filter((k) => manifest[k].isDynamicEntry);
const first = budget.after && lazyKeys.find((k) => k.includes(budget.after!.entry));
const loadedFirst = first ? staticClosure(manifest, [first]) : new Set<string>();
for (const key of lazyKeys) {
const follows = first !== undefined && key !== first && key.includes(budget.after!.within);
const loaded = (k: string) => startup.has(k) || (follows && loadedFirst.has(k));
const own = [...staticClosure(manifest, [key])].filter((k) => !loaded(k)).map((k) => manifest[k]);
const limit = Object.entries(budget.lazyKb).find(([fragment]) => key.includes(fragment))?.[1];
const files = [...own.map((chunk) => chunk.file), ...new Set(own.flatMap((chunk) => chunk.css ?? []))];
result.push({ name: manifest[key].src ?? key, files, limitKb: limit ?? budget.lazyDefaultKb });
Expand Down
49 changes: 49 additions & 0 deletions app/scripts/package-imports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { importsIn, packageImports } from './package-imports.ts';

describe('importsIn', () => {
it('finds static imports, re-exports, and lazy imports of packages', () => {
const source = [
"import { Editor } from '@tiptap/core';",
"import Moon, {\n type Icon,\n} from '@phosphor-icons/react/dist/csr/Moon';",
"export { lowlight } from 'lowlight';",
"const load = () => import('markdown-it');",
].join('\n');
expect(importsIn(source)).toEqual([
'@tiptap/core',
'@phosphor-icons/react/dist/csr/Moon',
'lowlight',
'markdown-it',
]);
});

it('leaves out the app’s own modules, types, styles, Node, and the test runner', () => {
const source = [
"import { t } from '../strings/t';",
"import type { Root } from 'hast';",
"import type { Node } from '@tiptap/pm/model';",
"export type { Plugin } from 'vite';",
"import sheet from 'some-package/style.css';",
"import { readFileSync } from 'node:fs';",
"import { expect } from 'vitest';",
"import { page } from 'vitest/browser';",
].join('\n');
expect(importsIn(source)).toEqual([]);
});
});

describe('packageImports', () => {
const found = packageImports(join(import.meta.dirname, '..', 'src'));

it('lists what the app imports, once each and sorted', () => {
expect(found).toContain('react');
expect(found).toContain('@tiptap/pm/state');
expect(found).toEqual([...new Set(found)].sort());
});

it('lists the modules lazy chunks import, which a late scan would meet mid-test', () => {
expect(found.some((name) => name.startsWith('@phosphor-icons/react/dist/csr/'))).toBe(true);
expect(found.some((name) => name.startsWith('highlight.js/lib/languages/'))).toBe(true);
});
});
43 changes: 43 additions & 0 deletions app/scripts/package-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// The package modules the app's source imports, for the component tests' dependency bundling (vitest.config.ts).
//
// Vite bundles a package the first time it meets it. One it meets after the tests start, in a chunk that loads
// lazily, reloads the page under a running test, and the test fails. Vite's own scan misses some of them, so on a
// machine with no cache (CI) the first run failed. Listing every import bundles them all before the first test.

import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';

/** `import ... from 'x'`, `export ... from 'x'`, and `import('x')`, where x is a package and no type import. */
const IMPORT = /\b(?:import|export)\s+(?!type\b)[^'"`;]*?\bfrom\s+'([^'./][^']*)'|\bimport\(\s*'([^'./][^']*)'\s*\)/g;

/** Packages that are not bundled for the browser: Node's own, the test runner's, and type-only ones. */
const SKIPPED = /^(node:|virtual:|vitest($|\/)|@vitest\/|hast$)/;

const SOURCE = /\.tsx?$/;
const TEST = /\.test\.tsx?$/;

/** The package modules imported in one module's text. */
export function importsIn(source: string): string[] {
const found: string[] = [];
for (const match of source.matchAll(IMPORT)) {
const name = match[1] ?? match[2];
if (name && !SKIPPED.test(name) && !/\.css$|\?/.test(name)) found.push(name);
}
return found;
}

/** Every package module imported under `dir`, outside test files, sorted. */
export function packageImports(dir: string): string[] {
const found = new Set<string>();
const walk = (folder: string): void => {
for (const entry of readdirSync(folder, { withFileTypes: true })) {
const path = join(folder, entry.name);
if (entry.isDirectory()) walk(path);
else if (SOURCE.test(entry.name) && !TEST.test(entry.name)) {
for (const name of importsIn(readFileSync(path, 'utf8'))) found.add(name);
}
}
};
walk(dir);
return [...found].sort();
}
6 changes: 6 additions & 0 deletions app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
tauri = { version = "2", features = [] }
# Web images for paste (Phase 4 ARCHITECTURE.md section 12.2), with the same TLS stack and version as the updater.
ureq = { version = "3.4.2", default-features = false, features = ["native-tls-no-default"] }

[target.'cfg(windows)'.dependencies]
webview2-com = "0.39.1"
Expand All @@ -39,14 +41,18 @@ features = [
"Networking_Connectivity",
"UI_ViewManagement",
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Security",
"Win32_Storage_FileSystem",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_DataExchange",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_Registry",
"Win32_System_Threading",
"Win32_System_Variant",
Expand Down
25 changes: 24 additions & 1 deletion app/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,29 @@
"allow-updater-unskip",
"allow-updater-go-back",
"allow-notes-snapshot-load",
"allow-notes-snapshot-save"
"allow-notes-snapshot-save",
"allow-page-open",
"allow-page-apply",
"allow-page-undo",
"allow-page-redo",
"allow-page-save-now",
"allow-page-close",
"allow-history-list",
"allow-history-open",
"allow-history-restore",
"allow-history-restore-blocks",
"allow-history-name",
"allow-clipboard-facts",
"allow-clipboard-read",
"allow-image-import",
"allow-image-import-url",
"allow-image-import-clip",
"allow-spell-languages",
"allow-spell-check",
"allow-spell-suggest",
"allow-spell-add-word",
"allow-spell-remove-word",
"allow-speech-voices",
"allow-speech-synthesize"
]
}
Loading
Loading