From b52c870f3b4d8cafbcd23786ace39a22ecdf1655 Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Thu, 30 Jul 2026 15:03:14 +0000 Subject: [PATCH 01/28] Move to unreleased version of Logux --- api/package.json | 4 +- core/package.json | 12 +-- loader-tests/package.json | 4 +- pnpm-lock.yaml | 198 +++++++++++++++++++++++--------------- pnpm-workspace.yaml | 2 +- server/modules/sync.ts | 12 +-- server/package.json | 6 +- server/test/users.test.ts | 8 +- web/.size-limit.json | 2 +- web/package.json | 6 +- 10 files changed, 152 insertions(+), 102 deletions(-) diff --git a/api/package.json b/api/package.json index a7d24c84..c29b5ba0 100644 --- a/api/package.json +++ b/api/package.json @@ -7,10 +7,10 @@ "./package.json": "./package.json" }, "dependencies": { - "@logux/actions": "0.5.0" + "@logux/actions": "github:logux/actions#next" }, "devDependencies": { - "@logux/core": "0.10.0" + "@logux/core": "github:logux/core#next" }, "engines": { "node": "^26.5.0", diff --git a/core/package.json b/core/package.json index ca21a44b..ec12fa10 100644 --- a/core/package.json +++ b/core/package.json @@ -12,9 +12,9 @@ "test:coverage": "bnt --coverage 100 --coverage-exclude '{devtools.ts,errors.ts,test.ts,../api/**,../server/**,../proxy/**,**/test/**}'" }, "dependencies": { - "@logux/actions": "0.5.0", - "@logux/client": "0.23.2", - "@logux/core": "0.10.0", + "@logux/actions": "github:logux/actions#next", + "@logux/client": "github:logux/client#next", + "@logux/core": "github:logux/core#next", "@nanostores/i18n": "1.3.3", "@nanostores/persistent": "1.3.5", "@slowreader/api": "workspace:*", @@ -24,15 +24,15 @@ "lz-string": "1.5.0", "nanodelay": "2.0.2", "nanoid": "6.0.0", - "nanostores": "1.4.1" + "nanostores": "1.4.2" }, "devDependencies": { - "@logux/server": "0.14.0", + "@logux/server": "github:logux/server", "@slowreader/proxy": "workspace:*", "@types/jsdom": "28.0.3", "@types/trusted-types": "2.0.7", "better-node-test": "1.0.0", - "jsdom": "30.0.0", + "jsdom": "30.0.1", "nanospy": "2.0.2" }, "engines": { diff --git a/loader-tests/package.json b/loader-tests/package.json index 37680768..be0b905f 100644 --- a/loader-tests/package.json +++ b/loader-tests/package.json @@ -13,8 +13,8 @@ "@slowreader/proxy": "workspace:*", "@types/jsdom": "28.0.3", "@types/trusted-types": "2.0.7", - "jsdom": "30.0.0", - "nanostores": "1.4.1", + "jsdom": "30.0.1", + "nanostores": "1.4.2", "yaml": "2.9.0" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87ddee49..399568d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,30 +118,30 @@ importers: api: dependencies: '@logux/actions': - specifier: 0.5.0 - version: 0.5.0(@logux/core@0.10.0) + specifier: github:logux/actions#next + version: git+ssh://git@github.com/logux/actions.git#fbede590f2f311130c3db2c0a231228ca397a6f6(@logux/core@0.10.0) devDependencies: '@logux/core': - specifier: 0.10.0 - version: 0.10.0 + specifier: github:logux/core#next + version: git+ssh://git@github.com/logux/core.git#c558b5ff2c8b17bf29b39cf5db94af174966659e core: dependencies: '@logux/actions': - specifier: 0.5.0 - version: 0.5.0(@logux/core@0.10.0) + specifier: github:logux/actions#next + version: git+ssh://git@github.com/logux/actions.git#fbede590f2f311130c3db2c0a231228ca397a6f6(@logux/core@0.10.0) '@logux/client': - specifier: 0.23.2 - version: 0.23.2(nanostores@1.4.1)(react-dom@18.3.1)(react@18.3.1) + specifier: github:logux/client#next + version: git+ssh://git@github.com/logux/client.git#a80ea1974f303f281d94a614d86f320ad422737f(nanostores@1.4.2)(react-dom@18.3.1)(react@18.3.1) '@logux/core': - specifier: 0.10.0 - version: 0.10.0 + specifier: github:logux/core#next + version: git+ssh://git@github.com/logux/core.git#c558b5ff2c8b17bf29b39cf5db94af174966659e '@nanostores/i18n': specifier: 1.3.3 - version: 1.3.3(nanostores@1.4.1) + version: 1.3.3(nanostores@1.4.2) '@nanostores/persistent': specifier: 1.3.5 - version: 1.3.5(nanostores@1.4.1) + version: 1.3.5(nanostores@1.4.2) '@slowreader/api': specifier: workspace:* version: link:../api @@ -164,12 +164,12 @@ importers: specifier: 6.0.0 version: 6.0.0 nanostores: - specifier: 1.4.1 - version: 1.4.1 + specifier: 1.4.2 + version: 1.4.2 devDependencies: '@logux/server': - specifier: 0.14.0 - version: 0.14.0 + specifier: github:logux/server + version: git+ssh://git@github.com/logux/server.git#3a314f86868e9436704d0b3ea29fc8a7d877cfd3 '@slowreader/proxy': specifier: workspace:* version: link:../proxy @@ -183,8 +183,8 @@ importers: specifier: 1.0.0 version: 1.0.0 jsdom: - specifier: 30.0.0 - version: 30.0.0 + specifier: 30.0.1 + version: 30.0.1 nanospy: specifier: 2.0.2 version: 2.0.2 @@ -213,11 +213,11 @@ importers: specifier: 2.0.7 version: 2.0.7 jsdom: - specifier: 30.0.0 - version: 30.0.0 + specifier: 30.0.1 + version: 30.0.1 nanostores: - specifier: 1.4.1 - version: 1.4.1 + specifier: 1.4.2 + version: 1.4.2 yaml: specifier: 2.9.0 version: 2.9.0 @@ -234,11 +234,11 @@ importers: specifier: 0.5.4 version: 0.5.4 '@logux/actions': - specifier: 0.5.0 - version: 0.5.0(@logux/core@0.10.0) + specifier: github:logux/actions#next + version: git+ssh://git@github.com/logux/actions.git#fbede590f2f311130c3db2c0a231228ca397a6f6(@logux/core@0.10.0) '@logux/server': - specifier: 0.14.0 - version: 0.14.0 + specifier: github:logux/server + version: git+ssh://git@github.com/logux/server.git#3a314f86868e9436704d0b3ea29fc8a7d877cfd3 '@slowreader/api': specifier: workspace:* version: link:../api @@ -265,8 +265,8 @@ importers: version: 4.23.1 devDependencies: '@logux/client': - specifier: 0.23.2 - version: 0.23.2(nanostores@1.4.1)(react-dom@18.3.1)(react@18.3.1) + specifier: github:logux/client#next + version: git+ssh://git@github.com/logux/client.git#a80ea1974f303f281d94a614d86f320ad422737f(nanostores@1.4.2)(react-dom@18.3.1)(react@18.3.1) better-node-test: specifier: 1.0.0 version: 1.0.0 @@ -292,26 +292,26 @@ importers: specifier: 5.3.0 version: 5.3.0 '@logux/client': - specifier: 0.23.2 - version: 0.23.2(nanostores@1.4.1)(react-dom@18.3.1)(react@18.3.1) + specifier: github:logux/client#next + version: git+ssh://git@github.com/logux/client.git#a80ea1974f303f281d94a614d86f320ad422737f(nanostores@1.4.2)(react-dom@18.3.1)(react@18.3.1) '@logux/core': - specifier: 0.10.0 - version: 0.10.0 + specifier: github:logux/core#next + version: git+ssh://git@github.com/logux/core.git#c558b5ff2c8b17bf29b39cf5db94af174966659e '@mdi/js': specifier: 7.4.47 version: 7.4.47 '@nanostores/i18n': specifier: 1.3.3 - version: 1.3.3(nanostores@1.4.1) + version: 1.3.3(nanostores@1.4.2) '@nanostores/media-query': specifier: 0.1.2 - version: 0.1.2(nanostores@1.4.1) + version: 0.1.2(nanostores@1.4.2) '@nanostores/persistent': specifier: 1.3.5 - version: 1.3.5(nanostores@1.4.1) + version: 1.3.5(nanostores@1.4.2) '@nanostores/router': specifier: 1.1.0 - version: 1.1.0(nanostores@1.4.1) + version: 1.1.0(nanostores@1.4.2) '@slowreader/core': specifier: workspace:* version: link:../core @@ -343,8 +343,8 @@ importers: specifier: 1.33.0 version: 1.33.0 nanostores: - specifier: 1.4.1 - version: 1.4.1 + specifier: 1.4.2 + version: 1.4.2 postcss: specifier: 8.5.25 version: 8.5.25 @@ -1121,12 +1121,21 @@ packages: peerDependencies: '@logux/core': ^0.10.0 - '@logux/client@0.23.2': - resolution: {integrity: sha512-cqDegrUH+u7WlHUa7bnq7+0UwPLlNgy+YprruO0d3uG/oO6vfkGyWUZerQt7htaKOah2Hl0vqJgvvZljNfL98w==} - engines: {node: ^20.0.0 || >=22.0.0} + '@logux/actions@git+ssh://git@github.com/logux/actions.git#fbede590f2f311130c3db2c0a231228ca397a6f6': + resolution: {commit: fbede590f2f311130c3db2c0a231228ca397a6f6, repo: git@github.com:logux/actions.git, type: git} + version: 0.5.0 + engines: {node: ^22.0.0 || ^24.0.0 || >=26.0.0} + peerDependencies: + '@logux/core': ^0.10.0 + + '@logux/client@git+ssh://git@github.com/logux/client.git#a80ea1974f303f281d94a614d86f320ad422737f': + resolution: {commit: a80ea1974f303f281d94a614d86f320ad422737f, repo: git@github.com:logux/client.git, type: git} + version: 0.23.2 + engines: {node: ^22.0.0 || ^24.0.0 || >=26.0.0} peerDependencies: '@nanostores/preact': '>=0.0.0' '@nanostores/react': '>=0.0.0' + '@nanostores/sql': '>=0.0.0' '@nanostores/vue': '>=0.0.0' nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0 preact: '>=10.0.0' @@ -1138,6 +1147,8 @@ packages: optional: true '@nanostores/react': optional: true + '@nanostores/sql': + optional: true '@nanostores/vue': optional: true preact: @@ -1153,6 +1164,11 @@ packages: resolution: {integrity: sha512-VbgVkEloAe2ym5w0cjyRI16Eeyk2R0BgJa9pMYT1FcJEhFnzrB23E0ExIEhrok0iD48bFh7goledwmDLmgWaiw==} engines: {node: ^20.0.0 || >=22.0.0} + '@logux/core@git+ssh://git@github.com/logux/core.git#c558b5ff2c8b17bf29b39cf5db94af174966659e': + resolution: {commit: c558b5ff2c8b17bf29b39cf5db94af174966659e, repo: git@github.com:logux/core.git, type: git} + version: 0.10.0 + engines: {node: ^22.0.0 || ^24.0.0 || >=26.0.0} + '@logux/eslint-config@57.1.0': resolution: {integrity: sha512-oOlYsAu5iQBzNL75QqGWVnpgle/Nz5/Gp5P6BtHRy5vBg7UXyGVqEzaG3lMHF6dgYGVb+wkLqIC0DWIH8wp/LA==} engines: {node: '>=18.0.0'} @@ -1181,6 +1197,11 @@ packages: resolution: {integrity: sha512-a7KRD30U252cfIekBGktEIRuwFRT7vOPZXkEsD05DwApsPi70iRnFaHEgMde8tZu+jJUuTDN/xMgabtjzHzXaQ==} engines: {node: ^20.0.0 || >=22.0.0} + '@logux/server@git+ssh://git@github.com/logux/server.git#3a314f86868e9436704d0b3ea29fc8a7d877cfd3': + resolution: {commit: 3a314f86868e9436704d0b3ea29fc8a7d877cfd3, repo: git@github.com:logux/server.git, type: git} + version: 0.14.0 + engines: {node: ^22.5.0 || ^24.0.0 || >=26.0.0} + '@logux/stylelint-config@1.0.1': resolution: {integrity: sha512-a8vGb8ehfLtd5+ivfSgsA2MQpoM1K1aB+gmF1n6mAykweXhweWSmQzGPXYkpSyxWuo6UCZi5T1mqpVnE0oSlkw==} peerDependencies: @@ -2699,8 +2720,8 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.16: - resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + brace-expansion@1.1.17: + resolution: {integrity: sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==} brace-expansion@2.1.3: resolution: {integrity: sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==} @@ -3141,8 +3162,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.397: - resolution: {integrity: sha512-khGTy9U9x02KEtsKM8vx5A62BsRmcOsIgDpWr1ImE32Ax8GxHGPHZf+Eu9H8zOOyHJnB0jTbseyTHbq2XCT8yw==} + electron-to-chromium@1.5.398: + resolution: {integrity: sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3153,8 +3174,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.24.3: - resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} + enhanced-resolve@5.24.4: + resolution: {integrity: sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -3738,8 +3759,8 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true - jsdom@30.0.0: - resolution: {integrity: sha512-JQHfRGmmKmaZoUAvIgff5jjG/0SzTQlGz8c7t72KzBzo8ZULEjAjnYE0sNwBOUA4QtWwYE2xoYitg8NFsmiYxA==} + jsdom@30.0.1: + resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} peerDependencies: canvas: ^3.2.3 @@ -4118,6 +4139,10 @@ packages: resolution: {integrity: sha512-6AS5aCSXsjoxq2Jr9CdaAeT60yoYDOTp6po9ziqeOeY6vf6uTEHYSqWql6EFILrM3fEfXgkZ4KqE9L0rTm/wlA==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + nanoevents@10.0.0: + resolution: {integrity: sha512-PmIJ3BNxOzgVgnS1r/Pvyj6eZx/xUV2JCPogh0brD6smDIwjRlr1KBl9yoRX323PlfaNrBDDnfCgC9SL00OjSg==} + engines: {node: ^22.0.0 || ^24.0.0 || >=26.0.0} + nanoevents@9.1.0: resolution: {integrity: sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4144,8 +4169,8 @@ packages: resolution: {integrity: sha512-AvkslkHQavd4abp7clE0xsv4afGpBpnWUqWY23V3o4ljdyi/YIOpLiiWtrlLh1oR7kWC4GT7iBN5M2SjL5I5yw==} engines: {node: ^8.0.0 || ^10.0.0 || ^12.0.0 || ^14.0.0 || ^16.0.0 || ^18.0.0 || ^20.0.0 || ^22.0.0 || ^24.0.0 || >=26.0.0} - nanostores@1.4.1: - resolution: {integrity: sha512-PGd3uPojJB9Z07d5NX3Db/SOSBbyy3wLMUGq0GpnEEJfVzY9mq7daPMAZ3jObV5D3Jn+YKND636eI5ULg7F80Q==} + nanostores@1.4.2: + resolution: {integrity: sha512-Wxv8Roefr2nqtiRG0bnaFlpYqpIVtOEeJZHaH+4nGgOK1/7n6OHOuHCb/bhqrNQgZM8fyd0s1PqhdrJc9Ib44g==} engines: {node: ^20.0.0 || >=22.0.0} napi-postinstall@0.3.4: @@ -5864,15 +5889,17 @@ snapshots: dependencies: '@logux/core': 0.10.0 - '@logux/client@0.23.2(nanostores@1.4.1)(react-dom@18.3.1)(react@18.3.1)': + '@logux/actions@git+ssh://git@github.com/logux/actions.git#fbede590f2f311130c3db2c0a231228ca397a6f6(@logux/core@0.10.0)': dependencies: - '@logux/actions': 0.5.0(@logux/core@0.10.0) - '@logux/core': 0.10.0 - fast-json-stable-stringify: 2.1.0 - nanodelay: 2.0.2 - nanoevents: 9.1.0 - nanoid: 5.1.16 - nanostores: 1.4.1 + '@logux/core': git+ssh://git@github.com/logux/core.git#c558b5ff2c8b17bf29b39cf5db94af174966659e + + '@logux/client@git+ssh://git@github.com/logux/client.git#a80ea1974f303f281d94a614d86f320ad422737f(nanostores@1.4.2)(react-dom@18.3.1)(react@18.3.1)': + dependencies: + '@logux/actions': git+ssh://git@github.com/logux/actions.git#fbede590f2f311130c3db2c0a231228ca397a6f6(@logux/core@0.10.0) + '@logux/core': git+ssh://git@github.com/logux/core.git#c558b5ff2c8b17bf29b39cf5db94af174966659e + nanoevents: 10.0.0 + nanoid: 6.0.0 + nanostores: 1.4.2 optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -5881,6 +5908,10 @@ snapshots: dependencies: nanoevents: 9.1.0 + '@logux/core@git+ssh://git@github.com/logux/core.git#c558b5ff2c8b17bf29b39cf5db94af174966659e': + dependencies: + nanoevents: 10.0.0 + '@logux/eslint-config@57.1.0(@typescript-eslint/utils@8.65.0)(eslint-plugin-svelte@3.22.0)(eslint@10.8.0)(supports-color@10.2.2)(svelte-eslint-parser@1.8.0)(svelte@5.56.8)(typescript@6.0.3)': dependencies: '@eslint/eslintrc': 3.3.6(supports-color@10.2.2) @@ -5923,6 +5954,19 @@ snapshots: - bufferutil - utf-8-validate + '@logux/server@git+ssh://git@github.com/logux/server.git#3a314f86868e9436704d0b3ea29fc8a7d877cfd3': + dependencies: + '@logux/actions': git+ssh://git@github.com/logux/actions.git#fbede590f2f311130c3db2c0a231228ca397a6f6(@logux/core@0.10.0) + '@logux/core': git+ssh://git@github.com/logux/core.git#c558b5ff2c8b17bf29b39cf5db94af174966659e + cookie: 2.0.1 + fastq: 1.20.1 + nanoevents: 10.0.0 + nanoid: 6.0.0 + ws: 8.21.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@logux/stylelint-config@1.0.1(stylelint@17.14.1)': dependencies: stylelint: 17.14.1(supports-color@10.2.2)(typescript@6.0.3) @@ -5933,21 +5977,21 @@ snapshots: '@mdi/js@7.4.47': {} - '@nanostores/i18n@1.3.3(nanostores@1.4.1)': + '@nanostores/i18n@1.3.3(nanostores@1.4.2)': dependencies: - nanostores: 1.4.1 + nanostores: 1.4.2 - '@nanostores/media-query@0.1.2(nanostores@1.4.1)': + '@nanostores/media-query@0.1.2(nanostores@1.4.2)': dependencies: - nanostores: 1.4.1 + nanostores: 1.4.2 - '@nanostores/persistent@1.3.5(nanostores@1.4.1)': + '@nanostores/persistent@1.3.5(nanostores@1.4.2)': dependencies: - nanostores: 1.4.1 + nanostores: 1.4.2 - '@nanostores/router@1.1.0(nanostores@1.4.1)': + '@nanostores/router@1.1.0(nanostores@1.4.2)': dependencies: - nanostores: 1.4.1 + nanostores: 1.4.2 '@napi-rs/wasm-runtime@1.2.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: @@ -7050,7 +7094,7 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.16: + brace-expansion@1.1.17: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 @@ -7071,7 +7115,7 @@ snapshots: dependencies: baseline-browser-mapping: 2.11.7 caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.397 + electron-to-chromium: 1.5.398 node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.7) @@ -7378,7 +7422,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.397: {} + electron-to-chromium@1.5.398: {} emoji-regex@10.6.0: {} @@ -7386,7 +7430,7 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.24.3: + enhanced-resolve@5.24.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -7542,7 +7586,7 @@ snapshots: eslint-plugin-n@17.24.0(eslint@10.8.0)(typescript@6.0.3): dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0) - enhanced-resolve: 5.24.3 + enhanced-resolve: 5.24.4 eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2) eslint-plugin-es-x: 7.8.0(eslint@10.8.0) get-tsconfig: 4.14.0 @@ -8070,7 +8114,7 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@30.0.0: + jsdom@30.0.1: dependencies: '@asamuzakjp/css-color': 6.0.5 '@asamuzakjp/dom-selector': 8.3.0 @@ -8502,7 +8546,7 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 1.1.16 + brace-expansion: 1.1.17 minimatch@9.0.9: dependencies: @@ -8524,7 +8568,7 @@ snapshots: dompurify: 3.4.12 highlight.js: 11.11.1 marked: 18.0.7 - nanostores: 1.4.1 + nanostores: 1.4.2 yaml: 2.9.0 transitivePeerDependencies: - '@logux/core' @@ -8535,6 +8579,8 @@ snapshots: nanodelay@2.0.2: {} + nanoevents@10.0.0: {} + nanoevents@9.1.0: {} nanoid@3.3.16: {} @@ -8549,7 +8595,7 @@ snapshots: nanospy@2.0.2: {} - nanostores@1.4.1: {} + nanostores@1.4.2: {} napi-postinstall@0.3.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index aa24a98c..d04170b2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,7 +12,7 @@ allowBuilds: esbuild: true unrs-resolver: false -blockExoticSubdeps: true +blockExoticSubdeps: false forceLegacyDeploy: true diff --git a/server/modules/sync.ts b/server/modules/sync.ts index e722531b..4cb305bb 100644 --- a/server/modules/sync.ts +++ b/server/modules/sync.ts @@ -15,10 +15,10 @@ export default (server: BaseServer): void => { async process(ctx, action, meta) { await db.insert(actions).values({ added: await server.log.store.getLastAdded(), - compressed: action.z, - encrypted: Buffer.from(action.d, 'base64'), + compressed: action.compressed, + encrypted: Buffer.from(action.d), id: meta.id, - iv: Buffer.from(action.iv, 'base64'), + iv: Buffer.from(action.iv), subprotocol: meta.subprotocol ?? SUBPROTOCOL, time: meta.time - EPOCH, userId: ctx.userId @@ -55,9 +55,9 @@ export default (server: BaseServer): void => { return list.map(column => { return [ zero({ - d: Buffer.from(column.encrypted).toString('base64'), - iv: Buffer.from(column.iv).toString('base64'), - z: column.compressed + compressed: column.compressed, + d: column.encrypted, + iv: column.iv }), { added: column.added, diff --git a/server/package.json b/server/package.json index 712c6fbd..7aa96665 100644 --- a/server/package.json +++ b/server/package.json @@ -21,8 +21,8 @@ }, "dependencies": { "@electric-sql/pglite": "0.5.4", - "@logux/actions": "0.5.0", - "@logux/server": "0.14.0", + "@logux/actions": "github:logux/actions#next", + "@logux/server": "github:logux/server", "@slowreader/api": "workspace:*", "@slowreader/proxy": "workspace:*", "argon2": "0.45.1", @@ -33,7 +33,7 @@ "tsx": "4.23.1" }, "devDependencies": { - "@logux/client": "0.23.2", + "@logux/client": "github:logux/client#next", "better-node-test": "1.0.0", "drizzle-kit": "0.31.10" }, diff --git a/server/test/users.test.ts b/server/test/users.test.ts index f9c8f5d7..9560b4ee 100644 --- a/server/test/users.test.ts +++ b/server/test/users.test.ts @@ -47,8 +47,12 @@ describe('server users', () => { cookie: { session: userB.session } }) - await clientA1.process(zero({ d: 'a', iv: 'a', z: false })) - await clientB.process(zero({ d: 'b', iv: 'b', z: false })) + await clientA1.process( + zero({ compressed: false, d: Buffer.from('a'), iv: Buffer.from('a') }) + ) + await clientB.process( + zero({ compressed: false, d: Buffer.from('b'), iv: Buffer.from('b') }) + ) await clientA1.process(deleteUser({})) await setTimeout(100) diff --git a/web/.size-limit.json b/web/.size-limit.json index a8b9537a..cb0bef4e 100644 --- a/web/.size-limit.json +++ b/web/.size-limit.json @@ -22,6 +22,6 @@ "name": "Core scripts to execute", "path": "dist/assets/index-*.js", "brotli": false, - "limit": "315 KB" + "limit": "320 KB" } ] diff --git a/web/package.json b/web/package.json index 49d40720..346a53ef 100644 --- a/web/package.json +++ b/web/package.json @@ -27,8 +27,8 @@ "@fontsource-variable/noto-serif": "5.3.0", "@fontsource/inika": "5.3.0", "@fontsource/playpen-sans": "5.3.0", - "@logux/client": "0.23.2", - "@logux/core": "0.10.0", + "@logux/client": "github:logux/client#next", + "@logux/core": "github:logux/core#next", "@mdi/js": "7.4.47", "@nanostores/i18n": "1.3.3", "@nanostores/media-query": "0.1.2", @@ -44,7 +44,7 @@ "autoprefixer": "10.5.4", "keyux": "0.11.6", "lightningcss": "1.33.0", - "nanostores": "1.4.1", + "nanostores": "1.4.2", "postcss": "8.5.25", "postcss-load-config": "6.0.1", "postcss-mixins": "12.1.2", From beba3a9098aef1a797c935e001f53f43209850f3 Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Thu, 30 Jul 2026 15:34:01 +0000 Subject: [PATCH 02/28] Fix server start in one command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f7002c7..f316f4fd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "FORCE_COLOR=1 pnpm run -r --include-workspace-root /^test:/", "quick": "nano-staged --unstaged --quiet --bail", "offline": "FORCE_COLOR=1 pnpm run -r --include-workspace-root '/^test:(?!online\\b)/'", - "start": "FORCE_COLOR=1 pnpm run -F server -F web start", + "start": "FORCE_COLOR=1 pnpm run --parallel -F server -F web start", "format": "pnpm run -r --include-workspace-root /^format:/", "clean": "pnpm run -r --include-workspace-root /^clean:/", "build": "pnpm run -r build", From f299c7a1d7108e9debdd2a293e9f85b3afc3de86 Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Thu, 30 Jul 2026 16:23:35 +0000 Subject: [PATCH 03/28] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54ca22b7..1833fc03 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Each project has its own tools, too. - `pnpm test`: run all tests. - `pnpm quick`: run quick checks for changed files. - `pnpm offline`: run all tests expect which need Internet. It is useful for developing in airplane/train. -- `pnpm start`: run proxy and web client development server. +- `pnpm start`: run server and web client development server. - `pnpm format`: fix code style in all files. - `pnpm clean`: remove all temporary files. - `pnpm check-opml`: test loaders with user’s OPML RSS export. From 24e742a1131d7401210543640bfe1547db91a188 Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Thu, 30 Jul 2026 18:14:11 +0000 Subject: [PATCH 04/28] Add browser MCP for LLM debugging in the browser --- .devcontainer/Dockerfile | 3 +++ .devcontainer/devcontainer.json | 3 +++ .mcp.json | 44 +++++++++++++++++++++++++++++++++ AGENTS.md | 4 +++ docs/browser.md | 41 ++++++++++++++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 39 +++++++++++++++++++++++++++++ 7 files changed, 135 insertions(+) create mode 100644 .mcp.json create mode 100644 docs/browser.md diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3ac0be25..81af786d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -15,6 +15,9 @@ ENV PNPM_VERSION=11.18.0 \ PNPM_CHECKSUM_ARM64=f9c09bb564421d9d39dfff60af41df88a4864af5637afdabe5f54d12853f8426 RUN dnf install -yq zsh micro git tig psmisc procps-ng awk libatomic wget \ + gtk3 nss nspr alsa-lib cups-libs mesa-libgbm dbus-libs libX11-xcb \ + libXcomposite libXdamage libXrandr libXcursor libXi libxkbcommon \ + google-noto-sans-fonts google-noto-emoji-fonts \ && dnf clean all \ && rm -rf /var/cache/dnf diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 570cb675..10b8790f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,6 +4,9 @@ }, "runArgs": [ "--net=bridge", + "--shm-size=1g", + "--security-opt", + "seccomp=unconfined", "-p", "2553:2553", "-p", diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..a918b86d --- /dev/null +++ b/.mcp.json @@ -0,0 +1,44 @@ +{ + "mcpServers": { + "chromium": { + "args": [ + "-c", + "./node_modules/.bin/playwright-mcp install-browser chromium 1>&2 && exec ./node_modules/.bin/playwright-mcp \"$@\"", + "playwright-mcp", + "--headless", + "--browser", + "chromium", + "--caps", + "devtools", + "--viewport-size", + "1280,800", + "--user-data-dir", + "/home/developer/.local/share/playwright-chromium", + "--output-dir", + "/home/developer/.local/share/playwright-output/chromium", + "--save-session" + ], + "command": "sh" + }, + "firefox": { + "args": [ + "-c", + "./node_modules/.bin/playwright-mcp install-browser firefox 1>&2 && exec ./node_modules/.bin/playwright-mcp \"$@\"", + "playwright-mcp", + "--headless", + "--browser", + "firefox", + "--caps", + "devtools", + "--viewport-size", + "1280,800", + "--user-data-dir", + "/home/developer/.local/share/playwright-firefox", + "--output-dir", + "/home/developer/.local/share/playwright-output/firefox", + "--save-session" + ], + "command": "sh" + } + } +} diff --git a/AGENTS.md b/AGENTS.md index 67831f7b..efee5139 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,3 +27,7 @@ In all interactions, plans, and commit messages, be extremely concise and sacrif ## Testing - Run specific test by `pnpm bnt path/to/test.test.ts -t 'test name'` + +## Browser + +- Use Playwright MCP for visual issues and client database bugs. See [docs/browser.md](./docs/browser.md). diff --git a/docs/browser.md b/docs/browser.md new file mode 100644 index 00000000..f3ffc4ba --- /dev/null +++ b/docs/browser.md @@ -0,0 +1,41 @@ +# How to Debug in Browser? + +Dev Container has Firefox and Chromium with [Playwright MCP](https://github.com/microsoft/playwright-mcp) server. LLM uses them to check visual issues and to debug client database. + +## Run Application + +```sh +pnpm start +``` + +Web client will be on `http://localhost:2553`. + +Storybook is a separate process: + +```sh +pnpm -F web visual +``` + +## Open Story + +Open story without Storybook UI to keep screenshot clean: + +```text +http://localhost:2555/iframe.html?id=STORY_ID&globals=theme:light +``` + +Story IDs are in `http://localhost:2555/index.json`. Themes are `theme:light` and `theme:dark`. + +## Record Steps + +`--caps devtools` adds `browser_start_tracing`, `browser_stop_tracing`, and video tools. Record long scenario to get DOM snapshots, console, and network for every step. + +## Reset State + +Browser profile keeps cookies, `localStorage`, and IndexedDB between sessions: + +```sh +rm -rf ~/.local/share/playwright-chromium ~/.local/share/playwright-firefox +``` + +Screenshots and session logs are in `~/.local/share/playwright-output`. diff --git a/package.json b/package.json index f316f4fd..5129dde6 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@logux/eslint-config": "57.1.0", "@logux/oxc-configs": "1.0.0", + "@playwright/mcp": "0.0.78", "@types/chrome": "0.2.2", "@types/node": "26.1.2", "@types/ws": "8.18.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 399568d7..c9bc914b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: '@logux/oxc-configs': specifier: 1.0.0 version: 1.0.0(oxlint-tsgolint@7.0.2001)(oxlint@1.76.0)(typescript@6.0.3) + '@playwright/mcp': + specifier: 0.0.78 + version: 0.0.78 '@types/chrome': specifier: 0.2.2 version: 0.2.2 @@ -1795,6 +1798,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/mcp@0.0.78': + resolution: {integrity: sha512-XLTUeA6mEN9sQ+hJ4dfG8EIkDbxS0K3Trc2RBkUJuf02TgE2FQRNTMtq/aJfhyRMINsRl/Ybc4sxcWLtFn4/TQ==} + engines: {node: '>=18'} + hasBin: true + '@profoundlogic/hogan@3.0.4': resolution: {integrity: sha512-pmNVGuooS30Mm7YbZd5T7E5zYVO6D5Ct91sn4T39mUvMUc3sCGridcnhAufL1/Bz2QzAtzEn0agNrdk3+5yWzw==} hasBin: true @@ -3451,6 +3459,11 @@ packages: resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} engines: {node: '>=14.14'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4385,6 +4398,16 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + playwright-core@1.62.0-alpha-1783623505000: + resolution: {integrity: sha512-CPJZdsA/KGT2QQlekiV6Wt+QlQrZHVSZ6oiNtOI/bYYOIVLM8jfKGWTM4zQiyd4UN+40Cq4cA6lxmZHZbtPvJQ==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.0-alpha-1783623505000: + resolution: {integrity: sha512-6KV9h4PP3hqu4NaGdxxcijWfYh9LJcFI/R2sP4TTC4I5cFo3oRawN0ETlW5MkE3cQEgKhhoj0KUNz4sfpCT0Tg==} + engines: {node: '>=20'} + hasBin: true + postcss-html@1.8.1: resolution: {integrity: sha512-OLF6P7qctfAWayOhLpcVnTGqVeJzu2W3WpIYelfz2+JV5oGxfkcEvweN9U4XpeqE0P98dcD9ssusGwlF0TK0uQ==} engines: {node: ^12 || >=14} @@ -6351,6 +6374,11 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/mcp@0.0.78': + dependencies: + playwright: 1.62.0-alpha-1783623505000 + playwright-core: 1.62.0-alpha-1783623505000 + '@profoundlogic/hogan@3.0.4': dependencies: nopt: 1.0.10 @@ -7802,6 +7830,9 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -8881,6 +8912,14 @@ snapshots: picomatch@4.0.5: {} + playwright-core@1.62.0-alpha-1783623505000: {} + + playwright@1.62.0-alpha-1783623505000: + dependencies: + playwright-core: 1.62.0-alpha-1783623505000 + optionalDependencies: + fsevents: 2.3.2 + postcss-html@1.8.1: dependencies: htmlparser2: 8.0.2 From 3d797ddc91e54f75530e847c2c6a4b472c29fc3f Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Sun, 2 Aug 2026 18:58:21 +0000 Subject: [PATCH 05/28] Move ignore comments from ESLint to oxlint --- api/http/utils.ts | 3 +-- core/current-page.ts | 2 +- core/lib/stores.ts | 2 +- core/readers/feed.ts | 2 -- proxy/index.ts | 2 +- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/api/http/utils.ts b/api/http/utils.ts index b96c51f3..64750a6e 100644 --- a/api/http/utils.ts +++ b/api/http/utils.ts @@ -76,8 +76,7 @@ export function createRequester( } export interface Endpoint< - // Need to put it inside type to pass all types to server in a single variable - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // oxlint-disable-next-line no-unused-vars Response, Request, UrlParams extends Record = Record diff --git a/core/current-page.ts b/core/current-page.ts index 6b846f9e..c068039b 100644 --- a/core/current-page.ts +++ b/core/current-page.ts @@ -40,7 +40,7 @@ function getPageParams( for (let i in page.params) { let name = i as keyof SomeRoute['params'] // @ts-expect-error Too complex types for TS - // eslint-disable-next-line + // oxlint-disable-next-line params[name] = page.params[name].get() } return params diff --git a/core/lib/stores.ts b/core/lib/stores.ts index 84a5cdac..203a3755 100644 --- a/core/lib/stores.ts +++ b/core/lib/stores.ts @@ -21,7 +21,7 @@ export function increaseKey( store: Store, key: NumberKeys> ): void { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + // oxlint-disable-next-line typescript/no-unsafe-member-access let value = store.get()[key] as number store.setKey(key, value + 1) } diff --git a/core/readers/feed.ts b/core/readers/feed.ts index 7eef3adb..11fe1c40 100644 --- a/core/readers/feed.ts +++ b/core/readers/feed.ts @@ -47,8 +47,6 @@ export const feedReader = createReader('feed', (filter, params, helpers) => { ) ) ) - // It could be switched to false during await above - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (exited) return } diff --git a/proxy/index.ts b/proxy/index.ts index a66da0e0..3fed3718 100644 --- a/proxy/index.ts +++ b/proxy/index.ts @@ -229,7 +229,7 @@ export function createProxy( if (targetResponse.body) { let nodeStream = Readable.fromWeb( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + // oxlint-disable-next-line typescript/no-unnecessary-type-assertion targetResponse.body as WebReadableStream ) await pipeline(nodeStream, res, { From 18930ef18754eba7bb0eec61336732c90f57fc44 Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Sun, 2 Aug 2026 19:18:21 +0000 Subject: [PATCH 06/28] Remove oxlint ignore by updating config --- api/http/utils.ts | 3 +-- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/api/http/utils.ts b/api/http/utils.ts index 64750a6e..6c4b55b7 100644 --- a/api/http/utils.ts +++ b/api/http/utils.ts @@ -76,8 +76,7 @@ export function createRequester( } export interface Endpoint< - // oxlint-disable-next-line no-unused-vars - Response, + _Response, Request, UrlParams extends Record = Record > { diff --git a/package.json b/package.json index 5129dde6..2daa8561 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ }, "devDependencies": { "@logux/eslint-config": "57.1.0", - "@logux/oxc-configs": "1.0.0", + "@logux/oxc-configs": "1.1.0", "@playwright/mcp": "0.0.78", "@types/chrome": "0.2.2", "@types/node": "26.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9bc914b..8757340f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: specifier: 57.1.0 version: 57.1.0(@typescript-eslint/utils@8.65.0)(eslint-plugin-svelte@3.22.0)(eslint@10.8.0)(supports-color@10.2.2)(svelte-eslint-parser@1.8.0)(svelte@5.56.8)(typescript@6.0.3) '@logux/oxc-configs': - specifier: 1.0.0 - version: 1.0.0(oxlint-tsgolint@7.0.2001)(oxlint@1.76.0)(typescript@6.0.3) + specifier: 1.1.0 + version: 1.1.0(oxlint-tsgolint@7.0.2001)(oxlint@1.76.0)(typescript@6.0.3) '@playwright/mcp': specifier: 0.0.78 version: 0.0.78 @@ -1188,8 +1188,8 @@ packages: svelte-eslint-parser: optional: true - '@logux/oxc-configs@1.0.0': - resolution: {integrity: sha512-BHn8L2r2HT/Kp508jiTX+gTC3Rij2Sua7rjLOjweNzkWkQjzAbJdXwtRYI4DeBkrvdF7PmI8A6EvcCqHtDFNEQ==} + '@logux/oxc-configs@1.1.0': + resolution: {integrity: sha512-X+yJcCwTpwnaESokhw2zjH2Wn+XAUGX3VsEToYBQzXfgdXQ4IMCyjTUURKhHO+9JoSL+vGgxtqaF7LMGtPHj5w==} engines: {node: '>=22.0.0'} peerDependencies: oxlint: ^1.57.0 @@ -5955,7 +5955,7 @@ snapshots: - supports-color - typescript - '@logux/oxc-configs@1.0.0(oxlint-tsgolint@7.0.2001)(oxlint@1.76.0)(typescript@6.0.3)': + '@logux/oxc-configs@1.1.0(oxlint-tsgolint@7.0.2001)(oxlint@1.76.0)(typescript@6.0.3)': dependencies: eslint-plugin-prefer-let: 4.2.2 oxlint: 1.76.0(oxlint-tsgolint@7.0.2001) From add360843e4309b749270297b61772ea8ffe3522 Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Sun, 2 Aug 2026 20:47:54 +0000 Subject: [PATCH 07/28] Fix menu loading --- core/menu.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/menu.ts b/core/menu.ts index 0b66bdcf..37399e0e 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -1,4 +1,4 @@ -import { ensureLoaded, loadValue } from '@logux/client' +import { loadValue } from '@logux/client' import { persistentAtom } from '@nanostores/persistent' import { atom, computed, keepMount } from 'nanostores' @@ -94,13 +94,15 @@ async function rebuild(): Promise { let slowPosts = posts.list.filter(i => i.reading === 'slow' && !i.read) let fastFeeds = feeds.list.filter(i => i.reading === 'fast') - let feedsWithFastFilters = fastFilters.list.map(i => { - return feeds.stores.get(i.feedId)!.get() - }) + let feedsWithFastFilters = fastFilters.list + .map(i => feeds.stores.get(i.feedId)?.get()) + .filter(i => !!i) let uniqueFastCategories: Record = {} for (let feed of [...fastFeeds, ...feedsWithFastFilters]) { - let id = ensureLoaded(feed).categoryId + /* node:coverage ignore next */ + if (feed.isLoading) continue + let id = feed.categoryId if (!uniqueFastCategories[id]) { if (id === 'general') { uniqueFastCategories[id] = getGeneralCategory() @@ -131,7 +133,9 @@ async function rebuild(): Promise { for (let [feedId, unread] of Object.entries(unreadByFeed)) { let feedStore = feeds.stores.get(feedId) if (!feedStore) continue - let feed = ensureLoaded(feedStore.get()) + let feed = feedStore.get() + /* node:coverage ignore next */ + if (feed.isLoading) continue let category = feed.categoryId if (category === 'general') { general = true From 84322f88625cc7b2103fe3a9ac50c31422059944 Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Sun, 2 Aug 2026 21:06:47 +0000 Subject: [PATCH 08/28] Tune LLM instructions according to real user experience --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index efee5139..77379148 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,8 @@ In all interactions, plans, and commit messages, be extremely concise and sacrif ## Architecture - Avoid adding dependencies. +- Keep non-browser logic in `core/`. Client folders should have only environment-specific code. +- Update `README.md` of the package on every new script, folder, or tool. ## LLMS From 05caba2911a4f6cc6f3a29066eb62a34e1e37fd5 Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Sun, 2 Aug 2026 21:15:59 +0000 Subject: [PATCH 09/28] Add ability to add label for app is busy loader --- core/busy.ts | 43 +++++++++++++++++------ core/test/busy.test.ts | 30 +++++++++++++--- core/test/menu.test.ts | 2 +- web/pages/busy.svelte | 12 ++++--- web/stories/pages/busy.stories.svelte | 50 ++++++++++++++++++++++++++- web/ui/captioned.svelte | 50 +++++++++++++++++++++++++++ 6 files changed, 167 insertions(+), 20 deletions(-) create mode 100644 web/ui/captioned.svelte diff --git a/core/busy.ts b/core/busy.ts index 2fae237e..5a99310e 100644 --- a/core/busy.ts +++ b/core/busy.ts @@ -1,20 +1,43 @@ -import { atom, computed } from 'nanostores' +import { atom } from 'nanostores' -let $tasks = atom(0) +export interface BusyValue { + label?: string + progress?: number +} + +let tasks: BusyValue[] = [] /** * Show loader over whole app until task is running. */ -export async function busyDuring(cb: () => Promise): Promise { - $tasks.set($tasks.get() + 1) - try { - await cb() - } finally { - $tasks.set($tasks.get() - 1) +export const busy = atom(false) + +function update(): void { + if (tasks.length === 0) { + busy.set(false) + } else { + let last = tasks.findLast(i => i.label || i.progress !== undefined) + busy.set({ label: last?.label, progress: last?.progress }) } } /** - * Do we need to show loader over whole app. + * Show loader over whole app until task is running. */ -export const busy = computed($tasks, tasks => tasks > 0) +export async function busyDuring( + cb: (setProgress: (progress: number) => void) => Promise, + label?: string +): Promise { + let task: BusyValue = { label } + tasks.push(task) + update() + try { + return await cb(progress => { + task.progress = progress + update() + }) + } finally { + tasks = tasks.filter(i => i !== task) + update() + } +} diff --git a/core/test/busy.test.ts b/core/test/busy.test.ts index 768879b7..2d5ccef3 100644 --- a/core/test/busy.test.ts +++ b/core/test/busy.test.ts @@ -1,4 +1,4 @@ -import { equal } from 'node:assert/strict' +import { deepStrictEqual, equal } from 'node:assert/strict' import { afterEach, describe, test } from 'node:test' import { setTimeout } from 'node:timers/promises' @@ -13,13 +13,35 @@ describe('busy', () => { test('allows to manually set busy state', async () => { equal(busy.get(), false) await busyDuring(async () => { - equal(busy.get(), true) + deepStrictEqual(busy.get(), { label: undefined, progress: undefined }) await busyDuring(async () => { - equal(busy.get(), true) await setTimeout(10) }) - equal(busy.get(), true) + deepStrictEqual(busy.get(), { label: undefined, progress: undefined }) }) equal(busy.get(), false) }) + + test('shows label and progress of the latest task', async () => { + let result = await busyDuring(async setProgress => { + deepStrictEqual(busy.get(), { label: 'Loading', progress: undefined }) + setProgress(0.5) + deepStrictEqual(busy.get(), { label: 'Loading', progress: 0.5 }) + + await busyDuring(async setNested => { + deepStrictEqual(busy.get(), { label: 'Loading', progress: 0.5 }) + setNested(0.1) + deepStrictEqual(busy.get(), { label: undefined, progress: 0.1 }) + await busyDuring(async () => { + deepStrictEqual(busy.get(), { label: 'Saving', progress: undefined }) + await setTimeout(10) + }, 'Saving') + }) + + deepStrictEqual(busy.get(), { label: 'Loading', progress: 0.5 }) + return 'done' + }, 'Loading') + equal(result, 'done') + equal(busy.get(), false) + }) }) diff --git a/core/test/menu.test.ts b/core/test/menu.test.ts index 4651feb9..9f4d878c 100644 --- a/core/test/menu.test.ts +++ b/core/test/menu.test.ts @@ -352,7 +352,7 @@ describe('menu', () => { test('has helper to block app while menu is loading', async () => { busyUntilMenuLoader() - equal(busy.get(), true) + deepEqual(busy.get(), { label: undefined, progress: undefined }) await setTimeout(10) equal(busy.get(), false) equal(menuLoading.get(), false) diff --git a/web/pages/busy.svelte b/web/pages/busy.svelte index 1b1374c7..405d3809 100644 --- a/web/pages/busy.svelte +++ b/web/pages/busy.svelte @@ -1,8 +1,12 @@ - - - + + + diff --git a/web/stories/pages/busy.stories.svelte b/web/stories/pages/busy.stories.svelte index f494a2b3..ca8fc52c 100644 --- a/web/stories/pages/busy.stories.svelte +++ b/web/stories/pages/busy.stories.svelte @@ -1,7 +1,9 @@ - + + { + busy.set({}) + }} + > + + + + + + { + busy.set({ label: 'Creating database' }) + }} + > + + + + + + { + busy.set({ + label: 'Creating benchmark data', + progress: 0.72 + }) + }} + > + + + + + + { + busy.set({ + label: + 'Creating benchmark data with a very long description, ' + + 'which should be wrapped to a few lines on any screen', + progress: 0.72 + }) + }} + > + + + diff --git a/web/ui/captioned.svelte b/web/ui/captioned.svelte new file mode 100644 index 00000000..8e4b0e53 --- /dev/null +++ b/web/ui/captioned.svelte @@ -0,0 +1,50 @@ + + +
+
+ {@render children()} +
+ {#if caption} +

+ {caption} +

+ {/if} +
+ + From 595914c53ae1f20dc6ebf3205bc287657499f3c7 Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Mon, 3 Aug 2026 14:23:32 +0000 Subject: [PATCH 10/28] Move progress render to root to later use in benchmark --- loader-tests/check-opml.ts | 6 +- loader-tests/test-loaders.ts | 5 +- loader-tests/tsconfig.json | 2 +- loader-tests/utils.ts | 125 +++----------------------------- scripts/progress.ts | 135 +++++++++++++++++++++++++++++++++++ 5 files changed, 151 insertions(+), 122 deletions(-) create mode 100644 scripts/progress.ts diff --git a/loader-tests/check-opml.ts b/loader-tests/check-opml.ts index 8bbe7c06..7c3651fb 100644 --- a/loader-tests/check-opml.ts +++ b/loader-tests/check-opml.ts @@ -7,13 +7,11 @@ import { import { createProxy, DEFAULT_PROXY_CONFIG } from '@slowreader/proxy' import { createServer } from 'node:http' +import { error, finish, startProgress } from '../scripts/progress.ts' import { createCLI, enableTestClient, - error, fetchAndParsePosts, - finish, - initializeProgressBar, readText } from './utils.ts' @@ -83,7 +81,7 @@ await cli.run(async args => { } let unbindTotal = page.total.listen(all => { - initializeProgressBar(all) + startProgress(all) unbindTotal() }) let unbindLastAdded = page.lastAdded.listen(async url => { diff --git a/loader-tests/test-loaders.ts b/loader-tests/test-loaders.ts index 95602639..6f739eab 100644 --- a/loader-tests/test-loaders.ts +++ b/loader-tests/test-loaders.ts @@ -1,14 +1,13 @@ import { join } from 'node:path' import yaml from 'yaml' +import { finish, startProgress } from '../scripts/progress.ts' import { completeTasks, createCLI, enableTestClient, fetchAndParsePosts, findRSSFromHome, - finish, - initializeProgressBar, type LoaderTestFeed, readText } from './utils.ts' @@ -30,7 +29,7 @@ await cli.run(async () => { enableTestClient() let feeds = await parseFeedsFromFile(FEEDS) - initializeProgressBar( + startProgress( feeds.length + feeds.filter(feed => feed.findFromHome !== false).length ) diff --git a/loader-tests/tsconfig.json b/loader-tests/tsconfig.json index 6858cda8..0b5bd10a 100644 --- a/loader-tests/tsconfig.json +++ b/loader-tests/tsconfig.json @@ -4,5 +4,5 @@ "composite": true, "types": ["node", "trusted-types"] }, - "include": ["**/*.ts"] + "include": ["../scripts/progress.ts", "**/*.ts"] } diff --git a/loader-tests/utils.ts b/loader-tests/utils.ts index 2fd47dcd..8502cb53 100644 --- a/loader-tests/utils.ts +++ b/loader-tests/utils.ts @@ -17,9 +17,14 @@ import { import { getTestEnvironment, setBaseTestRoute } from '@slowreader/core/test' import { readFile } from 'node:fs/promises' import { isAbsolute, join } from 'node:path' -import readline from 'node:readline' -import { isatty } from 'node:tty' -import { styleText } from 'node:util' + +import { + error, + printAboveProgress, + semiSuccess, + success, + warning +} from '../scripts/progress.ts' export interface LoaderTestFeed { homeUrl?: string @@ -67,114 +72,6 @@ export function timeout( ]) } -interface NoFileError extends Error { - code: string - path: string -} - -function isNoFileError(e: unknown): e is NoFileError { - return e instanceof Error && `code` in e && e.code === 'ENOENT' -} - -let progress = 0 -let totalJobs = 0 - -export function initializeProgressBar(totalValue: number): void { - if (!process.env.CI) { - totalJobs = totalValue - renderProgressBar() - } -} - -function renderProgressBar(): void { - let filled = Math.floor((process.stderr.columns * progress) / totalJobs) - process.stderr.write( - '█'.repeat(filled) + '░'.repeat(process.stderr.columns - filled) + '\n' - ) - readline.moveCursor(process.stderr, 0, 0) -} - -const SIMPLE_SHELL = !isatty(1) || process.env.CI - -function updateProgressBar(): void { - if (totalJobs > 0 && progress < totalJobs && !SIMPLE_SHELL) { - progress += 1 - readline.moveCursor(process.stderr, 0, -1) - readline.clearLine(process.stderr, 0) - if (progress < totalJobs) { - renderProgressBar() - } - } -} - -export function print(msg: string): void { - if (totalJobs > 0 && progress < totalJobs && !SIMPLE_SHELL) { - readline.moveCursor(process.stderr, 0, -1) - readline.clearLine(process.stderr, 0) - process.stderr.write(`${msg}\n`) - renderProgressBar() - } else { - process.stderr.write(`${msg}\n`) - } -} - -let errors = 0 - -export function warning(text: string): void { - print(styleText('yellow', text)) - updateProgressBar() -} - -export function error(err: unknown, details?: string): void { - errors += 1 - let msg: string - if (isNoFileError(err)) { - msg = `File not found: ${err.path}` - } else if (err instanceof Error) { - msg = err.stack ?? err.message - } else { - msg = String(err) - } - print('') - print( - styleText('bold', styleText('bgRed', ' ERROR ')) + - ' ' + - styleText('bold', styleText('red', msg)) - ) - if (details) print(details) - print('') - updateProgressBar() -} - -export function finish(msg: string): void { - print('') - let postfix = '' - if (errors > 0) { - postfix = - ', ' + styleText('red', styleText('bold', `${errors} errors found`)) - } - print(styleText('gray', msg + postfix)) - process.exit(errors > 0 ? 1 : 0) -} - -export function success(msg: string, details?: string): void { - if (details) { - msg += ` ${styleText('gray', details)}` - } - print(styleText('green', styleText('bold', '✓ ') + msg)) - updateProgressBar() -} - -export function semiSuccess(msg: string, note: string): void { - print( - styleText( - 'yellow', - styleText('bold', '✓ ') + msg + ' ' + styleText('bold', note) - ) - ) - updateProgressBar() -} - export async function fetchAndParsePosts( url: string, badSource = false @@ -337,8 +234,8 @@ export function createCLI(help: string, usage?: string): CLI { args.includes('-h') || args.includes('help') ) { - print(help) - if (usage) print('Usage:\n' + usage) + printAboveProgress(help) + if (usage) printAboveProgress('Usage:\n' + usage) process.exit(0) } else { try { @@ -351,7 +248,7 @@ export function createCLI(help: string, usage?: string): CLI { }, wrongArg(message) { error(message) - if (usage) print('Usage:\n' + usage) + if (usage) printAboveProgress('Usage:\n' + usage) process.exit(1) } } diff --git a/scripts/progress.ts b/scripts/progress.ts new file mode 100644 index 00000000..7f467e08 --- /dev/null +++ b/scripts/progress.ts @@ -0,0 +1,135 @@ +// Progress bar for long scripts. It is rendered in stderr to keep stdout +// for the script’s result. + +import readline from 'node:readline' +import { isatty } from 'node:tty' +import { styleText } from 'node:util' + +const COLORS = styleText('bgGreen', ' ', { stream: process.stderr }) !== ' ' + +let total = 0 +let current = 0 +let enabled = false + +function render(): void { + let width = process.stderr.columns + let filled = Math.floor((width * current) / total) + let rest = width - filled + let bar: string + if (COLORS) { + bar = + styleText('bgGreen', ' '.repeat(filled), { stream: process.stderr }) + + styleText('bgWhite', ' '.repeat(rest), { stream: process.stderr }) + } else { + bar = '█'.repeat(filled) + '▒'.repeat(rest) + } + process.stderr.write(`${bar}\n`) + readline.moveCursor(process.stderr, 0, 0) +} + +function rendered(): boolean { + return enabled && total > 0 && current < total +} + +export function startProgress(steps: number): void { + enabled = isatty(2) && !process.env.CI + if (!enabled) return + total = steps + current = 0 + render() +} + +export function nextProgress(): void { + if (!rendered()) return + current += 1 + readline.moveCursor(process.stderr, 0, -1) + readline.clearLine(process.stderr, 0) + if (current < total) render() +} + +export function printAboveProgress( + message: string, + mode: 'error' | 'status' | 'text' | 'warning' = 'text' +): void { + let text = message + if (mode === 'error') { + text = styleText('red', message, { stream: process.stderr }) + } else if (mode === 'warning') { + text = styleText('yellow', message, { stream: process.stderr }) + } else if (mode === 'status') { + text = styleText('gray', message, { stream: process.stderr }) + } + if (rendered()) { + readline.moveCursor(process.stderr, 0, -1) + readline.clearLine(process.stderr, 0) + process.stderr.write(`${text}\n`) + render() + } else { + process.stderr.write(`${text}\n`) + } +} + +interface NoFileError extends Error { + code: string + path: string +} + +function isNoFileError(e: unknown): e is NoFileError { + return e instanceof Error && `code` in e && e.code === 'ENOENT' +} + +let errors = 0 + +export function warning(text: string): void { + printAboveProgress(text, 'warning') + nextProgress() +} + +export function error(err: unknown, details?: string): void { + errors += 1 + let msg: string + if (isNoFileError(err)) { + msg = `File not found: ${err.path}` + } else if (err instanceof Error) { + msg = err.stack ?? err.message + } else { + msg = String(err) + } + printAboveProgress('') + printAboveProgress( + styleText('bold', styleText('bgRed', ' ERROR ')) + + ' ' + + styleText('bold', styleText('red', msg)) + ) + if (details) printAboveProgress(details) + printAboveProgress('') + nextProgress() +} + +export function success(msg: string, details?: string): void { + let text = msg + if (details) { + text += ` ${styleText('gray', details)}` + } + printAboveProgress(styleText('green', styleText('bold', '✓ ') + text)) + nextProgress() +} + +export function semiSuccess(msg: string, note: string): void { + printAboveProgress( + styleText('bold', '✓ ') + msg + ' ' + styleText('bold', note), + 'warning' + ) + nextProgress() +} + +export function finish(msg: string): void { + printAboveProgress('') + let postfix = '' + if (errors > 0) { + postfix = + ', ' + styleText('red', styleText('bold', `${errors} errors found`)) + } + printAboveProgress(msg + postfix, 'status') + process.exit(errors > 0 ? 1 : 0) +} From 4185d182f9149aef7a1afb29826421822463e0dc Mon Sep 17 00:00:00 2001 From: Andrey Sitnik Date: Mon, 3 Aug 2026 14:25:10 +0000 Subject: [PATCH 11/28] Add API to track loaders to use later in benchmarks --- core/loader.ts | 23 +++++++++++++++++++++++ core/test/loader.test.ts | 29 +++++++++++++++++++++++++++++ web/.size-limit.json | 2 +- web/pages/add.svelte | 2 +- web/pages/busy.svelte | 6 +++++- web/pages/feeds/index.svelte | 2 +- web/pages/import.svelte | 2 +- web/popups/feed.svelte | 2 +- web/popups/loading.svelte | 2 +- web/popups/refresh.svelte | 1 + web/ui/button.svelte | 1 + web/ui/loader.svelte | 5 ++++- web/ui/navbar/progress.svelte | 3 ++- 13 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 core/loader.ts create mode 100644 core/test/loader.test.ts diff --git a/core/loader.ts b/core/loader.ts new file mode 100644 index 00000000..d5da50c0 --- /dev/null +++ b/core/loader.ts @@ -0,0 +1,23 @@ +// Loaders report themselves to the benchmark. Reporter is empty by default +// and `benchmark.ts` sets the real one, so clients will not download +// benchmark tools. + +export interface LoaderReporter { + (name: string): () => void +} + +let reporter: LoaderReporter = () => { + return () => {} +} + +export function setLoaderReporter(cb: LoaderReporter): void { + reporter = cb +} + +/** + * Tell benchmark that the loader is rendered. Returns function to call + * when the loader was removed from the screen. + */ +export function reportLoader(name: string): () => void { + return reporter(name) +} diff --git a/core/test/loader.test.ts b/core/test/loader.test.ts new file mode 100644 index 00000000..7dc00cf4 --- /dev/null +++ b/core/test/loader.test.ts @@ -0,0 +1,29 @@ +import { deepStrictEqual } from 'node:assert/strict' +import { describe, test } from 'node:test' + +import { reportLoader, setLoaderReporter } from '../index.ts' + +describe('loader', () => { + test('does nothing without reporter', () => { + reportLoader('test')() + }) + + test('reports loaders to the reporter', () => { + let calls: string[] = [] + setLoaderReporter(name => { + calls.push(`show ${name}`) + return () => { + calls.push(`hide ${name}`) + } + }) + + let hide = reportLoader('posts') + deepStrictEqual(calls, ['show posts']) + hide() + deepStrictEqual(calls, ['show posts', 'hide posts']) + + setLoaderReporter(() => { + return () => {} + }) + }) +}) diff --git a/web/.size-limit.json b/web/.size-limit.json index cb0bef4e..903f9052 100644 --- a/web/.size-limit.json +++ b/web/.size-limit.json @@ -22,6 +22,6 @@ "name": "Core scripts to execute", "path": "dist/assets/index-*.js", "brotli": false, - "limit": "320 KB" + "limit": "325 KB" } ] diff --git a/web/pages/add.svelte b/web/pages/add.svelte index 4cbdccb7..6cfd16f2 100644 --- a/web/pages/add.svelte +++ b/web/pages/add.svelte @@ -48,7 +48,7 @@ {:else if $searching} - + {:else if $error} {$t[$error]} {:else if $noResults} diff --git a/web/pages/busy.svelte b/web/pages/busy.svelte index 405d3809..fe8b6d28 100644 --- a/web/pages/busy.svelte +++ b/web/pages/busy.svelte @@ -8,5 +8,9 @@ - + diff --git a/web/pages/feeds/index.svelte b/web/pages/feeds/index.svelte index e0f26b71..dd00cbc0 100644 --- a/web/pages/feeds/index.svelte +++ b/web/pages/feeds/index.svelte @@ -50,7 +50,7 @@ {#if $postsLoading || !$posts} - + {:else if $posts.name === 'empty'} diff --git a/web/pages/import.svelte b/web/pages/import.svelte index e7333616..b6e34545 100644 --- a/web/pages/import.svelte +++ b/web/pages/import.svelte @@ -43,7 +43,7 @@ {#if $importing} - + {:else} {#if $posts.isLoading} - + {:else} {#if $posts.error} diff --git a/web/popups/loading.svelte b/web/popups/loading.svelte index 65d3aeff..4fd42746 100644 --- a/web/popups/loading.svelte +++ b/web/popups/loading.svelte @@ -10,6 +10,6 @@ - + diff --git a/web/popups/refresh.svelte b/web/popups/refresh.svelte index 4534cff0..2b80f38a 100644 --- a/web/popups/refresh.svelte +++ b/web/popups/refresh.svelte @@ -61,6 +61,7 @@ diff --git a/web/ui/button.svelte b/web/ui/button.svelte index ba700c2e..966c2562 100644 --- a/web/ui/button.svelte +++ b/web/ui/button.svelte @@ -84,6 +84,7 @@ {#if loader} diff --git a/web/ui/loader.svelte b/web/ui/loader.svelte index be920c71..f0683b79 100644 --- a/web/ui/loader.svelte +++ b/web/ui/loader.svelte @@ -1,14 +1,16 @@
    - {#each posts as post (post.get().id)} - + {#each posts as post (post.id)} + {/each}
diff --git a/web/ui/feed/post.svelte b/web/ui/feed/post.svelte index 1447ab6d..ba371421 100644 --- a/web/ui/feed/post.svelte +++ b/web/ui/feed/post.svelte @@ -1,5 +1,4 @@
  • -
    +
    {#if author} {/if} - {#if $post.title} + {#if post.title}

    - +

    {/if} - - {#each parseMedia($post.media) as media, index (`${media.url}${index}`)} + + {#each parseMedia(post.media) as media, index (`${media.url}${index}`)} {#if !media.fromText && media.type.startsWith('image')} {/if} {/each}