Compare commits

...

48 Commits

Author SHA1 Message Date
renovate[bot]
1240f461a0 chore: bump up capacitor monorepo to v8 2026-03-14 10:14:52 +00:00
sahilkhan09k
8f571ddc30 fix: ensure images load correctly when printing to PDF (#14618)
Fixes #14304

## Summary
This PR resolves an issue where images sometimes fail to appear when
exporting or printing AFFiNE pages to PDF. The issue occurs because
images may not finish loading inside the hidden print iframe before
`window.print()` is triggered.

## Changes
- Avoid using `display: none` for the print iframe and instead keep it
hidden while remaining in the rendering tree to ensure resources load
correctly.
- Remove `loading="lazy"` from all images before printing to prevent
viewport-based lazy loading from blocking image fetches.
- Force image reload by reassigning the `src` attribute after removing
lazy loading.
- Add a `waitForImages` helper to ensure all images (including those
inside Shadow DOM) finish loading before calling `window.print()`.
- Improve reliability by checking both `img.complete` and
`img.naturalWidth` to confirm successful image loading.
- Wait for fonts using `document.fonts.ready` before triggering the
print dialog.

## Verification
1. Run AFFiNE in development mode: npm run dev
2. Open a page containing multiple images.
3. Click **Print** and select **Save as PDF** (or any PDF printer).
4. Verify that all images appear correctly in the generated PDF.

## Notes
This change focuses only on improving the reliability of the existing
print-to-PDF workflow without altering any feature flags or export
behavior.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved PDF export reliability by waiting for all images (including
inside shadow content) and fonts to load before printing.
* Removed lazy-loading interference so images reliably appear in
exports.
* Ensured styles and light-theme attributes are consistently applied to
the print document.

* **Improvements**
* More robust print preparation using a hidden-but-rendering iframe
document, deep-cloning content (flattening shadow DOM), and preserved
canvas mapping for accurate renders.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-13 09:57:07 +08:00
Mohad
13ad1beb10 feat(i18n): automatic RTL layout for Arabic, Persian, and Urdu + complete Arabic translations (#14624)
## Changes

### RTL Support (automatic, locale-driven)
- Add `rtl?: boolean` metadata to locale definitions in
`SUPPORTED_LANGUAGES`
- Set `rtl: true` for Arabic (`ar`), Persian (`fa`), and Urdu (`ur`)
- Automatically set `document.documentElement.dir` based on locale RTL
metadata on language change
- Remove hardcoded `lang="en"` from HTML template — JS now controls both
`lang` and `dir`

### Arabic Translations
- Add 100 missing keys to `ar.json` (Calendar integration, Doc
Analytics, MCP Server, AI Chat, and more)
- Arabic locale now has 2,313/2,313 keys (100% coverage, matches
`en.json` exactly)

## Testing
Switching to Arabic/Persian/Urdu now automatically flips the entire UI
layout to RTL without any manual feature flag.

Fixes #7099

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Right-to-Left (RTL) support for Arabic, Persian, and Urdu with
automatic document direction and language attributes when a language is
selected.

* **Refactor**
* Centralized and reordered internal language handling so document
language and direction are applied earlier and consistently.

* **Chore**
  * Set a default text direction attribute on the base HTML template.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-12 23:14:19 +08:00
sahilkhan09k
9844ca4d54 fix(editor): ensure code and quote blocks render correctly in print (#14613)
Fixes #14608

## Summary

Fix printing issues when exporting documents while using the dark theme.

Previously, when printing or saving a document as PDF in dark mode, text
color was forced to black while some block containers (such as code
blocks and quotes) retained their dark backgrounds. This resulted in
**black text on dark backgrounds**, making the content unreadable in the
exported PDF.

## Changes

* Reset relevant CSS variables in the `@media print` section of
`print-to-pdf.ts`.
* Ensure block containers such as **code blocks and quotes** render with
light backgrounds during printing.
* Maintain readable text colors by forcing text color to black for print
output.

This approach updates the **CSS variables used by BlockSuite
components**, ensuring that elements relying on variables like
`--affine-background-code-block` and `--affine-quote-color` correctly
switch to light backgrounds in print mode.

## Result

Documents printed or exported as PDF from dark mode now render correctly
with:

* readable text
* proper light backgrounds for code blocks and quotes
* consistent formatting in print output


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Enhanced print-to-PDF styling for improved visual presentation of code
blocks, quotes, and borders when exporting or printing documents to
maintain better readability and consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: sahilkhan09k <sahilkhan392005@gmail.com>
2026-03-11 15:07:09 +08:00
renovate[bot]
d7d67841b8 chore: bump up file-type version to v21.3.1 [SECURITY] (#14625)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [file-type](https://redirect.github.com/sindresorhus/file-type) |
[`21.3.0` →
`21.3.1`](https://renovatebot.com/diffs/npm/file-type/21.3.0/21.3.1) |
![age](https://developer.mend.io/api/mc/badges/age/npm/file-type/21.3.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/file-type/21.3.0/21.3.1?slim=true)
|

### GitHub Vulnerability Alerts

####
[CVE-2026-31808](https://redirect.github.com/sindresorhus/file-type/security/advisories/GHSA-5v7r-6r5c-r473)

### Impact
A denial of service vulnerability exists in the ASF (WMV/WMA) file type
detection parser. When parsing a crafted input where an ASF sub-header
has a `size` field of zero, the parser enters an infinite loop. The
`payload` value becomes negative (-24), causing
`tokenizer.ignore(payload)` to move the read position backwards, so the
same sub-header is read repeatedly forever.

Any application that uses `file-type` to detect the type of
untrusted/attacker-controlled input is affected. An attacker can stall
the Node.js event loop with a 55-byte payload.

### Patches
Fixed in version 21.3.1. Users should upgrade to >= 21.3.1.

### Workarounds
Validate or limit the size of input buffers before passing them to
`file-type`, or run file type detection in a worker thread with a
timeout.

### References
- Fix commit: 319abf871b50ba2fa221b4a7050059f1ae096f4f

### Reporter

crnkovic@lokvica.com

---

### Release Notes

<details>
<summary>sindresorhus/file-type (file-type)</summary>

###
[`v21.3.1`](https://redirect.github.com/sindresorhus/file-type/releases/tag/v21.3.1)

[Compare
Source](https://redirect.github.com/sindresorhus/file-type/compare/v21.3.0...v21.3.1)

- Fix infinite loop in ASF parser on malformed input
[`319abf8`](https://redirect.github.com/sindresorhus/file-type/commit/319abf8)

***

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no
schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My41OS4wIiwidXBkYXRlZEluVmVyIjoiNDMuNTkuMCIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-11 13:58:31 +08:00
DarkSky
29a27b561b feat(server): migrate copilot to native (#14620)
#### PR Dependency Tree


* **PR #14620** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Native LLM workflows: structured outputs, embeddings, and reranking
plus richer multimodal attachments (images, audio, files) and improved
remote-attachment inlining.

* **Refactor**
* Tooling API unified behind a local tool-definition helper;
provider/adapters reorganized to route through native dispatch paths.

* **Chores**
* Dependency updates, removed legacy Google SDK integrations, and
increased front memory allocation.

* **Tests**
* Expanded end-to-end and streaming tests exercising native provider
flows, attachments, and rerank/structured scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-11 13:55:35 +08:00
renovate[bot]
02744cec00 chore: bump up apple/swift-collections version to from: "1.4.0" (#14616)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
|
[apple/swift-collections](https://redirect.github.com/apple/swift-collections)
| minor | `from: "1.3.0"` → `from: "1.4.0"` |

---

### Release Notes

<details>
<summary>apple/swift-collections (apple/swift-collections)</summary>

###
[`v1.4.0`](https://redirect.github.com/apple/swift-collections/releases/tag/1.4.0):
Swift Collections 1.4.0

[Compare
Source](https://redirect.github.com/apple/swift-collections/compare/1.3.0...1.4.0)

This feature release supports Swift toolchain versions 6.0, 6.1 and 6.2.
It includes a variety of bug fixes, and ships the following new
features:

##### New ownership-aware ring buffer and hashed container
implementations

In the `DequeModule` module, we have two new source-stable types that
provide ownership-aware ring buffer implementations:

- [`struct UniqueDeque<Element>`][UniqueDeque] is a uniquely held,
dynamically resizing, noncopyable deque.
- [`struct RigidDeque<Element>`][RigidDeque] is a fixed-capacity deque
implementation.

`RigidDeque`/`UniqueDeque` are to `Deque` like
`RigidArray`/`UniqueArray` are to `Array` -- they provide noncopyable
embodiments of the same basic data structure, with many of the same
operations.

[UniqueDeque]:
https://swiftpackageindex.com/apple/swift-collections/documentation/dequemodule/uniquedeque

[RigidDeque]:
https://swiftpackageindex.com/apple/swift-collections/documentation/dequemodule/rigiddeque

In the `BasicContainers` module, this release adds previews of four new
types, implementing ownership-aware hashed containers:

- [`struct UniqueSet<Element>`][UniqueSet] is a uniquely held,
dynamically resizing set.
- [`struct RigidSet<Element>`][RigidSet] is a fixed-capacity set.
- [`struct UniqueDictionary<Key, Value>`][UniqueDictionary] is a
uniquely held, dynamically resizing dictionary.
- [`struct RigidDictionary<Key, Value>`][RigidDictionary] is a
fixed-capacity dictionary.

[RigidSet]:
https://redirect.github.com/apple/swift-collections/tree/main/Sources/BasicContainers/RigidSet

[UniqueSet]:
https://redirect.github.com/apple/swift-collections/tree/main/Sources/BasicContainers/UniqueSet

[RigidDictionary]:
https://redirect.github.com/apple/swift-collections/tree/main/Sources/BasicContainers/RigidDictionary

[UniqueDictionary]:
https://redirect.github.com/apple/swift-collections/tree/main/Sources/BasicContainers/UniqueDictionary

These are direct analogues of the standard `Set` and `Dictionary` types.
These types are built on top of the `Equatable` and `Hashable` protocol
generalizations that were proposed in [SE-0499]; as that proposal is not
yet implemented in any shipping toolchain, these new types are shipping
as source-unstable previews, conditional on a new
`UnstableHashedContainers` package trait. The final API of these types
will also deeply depend on the `struct Borrow` and `struct Inout`
proposals (and potentially other language/stdlib improvements) that are
currently working their way through the Swift Evolution process.
Accordingly, we may need to make source-breaking changes to the
interfaces of these types -- they are not ready to be blessed as Public
API. However, we encourage intrepid engineers to try them on for size,
and report pain points. (Of which we expect there will be many in this
first preview.)

[SE-0499]:
https://redirect.github.com/swiftlang/swift-evolution/blob/main/proposals/0499-support-non-copyable-simple-protocols.md

We continue the pattern of `Rigid-` and `Unique-` naming prefixes with
these new types:

- The `Unique` types (`UniqueArray`, `UniqueDeque`, `UniqueSet`,
`UniqueDictionary` etc.) are dynamically self-sizing containers that
automatically reallocate their storage as needed to best accommodate
their contents; the `Unique` prefix was chosen to highlight that these
types are always uniquely held, avoiding the complications of mutating
shared copies.
- The `Rigid` types remove dynamic sizing, and they operate strictly
within an explicitly configured capacity. Dynamic sizing is not always
appropriate -- when targeting space- or time-constrained environments
(think embedded use cases or real-time work), it is preferable to avoid
implicit reallocations, and to instead choose to have explicit control
over when (and if) storage is reallocated, and to what size. This is
where the `Rigid` types come in: their instances are created with a
specific capacity and it is a runtime error to exceed that. This makes
them quite inflexible (hence the "rigid" qualifier), but in exchange,
their operations provide far stricter complexity guarantees: they
exhibit no random runtime latency spikes, and they can trivially fit in
strict memory budgets.

##### Early drafts of borrowing sequence, generative iteration and
container protocols

This release includes highly experimental but *working* implementations
of new protocols supplying ownership-aware alternatives to the classic
`Sequence`/`Collection` protocol hierarchy. These protocols and the
generic operations built on top of them can be turned on by enabling the
`UnstableContainersPreview` package trait.

- [`protocol BorrowingSequence<Element>`][BorrowingSequence] models
borrowing sequences with ephemeral lifetimes. (This is already
progressing through Swift Evolution.)
- [`protocol Container<Element>`][Container] models constructs that
physically store their contents, and can expose stable spans over them.
- [`protocol Producer<Element, ProducerError>`][Producer] models a
generative iterator -- a construct that generates items demand.
- [`protocol Drain<Element>`][Drain] refines `Producer` to model an
in-place consumable elements -- primarily for use around container
types.

[BorrowingSequence]:
https://redirect.github.com/apple/swift-collections/blob/main/Sources/ContainersPreview/Protocols/BorrowingSequence.swift

[BorrowingIteratorProtocol]:
https://redirect.github.com/apple/swift-collections/blob/main/Sources/ContainersPreview/Protocols/BorrowingIteratorProtocol.swift

[Container]:
https://redirect.github.com/apple/swift-collections/blob/main/Sources/ContainersPreview/Protocols/Container.swift

[Producer]:
https://redirect.github.com/apple/swift-collections/blob/main/Sources/ContainersPreview/Protocols/Producer.swift

[Drain]:
https://redirect.github.com/apple/swift-collections/blob/main/Sources/ContainersPreview/Protocols/Drain.swift

In this version, the package has developed these protocols just enough
to implement basic generic operations for moving data between containers
like `UniqueArray` and `RigidDeque`. As we gain experience using these,
future releases may start adding basic generic algorithms, more
protocols (bidirectional, random-access, (per)mutable, range-replaceable
containers etc.) convenience adapters, and other features -- or we may
end up entirely overhauling or simply discarding some/all of them.
Accordingly, the experimental interfaces enabled by
`UnstableContainersPreview` are not source stable, and they are not
intended for production use. We expect the eventual production version
of these (or whatever designs they evolve into) to ship in the Swift
Standard Library. We do highly recommend interested folks to try playing
with these, to get a feel for the strange problems of Ownership.

Besides these protocols, the package also defines rudimentary
substitutes of some basic primitives that belong in the Standard
Library:

- [`struct InputSpan<Element>`][InputSpan] the dual of `OutputSpan` --
while `OutputSpan` is primarily for moving items *into* somebody else's
storage, `InputSpan` enables safely moving items *out of* storage.
- [`struct Borrow<Target>`][Borrow] represents a borrowing reference to
an item. (This package models this with a pointer, which is an
ill-fitting substitute for the real implementation in the stdlib.)
- [`struct Inout<Target>`][Inout] represents a mutating reference to an
item.

[InputSpan]:
https://redirect.github.com/apple/swift-collections/blob/main/Sources/ContainersPreview/Types/InputSpan.swift

[Borrow]:
https://redirect.github.com/apple/swift-collections/blob/main/Sources/ContainersPreview/Types/Borrow.swift

[Inout]:
https://redirect.github.com/apple/swift-collections/blob/main/Sources/ContainersPreview/Types/Inout.swift

##### A formal way to access `SortedSet` and `SortedDictionary` types

The `SortedCollections` module contains (preexisting) early drafts of
two sorted collection types `SortedSet` and `SortedDictionary`, built on
top of an in-memory B-tree implementation. This release defines an
`UnstableSortedCollections` package trait that can be used to enable
building these types for experimentation without manually modifying the
package. Like in previous releases, these implementations remain
unfinished in this release, with known API issues; accordingly, these
types remain unstable. (Issue
[#&#8203;1](https://redirect.github.com/apple/swift-collections/issues/1)
remains open.) Future package releases may change their interface in
ways that break source compatibility, or they may remove these types
altogether.

##### Minor interface-level changes

- The `Collections` module no longer uses the unstable `@_exported
import` feature. Instead, it publishes public typealiases of every type
that it previously reexported from `DequeModule`, `OrderedCollections`,
`BitCollections`, `HeapModule` and `HashTreeCollections`.

- We renamed some `RigidArray`/`UniqueArray` operations to improve their
clarity at the point of use. The old names are still available, but
deprecated.

| Old name | New name |
| ----------------------------------------------- |
------------------------------------------------- |
| `append(count:initializingWith:)` |
`append(addingCount:initializingWith:)` |
| `insert(count:at:initializingWith:)` |
`insert(addingCount:at:initializingWith:)` |
| `replaceSubrange(_:newCount:initializingWith:)` |
`replace(removing:addingCount:initializingWith:)` |
| `replaceSubrange(_:moving:)` | `replace(removing:moving:)` |
| `replaceSubrange(_:copying:)` | `replace(removing:copying:)` |
| `copy()` | `clone()` |
| `copy(capacity:)` | `clone(capacity:)` |

- We have now defined a complete set of `OutputSpan`/`InputSpan`-based
`append`/`insert`/`replace`/`consume` primitives, fully generalized to
be implementable by piecewise contiguous containers. These operations
pave the way for a `Container`-based analogue of the classic
`RangeReplaceableCollection` protocol, with most of the user-facing
operations becoming standard generic algorithms built on top of these
primitives:

  ```
  mutating func append<E: Error>(
      addingCount newItemCount: Int,
initializingWith initializer: (inout OutputSpan<Element>) throws(E) ->
Void
  )

  mutating func insert<E: Error>(
     addingCount newItemCount: Int,
     at index: Int,
initializingWith initializer: (inout OutputSpan<Element>) throws(E) ->
Void
  ) throws(E)

  mutating func replace<E: Error>(
      removing subrange: Range<Int>,
      consumingWith consumer: (inout InputSpan<Element>) -> Void,
      addingCount newItemCount: Int,
initializingWith initializer: (inout OutputSpan<Element>) throws(E) ->
Void
  ) throws(E)

  mutating func consume(
      _ subrange: Range<Int>,
      consumingWith consumer: (inout InputSpan<Element>) -> Void
  )
  ```

- The package no longer uses the code generation tool `gyb`.

#### What's Changed

- Fix links in GitHub templates by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;527](https://redirect.github.com/apple/swift-collections/pull/527)
- Adopt `package` access modifier and get rid of gybbing by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;526](https://redirect.github.com/apple/swift-collections/pull/526)
- \[Doc] Fix links in landing page by
[@&#8203;Azoy](https://redirect.github.com/Azoy) in
[#&#8203;531](https://redirect.github.com/apple/swift-collections/pull/531)
- \[BigString] Refactor \_Chunk to be its own managed buffer of UTF8 by
[@&#8203;Azoy](https://redirect.github.com/Azoy) in
[#&#8203;488](https://redirect.github.com/apple/swift-collections/pull/488)
- Add new package trait UnstableSortedCollections by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;533](https://redirect.github.com/apple/swift-collections/pull/533)
- \[RopeModule] Fix warnings by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;534](https://redirect.github.com/apple/swift-collections/pull/534)
- Fix ability to build & test BigString with Xcode & CMake by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;537](https://redirect.github.com/apple/swift-collections/pull/537)
- \[BigString] Bring back Index.\_isUTF16TrailingSurrogate by
[@&#8203;Azoy](https://redirect.github.com/Azoy) in
[#&#8203;539](https://redirect.github.com/apple/swift-collections/pull/539)
- chore: restrict GitHub workflow permissions - future-proof by
[@&#8203;incertum](https://redirect.github.com/incertum) in
[#&#8203;540](https://redirect.github.com/apple/swift-collections/pull/540)
- \[BitCollections] Add missing imports for InternalCollectionsUtilities
by [@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;554](https://redirect.github.com/apple/swift-collections/pull/554)
- Compare self.value to other, not itself by
[@&#8203;SiliconA-Z](https://redirect.github.com/SiliconA-Z) in
[#&#8203;553](https://redirect.github.com/apple/swift-collections/pull/553)
- Change useFloyd heuristic to match comment by
[@&#8203;SiliconA-Z](https://redirect.github.com/SiliconA-Z) in
[#&#8203;551](https://redirect.github.com/apple/swift-collections/pull/551)
- Typo: symmetric difference should be the xor, not intersection by
[@&#8203;SiliconA-Z](https://redirect.github.com/SiliconA-Z) in
[#&#8203;550](https://redirect.github.com/apple/swift-collections/pull/550)
- first should get the Initialized elements by
[@&#8203;SiliconA-Z](https://redirect.github.com/SiliconA-Z) in
[#&#8203;549](https://redirect.github.com/apple/swift-collections/pull/549)
- Replace Container with a far less powerful (but more universal)
Iterable construct by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;543](https://redirect.github.com/apple/swift-collections/pull/543)
- Temporarily stop testing RigidArray & UniqueArray on release/6.3
snapshots on Linux by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;562](https://redirect.github.com/apple/swift-collections/pull/562)
- \[RigidArray, HashTrees] Mark deinitializers inlinable by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;560](https://redirect.github.com/apple/swift-collections/pull/560)
- GHA: Add weekly dependabot by
[@&#8203;bkhouri](https://redirect.github.com/bkhouri) in
[#&#8203;563](https://redirect.github.com/apple/swift-collections/pull/563)
- Work around temporary issue with current 6.3 snapshots by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;565](https://redirect.github.com/apple/swift-collections/pull/565)
- Add `RigidDeque` and `UniqueDeque` by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;557](https://redirect.github.com/apple/swift-collections/pull/557)
- \[Collections module] Stop using `@_exported import` by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;566](https://redirect.github.com/apple/swift-collections/pull/566)
- Delete stray benchmark results files by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;567](https://redirect.github.com/apple/swift-collections/pull/567)
- Assorted `RigidArray`/`UniqueArray` updates by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;569](https://redirect.github.com/apple/swift-collections/pull/569)
- `RigidArray`/`UniqueArray`: Add new copying span initializers by
[@&#8203;Azoy](https://redirect.github.com/Azoy) in
[#&#8203;572](https://redirect.github.com/apple/swift-collections/pull/572)
- `RigidDeque`/`UniqueDeque`: Add some top-level documentation by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;571](https://redirect.github.com/apple/swift-collections/pull/571)
- Update docs for Container.nextSpan(after:maximumCount:) by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;574](https://redirect.github.com/apple/swift-collections/pull/574)
- Remove workaround for bug in OutputSpan.wUMBP by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;570](https://redirect.github.com/apple/swift-collections/pull/570)
- \[RigidArray, RigidDeque].nextSpan: Validate `maximumCount` by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;575](https://redirect.github.com/apple/swift-collections/pull/575)
- Bump swiftlang/github-workflows/.github/workflows/soundness.yml from
0.0.6 to 0.0.7 by
[@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot] in
[#&#8203;577](https://redirect.github.com/apple/swift-collections/pull/577)
- give constant folding an opportunity to select a much faster code path
for empty dictionary (and set) literals by
[@&#8203;tayloraswift](https://redirect.github.com/tayloraswift) in
[#&#8203;578](https://redirect.github.com/apple/swift-collections/pull/578)
- Bump
swiftlang/github-workflows/.github/workflows/swift\_package\_test.yml
from 0.0.6 to 0.0.7 by
[@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot] in
[#&#8203;576](https://redirect.github.com/apple/swift-collections/pull/576)
- Ownership-aware Set and Dictionary variants by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;573](https://redirect.github.com/apple/swift-collections/pull/573)
- \[Prerelease] Check API for consistency, fill holes, patch
incoherencies by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;581](https://redirect.github.com/apple/swift-collections/pull/581)
- \[BitSet] Amend return value of `update(with:)` method by
[@&#8203;benrimmington](https://redirect.github.com/benrimmington) in
[#&#8203;538](https://redirect.github.com/apple/swift-collections/pull/538)
- \[BasicContainers] Fix spelling of a source file by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;585](https://redirect.github.com/apple/swift-collections/pull/585)
- Include notes about index mutation in `span(after/before:)` (+ other
doc fixes) by
[@&#8203;natecook1000](https://redirect.github.com/natecook1000) in
[#&#8203;541](https://redirect.github.com/apple/swift-collections/pull/541)
- \[BasicContainers] Finalize requirements for hashed containers by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;586](https://redirect.github.com/apple/swift-collections/pull/586)
- Update README for 1.4.0 by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;587](https://redirect.github.com/apple/swift-collections/pull/587)
- Working towards the 1.4.0 tag by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;588](https://redirect.github.com/apple/swift-collections/pull/588)
- \[BasicContainers] Avoid defining set/dictionary types unless
UnstableHashedContainers is enabled by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;589](https://redirect.github.com/apple/swift-collections/pull/589)
- \[BasicContainers] RigidArray: Correct spelling of replacement for
deprecated method by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;590](https://redirect.github.com/apple/swift-collections/pull/590)

#### New Contributors

- [@&#8203;incertum](https://redirect.github.com/incertum) made their
first contribution in
[#&#8203;540](https://redirect.github.com/apple/swift-collections/pull/540)
- [@&#8203;SiliconA-Z](https://redirect.github.com/SiliconA-Z) made
their first contribution in
[#&#8203;553](https://redirect.github.com/apple/swift-collections/pull/553)
- [@&#8203;bkhouri](https://redirect.github.com/bkhouri) made their
first contribution in
[#&#8203;563](https://redirect.github.com/apple/swift-collections/pull/563)
- [@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot]
made their first contribution in
[#&#8203;577](https://redirect.github.com/apple/swift-collections/pull/577)
- [@&#8203;tayloraswift](https://redirect.github.com/tayloraswift) made
their first contribution in
[#&#8203;578](https://redirect.github.com/apple/swift-collections/pull/578)
- [@&#8203;benrimmington](https://redirect.github.com/benrimmington)
made their first contribution in
[#&#8203;538](https://redirect.github.com/apple/swift-collections/pull/538)

**Full Changelog**:
<https://github.com/apple/swift-collections/compare/1.3.0...1.4.0>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My41OS4wIiwidXBkYXRlZEluVmVyIjoiNDMuNTkuMCIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 12:31:54 +00:00
renovate[bot]
6d710f3bdc chore: bump up Node.js to v22.22.1 (#14598)
> ℹ️ **Note**
> 
> This PR body was truncated due to platform limits.

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [node](https://nodejs.org)
([source](https://redirect.github.com/nodejs/node)) | patch | `22.22.0`
→ `22.22.1` |

---

### Release Notes

<details>
<summary>nodejs/node (node)</summary>

###
[`v22.22.1`](https://redirect.github.com/nodejs/node/releases/tag/v22.22.1):
2026-03-05, Version 22.22.1 &#x27;Jod&#x27; (LTS)

[Compare
Source](https://redirect.github.com/nodejs/node/compare/v22.22.0...v22.22.1)

##### Notable Changes

-
\[[`7b93a65f27`](https://redirect.github.com/nodejs/node/commit/7b93a65f27)]
- **build**: test on Python 3.14 (Christian Clauss)
[#&#8203;59983](https://redirect.github.com/nodejs/node/pull/59983)
-
\[[`6063d888fe`](https://redirect.github.com/nodejs/node/commit/6063d888fe)]
- **cli**: mark `--heapsnapshot-near-heap-limit` as stable (Joyee
Cheung)
[#&#8203;60956](https://redirect.github.com/nodejs/node/pull/60956)
-
\[[`d950b151a2`](https://redirect.github.com/nodejs/node/commit/d950b151a2)]
- **crypto**: update root certificates to NSS 3.119 (Node.js GitHub Bot)
[#&#8203;61419](https://redirect.github.com/nodejs/node/pull/61419)
-
\[[`4f42f8c428`](https://redirect.github.com/nodejs/node/commit/4f42f8c428)]
- **crypto**: update root certificates to NSS 3.117 (Node.js GitHub Bot)
[#&#8203;60741](https://redirect.github.com/nodejs/node/pull/60741)
-
\[[`b6ebf2cd53`](https://redirect.github.com/nodejs/node/commit/b6ebf2cd53)]
- **doc**: add avivkeller to collaborators (Aviv Keller)
[#&#8203;61115](https://redirect.github.com/nodejs/node/pull/61115)
-
\[[`35854f424d`](https://redirect.github.com/nodejs/node/commit/35854f424d)]
- **doc**: add gurgunday to collaborators (Gürgün Dayıoğlu)
[#&#8203;61094](https://redirect.github.com/nodejs/node/pull/61094)
-
\[[`5c6a076e5d`](https://redirect.github.com/nodejs/node/commit/5c6a076e5d)]
- **meta**: add Renegade334 to collaborators (Renegade334)
[#&#8203;60714](https://redirect.github.com/nodejs/node/pull/60714)

##### Commits

-
\[[`5f773488c2`](https://redirect.github.com/nodejs/node/commit/5f773488c2)]
- **assert**: use a set instead of an array for faster lookup (Ruben
Bridgewater)
[#&#8203;61076](https://redirect.github.com/nodejs/node/pull/61076)
-
\[[`feecbb0eab`](https://redirect.github.com/nodejs/node/commit/feecbb0eab)]
- **assert,util**: fix deep comparison for sets and maps with mixed
types (Ruben Bridgewater)
[#&#8203;61388](https://redirect.github.com/nodejs/node/pull/61388)
-
\[[`096095b127`](https://redirect.github.com/nodejs/node/commit/096095b127)]
- **benchmark**: add SQLite benchmarks (Guilherme Araújo)
[#&#8203;61401](https://redirect.github.com/nodejs/node/pull/61401)
-
\[[`b5fe481415`](https://redirect.github.com/nodejs/node/commit/b5fe481415)]
- **benchmark**: use boolean options in benchmark tests (SeokhunEom)
[#&#8203;60129](https://redirect.github.com/nodejs/node/pull/60129)
-
\[[`fa9faacacb`](https://redirect.github.com/nodejs/node/commit/fa9faacacb)]
- **benchmark**: allow boolean option values (SeokhunEom)
[#&#8203;60129](https://redirect.github.com/nodejs/node/pull/60129)
-
\[[`ba8714ac21`](https://redirect.github.com/nodejs/node/commit/ba8714ac21)]
- **benchmark**: fix incorrect base64 input in byteLength benchmark
(semimikoh)
[#&#8203;60841](https://redirect.github.com/nodejs/node/pull/60841)
-
\[[`53596de876`](https://redirect.github.com/nodejs/node/commit/53596de876)]
- **benchmark**: use typescript for import cjs benchmark (Joyee Cheung)
[#&#8203;60663](https://redirect.github.com/nodejs/node/pull/60663)
-
\[[`e8930e9d7c`](https://redirect.github.com/nodejs/node/commit/e8930e9d7c)]
- **benchmark**: focus on import.meta intialization in import-meta
benchmark (Joyee Cheung)
[#&#8203;60603](https://redirect.github.com/nodejs/node/pull/60603)
-
\[[`1155e412b1`](https://redirect.github.com/nodejs/node/commit/1155e412b1)]
- **benchmark**: add per-suite setup option (Joyee Cheung)
[#&#8203;60574](https://redirect.github.com/nodejs/node/pull/60574)
-
\[[`e01903d304`](https://redirect.github.com/nodejs/node/commit/e01903d304)]
- **benchmark**: improve cpu.sh for safety and usability (Nam Yooseong)
[#&#8203;60162](https://redirect.github.com/nodejs/node/pull/60162)
-
\[[`623a405747`](https://redirect.github.com/nodejs/node/commit/623a405747)]
- **benchmark**: add benchmark for leaf source text modules (Joyee
Cheung)
[#&#8203;60205](https://redirect.github.com/nodejs/node/pull/60205)
-
\[[`7f5e7b9f7f`](https://redirect.github.com/nodejs/node/commit/7f5e7b9f7f)]
- **benchmark**: add microbench on isInsideNodeModules (Chengzhong Wu)
[#&#8203;60991](https://redirect.github.com/nodejs/node/pull/60991)
-
\[[`db132b85a8`](https://redirect.github.com/nodejs/node/commit/db132b85a8)]
- **bootstrap**: initialize http proxy after user module loader setup
(Joyee Cheung)
[#&#8203;58938](https://redirect.github.com/nodejs/node/pull/58938)
-
\[[`66aab9f987`](https://redirect.github.com/nodejs/node/commit/66aab9f987)]
- **buffer**: let Buffer.of use heap (Сковорода Никита Андреевич)
[#&#8203;60503](https://redirect.github.com/nodejs/node/pull/60503)
-
\[[`c3cf00c671`](https://redirect.github.com/nodejs/node/commit/c3cf00c671)]
- **buffer**: speed up concat via TypedArray#set (Gürgün Dayıoğlu)
[#&#8203;60399](https://redirect.github.com/nodejs/node/pull/60399)
-
\[[`f6fad231e9`](https://redirect.github.com/nodejs/node/commit/f6fad231e9)]
- **build**: skip sscache action on non-main branches (Joyee Cheung)
[#&#8203;61790](https://redirect.github.com/nodejs/node/pull/61790)
-
\[[`2145f91f6b`](https://redirect.github.com/nodejs/node/commit/2145f91f6b)]
- **build**: update android-patches/trap-handler.h.patch (Mo Luo)
[#&#8203;60369](https://redirect.github.com/nodejs/node/pull/60369)
-
\[[`5b49759dd8`](https://redirect.github.com/nodejs/node/commit/5b49759dd8)]
- **build**: update devcontainer.json to use paired nix env (Joyee
Cheung)
[#&#8203;61414](https://redirect.github.com/nodejs/node/pull/61414)
-
\[[`24724cde40`](https://redirect.github.com/nodejs/node/commit/24724cde40)]
- **build**: fix misplaced comma in ldflags (hqzing)
[#&#8203;61294](https://redirect.github.com/nodejs/node/pull/61294)
-
\[[`c57a19934e`](https://redirect.github.com/nodejs/node/commit/c57a19934e)]
- **build**: fix crate vendor file checksums on windows (Chengzhong Wu)
[#&#8203;61329](https://redirect.github.com/nodejs/node/pull/61329)
-
\[[`8659d7cd07`](https://redirect.github.com/nodejs/node/commit/8659d7cd07)]
- **build**: fix inconsistent quoting in `Makefile` (Antoine du Hamel)
[#&#8203;60511](https://redirect.github.com/nodejs/node/pull/60511)
-
\[[`44f339b315`](https://redirect.github.com/nodejs/node/commit/44f339b315)]
- **build**: remove temporal updater (Chengzhong Wu)
[#&#8203;61151](https://redirect.github.com/nodejs/node/pull/61151)
-
\[[`d60a6cebd5`](https://redirect.github.com/nodejs/node/commit/d60a6cebd5)]
- **build**: update test-wpt-report to use NODE instead of OUT\_NODE
(Filip Skokan)
[#&#8203;61024](https://redirect.github.com/nodejs/node/pull/61024)
-
\[[`34ccf187f5`](https://redirect.github.com/nodejs/node/commit/34ccf187f5)]
- **build**: skip build-ci on actions with a separate test step
(Chengzhong Wu)
[#&#8203;61073](https://redirect.github.com/nodejs/node/pull/61073)
-
\[[`7b19e101a2`](https://redirect.github.com/nodejs/node/commit/7b19e101a2)]
- **build**: run embedtest with node\_g when BUILDTYPE=Debug (Chengzhong
Wu) [#&#8203;60850](https://redirect.github.com/nodejs/node/pull/60850)
-
\[[`9408c4459f`](https://redirect.github.com/nodejs/node/commit/9408c4459f)]
- **build**: upgrade Python linter ruff, add rules ASYNC,PERF (Christian
Clauss)
[#&#8203;59984](https://redirect.github.com/nodejs/node/pull/59984)
-
\[[`2166ec7f0f`](https://redirect.github.com/nodejs/node/commit/2166ec7f0f)]
- **build**: use call command when calling python configure (Jacob
Nichols)
[#&#8203;60098](https://redirect.github.com/nodejs/node/pull/60098)
-
\[[`73ef70145d`](https://redirect.github.com/nodejs/node/commit/73ef70145d)]
- **build**: remove V8\_COMPRESS\_POINTERS\_IN\_ISOLATE\_CAGE defs
(Joyee Cheung)
[#&#8203;60296](https://redirect.github.com/nodejs/node/pull/60296)
-
\[[`7b93a65f27`](https://redirect.github.com/nodejs/node/commit/7b93a65f27)]
- **build**: test on Python 3.14 (Christian Clauss)
[#&#8203;59983](https://redirect.github.com/nodejs/node/pull/59983)
-
\[[`508ce6ec6c`](https://redirect.github.com/nodejs/node/commit/508ce6ec6c)]
- **build, src**: fix include paths for vtune files (Rahul)
[#&#8203;59999](https://redirect.github.com/nodejs/node/pull/59999)
-
\[[`c89d3cd570`](https://redirect.github.com/nodejs/node/commit/c89d3cd570)]
- **build,tools**: fix addon build deadlock on errors (Vladimir Morozov)
[#&#8203;61321](https://redirect.github.com/nodejs/node/pull/61321)
-
\[[`40904a0591`](https://redirect.github.com/nodejs/node/commit/40904a0591)]
- **build,win**: update WinGet configurations to Python 3.14 (Mike
McCready)
[#&#8203;61431](https://redirect.github.com/nodejs/node/pull/61431)
-
\[[`6d6742e7db`](https://redirect.github.com/nodejs/node/commit/6d6742e7db)]
- **child\_process**: treat ipc length header as unsigned uint32 (Ryuhei
Shima)
[#&#8203;61344](https://redirect.github.com/nodejs/node/pull/61344)
-
\[[`6063d888fe`](https://redirect.github.com/nodejs/node/commit/6063d888fe)]
- **cli**: mark --heapsnapshot-near-heap-limit as stable (Joyee Cheung)
[#&#8203;60956](https://redirect.github.com/nodejs/node/pull/60956)
-
\[[`3d324a0f88`](https://redirect.github.com/nodejs/node/commit/3d324a0f88)]
- **cluster**: fix port reuse between cluster (Ryuhei Shima)
[#&#8203;60141](https://redirect.github.com/nodejs/node/pull/60141)
-
\[[`40a58709b4`](https://redirect.github.com/nodejs/node/commit/40a58709b4)]
- **console**: optimize single-string logging (Gürgün Dayıoğlu)
[#&#8203;60422](https://redirect.github.com/nodejs/node/pull/60422)
-
\[[`d950b151a2`](https://redirect.github.com/nodejs/node/commit/d950b151a2)]
- **crypto**: update root certificates to NSS 3.119 (Node.js GitHub Bot)
[#&#8203;61419](https://redirect.github.com/nodejs/node/pull/61419)
-
\[[`4f42f8c428`](https://redirect.github.com/nodejs/node/commit/4f42f8c428)]
- **crypto**: update root certificates to NSS 3.117 (Node.js GitHub Bot)
[#&#8203;60741](https://redirect.github.com/nodejs/node/pull/60741)
-
\[[`a87499ae25`](https://redirect.github.com/nodejs/node/commit/a87499ae25)]
- **crypto**: ensure documented RSA-PSS saltLength default is used
(Filip Skokan)
[#&#8203;60662](https://redirect.github.com/nodejs/node/pull/60662)
-
\[[`8c65cc11e2`](https://redirect.github.com/nodejs/node/commit/8c65cc11e2)]
- **crypto**: update root certificates to NSS 3.116 (Node.js GitHub Bot)
[#&#8203;59956](https://redirect.github.com/nodejs/node/pull/59956)
-
\[[`91dc00a2c1`](https://redirect.github.com/nodejs/node/commit/91dc00a2c1)]
- **debugger**: fix event listener leak in the run command (Joyee
Cheung)
[#&#8203;60464](https://redirect.github.com/nodejs/node/pull/60464)
-
\[[`0781bd3764`](https://redirect.github.com/nodejs/node/commit/0781bd3764)]
- **deps**: V8: backport
[`6a0a25a`](https://redirect.github.com/nodejs/node/commit/6a0a25abaed3)
(Vivian Wang)
[#&#8203;61688](https://redirect.github.com/nodejs/node/pull/61688)
-
\[[`0cf1f9c3e9`](https://redirect.github.com/nodejs/node/commit/0cf1f9c3e9)]
- **deps**: update googletest to
[`8508785`](85087857ad)
(Node.js GitHub Bot)
[#&#8203;61417](https://redirect.github.com/nodejs/node/pull/61417)
-
\[[`521b4b1f07`](https://redirect.github.com/nodejs/node/commit/521b4b1f07)]
- **deps**: update sqlite to 3.51.2 (Node.js GitHub Bot)
[#&#8203;61339](https://redirect.github.com/nodejs/node/pull/61339)
-
\[[`58b9d219a3`](https://redirect.github.com/nodejs/node/commit/58b9d219a3)]
- **deps**: update icu to 78.2 (Node.js GitHub Bot)
[#&#8203;60523](https://redirect.github.com/nodejs/node/pull/60523)
-
\[[`cbc1e4306d`](https://redirect.github.com/nodejs/node/commit/cbc1e4306d)]
- **deps**: update zlib to 1.3.1-e00f703 (Node.js GitHub Bot)
[#&#8203;61135](https://redirect.github.com/nodejs/node/pull/61135)
-
\[[`db59c35ed8`](https://redirect.github.com/nodejs/node/commit/db59c35ed8)]
- **deps**: update cjs-module-lexer to 2.2.0 (Node.js GitHub Bot)
[#&#8203;61271](https://redirect.github.com/nodejs/node/pull/61271)
-
\[[`c18518ee3c`](https://redirect.github.com/nodejs/node/commit/c18518ee3c)]
- **deps**: update nbytes to 0.1.2 (Node.js GitHub Bot)
[#&#8203;61270](https://redirect.github.com/nodejs/node/pull/61270)
-
\[[`376df62d63`](https://redirect.github.com/nodejs/node/commit/376df62d63)]
- **deps**: update timezone to 2025c (Node.js GitHub Bot)
[#&#8203;61138](https://redirect.github.com/nodejs/node/pull/61138)
-
\[[`993e905302`](https://redirect.github.com/nodejs/node/commit/993e905302)]
- **deps**: update simdjson to 4.2.4 (Node.js GitHub Bot)
[#&#8203;61056](https://redirect.github.com/nodejs/node/pull/61056)
-
\[[`b72fd2a5d3`](https://redirect.github.com/nodejs/node/commit/b72fd2a5d3)]
- **deps**: update googletest to
[`065127f`](065127f1e4)
(Node.js GitHub Bot)
[#&#8203;61055](https://redirect.github.com/nodejs/node/pull/61055)
-
\[[`d765147405`](https://redirect.github.com/nodejs/node/commit/d765147405)]
- **deps**: update sqlite to 3.51.1 (Node.js GitHub Bot)
[#&#8203;60899](https://redirect.github.com/nodejs/node/pull/60899)
-
\[[`37abe2a7d2`](https://redirect.github.com/nodejs/node/commit/37abe2a7d2)]
- **deps**: update zlib to 1.3.1-63d7e16 (Node.js GitHub Bot)
[#&#8203;60898](https://redirect.github.com/nodejs/node/pull/60898)
-
\[[`97241fcb86`](https://redirect.github.com/nodejs/node/commit/97241fcb86)]
- **deps**: update sqlite to 3.51.0 (Node.js GitHub Bot)
[#&#8203;60614](https://redirect.github.com/nodejs/node/pull/60614)
-
\[[`3669c7b4f4`](https://redirect.github.com/nodejs/node/commit/3669c7b4f4)]
- **deps**: update simdjson to 4.2.2 (Node.js GitHub Bot)
[#&#8203;60740](https://redirect.github.com/nodejs/node/pull/60740)
-
\[[`9a056ec89c`](https://redirect.github.com/nodejs/node/commit/9a056ec89c)]
- **deps**: update googletest to
[`1b96fa1`](1b96fa13f5)
(Node.js GitHub Bot)
[#&#8203;60739](https://redirect.github.com/nodejs/node/pull/60739)
-
\[[`b5803b3ea0`](https://redirect.github.com/nodejs/node/commit/b5803b3ea0)]
- **deps**: update minimatch to 10.1.1 (Node.js GitHub Bot)
[#&#8203;60543](https://redirect.github.com/nodejs/node/pull/60543)
-
\[[`5bf99f3d46`](https://redirect.github.com/nodejs/node/commit/5bf99f3d46)]
- **deps**: update cjs-module-lexer to 2.1.1 (Node.js GitHub Bot)
[#&#8203;60646](https://redirect.github.com/nodejs/node/pull/60646)
-
\[[`801f187357`](https://redirect.github.com/nodejs/node/commit/801f187357)]
- **deps**: update simdjson to 4.2.1 (Node.js GitHub Bot)
[#&#8203;60644](https://redirect.github.com/nodejs/node/pull/60644)
-
\[[`03c16e5a4c`](https://redirect.github.com/nodejs/node/commit/03c16e5a4c)]
- **deps**: update simdjson to 4.1.0 (Node.js GitHub Bot)
[#&#8203;60542](https://redirect.github.com/nodejs/node/pull/60542)
-
\[[`2ebfc2ca56`](https://redirect.github.com/nodejs/node/commit/2ebfc2ca56)]
- **deps**: update amaro to 1.1.5 (Node.js GitHub Bot)
[#&#8203;60541](https://redirect.github.com/nodejs/node/pull/60541)
-
\[[`d24ba4fed6`](https://redirect.github.com/nodejs/node/commit/d24ba4fed6)]
- **deps**: update simdjson to 4.0.7 (Node.js GitHub Bot)
[#&#8203;59883](https://redirect.github.com/nodejs/node/pull/59883)
-
\[[`9480a139bf`](https://redirect.github.com/nodejs/node/commit/9480a139bf)]
- **deps**: update googletest to
[`279f847`](https://redirect.github.com/nodejs/node/commit/279f847)
(Node.js GitHub Bot)
[#&#8203;60219](https://redirect.github.com/nodejs/node/pull/60219)
-
\[[`635e67379e`](https://redirect.github.com/nodejs/node/commit/635e67379e)]
- **deps**: update archs files for openssl-3.5.5 (Node.js GitHub Bot)
[#&#8203;61547](https://redirect.github.com/nodejs/node/pull/61547)
-
\[[`c7b774047d`](https://redirect.github.com/nodejs/node/commit/c7b774047d)]
- **deps**: upgrade openssl sources to openssl-3.5.5 (Node.js GitHub
Bot) [#&#8203;61547](https://redirect.github.com/nodejs/node/pull/61547)
-
\[[`5b324d7d7f`](https://redirect.github.com/nodejs/node/commit/5b324d7d7f)]
- **deps**: update corepack to 0.34.6 (Node.js GitHub Bot)
[#&#8203;61510](https://redirect.github.com/nodejs/node/pull/61510)
-
\[[`eef8ba0667`](https://redirect.github.com/nodejs/node/commit/eef8ba0667)]
- **deps**: update corepack to 0.34.5 (Node.js GitHub Bot)
[#&#8203;60842](https://redirect.github.com/nodejs/node/pull/60842)
-
\[[`490f7c7fb1`](https://redirect.github.com/nodejs/node/commit/490f7c7fb1)]
- **deps**: update corepack to 0.34.4 (Node.js GitHub Bot)
[#&#8203;60643](https://redirect.github.com/nodejs/node/pull/60643)
-
\[[`66903ea3b3`](https://redirect.github.com/nodejs/node/commit/66903ea3b3)]
- **deps**: update corepack to 0.34.2 (Node.js GitHub Bot)
[#&#8203;60550](https://redirect.github.com/nodejs/node/pull/60550)
-
\[[`a2f0b69282`](https://redirect.github.com/nodejs/node/commit/a2f0b69282)]
- **deps**: update corepack to 0.34.1 (Node.js GitHub Bot)
[#&#8203;60314](https://redirect.github.com/nodejs/node/pull/60314)
-
\[[`c8044a48a6`](https://redirect.github.com/nodejs/node/commit/c8044a48a6)]
- **deps**: V8: backport
[`2e4c5cf`](https://redirect.github.com/nodejs/node/commit/2e4c5cf9b112)
(Michaël Zasso)
[#&#8203;60654](https://redirect.github.com/nodejs/node/pull/60654)
-
\[[`642f518198`](https://redirect.github.com/nodejs/node/commit/642f518198)]
- **doc**: supported toolchain with Visual Studio 2022 only (Mike
McCready)
[#&#8203;61451](https://redirect.github.com/nodejs/node/pull/61451)
-
\[[`625f674487`](https://redirect.github.com/nodejs/node/commit/625f674487)]
- **doc**: move Security-Team from TSC to SECURITY (Rafael Gonzaga)
[#&#8203;61495](https://redirect.github.com/nodejs/node/pull/61495)
-
\[[`029e32f8ba`](https://redirect.github.com/nodejs/node/commit/029e32f8ba)]
- **doc**: added `requestOCSP` option to `tls.connect` (ikeyan)
[#&#8203;61064](https://redirect.github.com/nodejs/node/pull/61064)
-
\[[`68e33dfa89`](https://redirect.github.com/nodejs/node/commit/68e33dfa89)]
- **doc**: restore
[@&#8203;ChALkeR](https://redirect.github.com/ChALkeR) to collaborators
(Сковорода Никита Андреевич)
[#&#8203;61553](https://redirect.github.com/nodejs/node/pull/61553)
-
\[[`e016770d62`](https://redirect.github.com/nodejs/node/commit/e016770d62)]
- **doc**: update IBM/Red Hat volunteers with dedicated project time
(Beth Griggs)
[#&#8203;61588](https://redirect.github.com/nodejs/node/pull/61588)
-
\[[`ec63954657`](https://redirect.github.com/nodejs/node/commit/ec63954657)]
- **doc**: mention constructor comparison in assert.deepStrictEqual
(Hamza Kargin)
[#&#8203;60253](https://redirect.github.com/nodejs/node/pull/60253)
-
\[[`c8e1563a98`](https://redirect.github.com/nodejs/node/commit/c8e1563a98)]
- **doc**: add CVE delay mention (Rafael Gonzaga)
[#&#8203;61465](https://redirect.github.com/nodejs/node/pull/61465)
-
\[[`4b00cf2b54`](https://redirect.github.com/nodejs/node/commit/4b00cf2b54)]
- **doc**: include OpenJSF handle for security stewards (Rafael Gonzaga)
[#&#8203;61454](https://redirect.github.com/nodejs/node/pull/61454)
-
\[[`4b73bf5bc8`](https://redirect.github.com/nodejs/node/commit/4b73bf5bc8)]
- **doc**: clarify process.argv\[1] behavior for -e/--eval (Jeevankumar
S) [#&#8203;61366](https://redirect.github.com/nodejs/node/pull/61366)
-
\[[`d3151df4b3`](https://redirect.github.com/nodejs/node/commit/d3151df4b3)]
- **doc**: remove Windows Dev Home instructions from BUILDING (Mike
McCready)
[#&#8203;61434](https://redirect.github.com/nodejs/node/pull/61434)
-
\[[`2323462e35`](https://redirect.github.com/nodejs/node/commit/2323462e35)]
- **doc**: clarify TypedArray properties on Buffer (Roman Reiss)
[#&#8203;61355](https://redirect.github.com/nodejs/node/pull/61355)
-
\[[`6c5478c8b2`](https://redirect.github.com/nodejs/node/commit/6c5478c8b2)]
- **doc**: note resume build should not be done on node-test-commit
(Stewart X Addison)
[#&#8203;61373](https://redirect.github.com/nodejs/node/pull/61373)
-
\[[`ba4a043103`](https://redirect.github.com/nodejs/node/commit/ba4a043103)]
- **doc**: refine WebAssembly error documentation (sangwook)
[#&#8203;61382](https://redirect.github.com/nodejs/node/pull/61382)
-
\[[`cd315ea589`](https://redirect.github.com/nodejs/node/commit/cd315ea589)]
- **doc**: add deprecation history for url.parse (Eng Zer Jun)
[#&#8203;61389](https://redirect.github.com/nodejs/node/pull/61389)
-
\[[`42db0c392d`](https://redirect.github.com/nodejs/node/commit/42db0c392d)]
- **doc**: add marco and rafael in last sec release (Marco Ippolito)
[#&#8203;61383](https://redirect.github.com/nodejs/node/pull/61383)
-
\[[`4c3b680fc7`](https://redirect.github.com/nodejs/node/commit/4c3b680fc7)]
- **doc**: packages: example of private import switch to internal
(coderaiser)
[#&#8203;61343](https://redirect.github.com/nodejs/node/pull/61343)
-
\[[`684d15e421`](https://redirect.github.com/nodejs/node/commit/684d15e421)]
- **doc**: add esm and cjs examples to node:v8 (Alfredo González)
[#&#8203;61328](https://redirect.github.com/nodejs/node/pull/61328)
-
\[[`c3f9c7a7d9`](https://redirect.github.com/nodejs/node/commit/c3f9c7a7d9)]
- **doc**: added 'secure' event to tls.TLSSocket (ikeyan)
[#&#8203;61066](https://redirect.github.com/nodejs/node/pull/61066)
-
\[[`aa9acad5ca`](https://redirect.github.com/nodejs/node/commit/aa9acad5ca)]
- **doc**: restore
[@&#8203;watilde](https://redirect.github.com/watilde) to collaborators
(Daijiro Wachi)
[#&#8203;61350](https://redirect.github.com/nodejs/node/pull/61350)
-
\[[`9cafec084e`](https://redirect.github.com/nodejs/node/commit/9cafec084e)]
- **doc**: run license-builder (github-actions\[bot])
[#&#8203;61348](https://redirect.github.com/nodejs/node/pull/61348)
-
\[[`cdb12ccbc6`](https://redirect.github.com/nodejs/node/commit/cdb12ccbc6)]
- **doc**: document ALPNCallback option for TLSSocket constructor
(ikeyan)
[#&#8203;61331](https://redirect.github.com/nodejs/node/pull/61331)
-
\[[`461c5e65c5`](https://redirect.github.com/nodejs/node/commit/461c5e65c5)]
- **doc**: update MDN links (Livia Medeiros)
[#&#8203;61062](https://redirect.github.com/nodejs/node/pull/61062)
-
\[[`dde45baeab`](https://redirect.github.com/nodejs/node/commit/dde45baeab)]
- **doc**: add documentation for process.traceProcessWarnings (Alireza
Ebrahimkhani)
[#&#8203;53641](https://redirect.github.com/nodejs/node/pull/53641)
-
\[[`59a7aeec92`](https://redirect.github.com/nodejs/node/commit/59a7aeec92)]
- **doc**: fix filename typo (Hardanish Singh)
[#&#8203;61297](https://redirect.github.com/nodejs/node/pull/61297)
-
\[[`9a0a40d1ed`](https://redirect.github.com/nodejs/node/commit/9a0a40d1ed)]
- **doc**: fix typos and grammar in `BUILDING.md` & `onboarding.md`
(Hardanish Singh)
[#&#8203;61267](https://redirect.github.com/nodejs/node/pull/61267)
-
\[[`dca7005f9d`](https://redirect.github.com/nodejs/node/commit/dca7005f9d)]
- **doc**: mention --newVersion release script (Rafael Gonzaga)
[#&#8203;61255](https://redirect.github.com/nodejs/node/pull/61255)
-
\[[`c0dc8ddf85`](https://redirect.github.com/nodejs/node/commit/c0dc8ddf85)]
- **doc**: correct typo in api contributing doc (Mike McCready)
[#&#8203;61260](https://redirect.github.com/nodejs/node/pull/61260)
-
\[[`066af38fe1`](https://redirect.github.com/nodejs/node/commit/066af38fe1)]
- **doc**: add PR-URL requirement for security backports (Rafael
Gonzaga)
[#&#8203;61256](https://redirect.github.com/nodejs/node/pull/61256)
-
\[[`71dd46bd0c`](https://redirect.github.com/nodejs/node/commit/71dd46bd0c)]
- **doc**: add reusePort error behavior to net module (mag123c)
[#&#8203;61250](https://redirect.github.com/nodejs/node/pull/61250)
-
\[[`f6abe3ba33`](https://redirect.github.com/nodejs/node/commit/f6abe3ba33)]
- **doc**: note corepack package removal in distribution doc (Mike
McCready)
[#&#8203;61207](https://redirect.github.com/nodejs/node/pull/61207)
-
\[[`9059d49d8c`](https://redirect.github.com/nodejs/node/commit/9059d49d8c)]
- **doc**: fix tls.connect() timeout documentation (Azad Gupta)
[#&#8203;61079](https://redirect.github.com/nodejs/node/pull/61079)
-
\[[`e7b34b76b0`](https://redirect.github.com/nodejs/node/commit/e7b34b76b0)]
- **doc**: missing `passed`, `error` and `passed` properties on
`TestContext` (Xavier Stouder)
[#&#8203;61185](https://redirect.github.com/nodejs/node/pull/61185)
-
\[[`9ae2dcfbb6`](https://redirect.github.com/nodejs/node/commit/9ae2dcfbb6)]
- **doc**: clarify threat model for application-level API exposure
(Rafael Gonzaga)
[#&#8203;61184](https://redirect.github.com/nodejs/node/pull/61184)
-
\[[`9902331a7c`](https://redirect.github.com/nodejs/node/commit/9902331a7c)]
- **doc**: correct options for net.Socket class and socket.connect
(Xavier Stouder)
[#&#8203;61179](https://redirect.github.com/nodejs/node/pull/61179)
-
\[[`a80122d2fe`](https://redirect.github.com/nodejs/node/commit/a80122d2fe)]
- **doc**: document error event on readline InterfaceConstructor (Xavier
Stouder)
[#&#8203;61170](https://redirect.github.com/nodejs/node/pull/61170)
-
\[[`38d73c9cfa`](https://redirect.github.com/nodejs/node/commit/38d73c9cfa)]
- **doc**: add a smooth scrolling effect to the sidebar (btea)
[#&#8203;59007](https://redirect.github.com/nodejs/node/pull/59007)
-
\[[`95c51fa984`](https://redirect.github.com/nodejs/node/commit/95c51fa984)]
- **doc**: correct invalid collaborator profile (JJ)
[#&#8203;61091](https://redirect.github.com/nodejs/node/pull/61091)
-
\[[`f5a044763c`](https://redirect.github.com/nodejs/node/commit/f5a044763c)]
- **doc**: exclude compile-time flag features from security policy
(Matteo Collina)
[#&#8203;61109](https://redirect.github.com/nodejs/node/pull/61109)
-
\[[`b6ebf2cd53`](https://redirect.github.com/nodejs/node/commit/b6ebf2cd53)]
- **doc**: add
[@&#8203;avivkeller](https://redirect.github.com/avivkeller) to
collaborators (Aviv Keller)
[#&#8203;61115](https://redirect.github.com/nodejs/node/pull/61115)
-
\[[`35854f424d`](https://redirect.github.com/nodejs/node/commit/35854f424d)]
- **doc**: add gurgunday to collaborators (Gürgün Dayıoğlu)
[#&#8203;61094](https://redirect.github.com/nodejs/node/pull/61094)
-
\[[`4932322c29`](https://redirect.github.com/nodejs/node/commit/4932322c29)]
- **doc**: add File modes cross-references in fs methods (Mohit Raj
Saxena)
[#&#8203;60286](https://redirect.github.com/nodejs/node/pull/60286)
-
\[[`c84904e047`](https://redirect.github.com/nodejs/node/commit/c84904e047)]
- **doc**: add missing `zstd` to mjs example of zlib (Deokjin Kim)
[#&#8203;60915](https://redirect.github.com/nodejs/node/pull/60915)
-
\[[`e615b9e2f2`](https://redirect.github.com/nodejs/node/commit/e615b9e2f2)]
- **doc**: clarify fileURLToPath security considerations (Rafael
Gonzaga)
[#&#8203;60887](https://redirect.github.com/nodejs/node/pull/60887)
-
\[[`99e384e6d4`](https://redirect.github.com/nodejs/node/commit/99e384e6d4)]
- **doc**: replace column with columnNumber in example of
`util.getCallSites` (Deokjin Kim)
[#&#8203;60881](https://redirect.github.com/nodejs/node/pull/60881)
-
\[[`9351bb4d02`](https://redirect.github.com/nodejs/node/commit/9351bb4d02)]
- **doc**: correct spelling in BUILDING.md (Rich Trott)
[#&#8203;60875](https://redirect.github.com/nodejs/node/pull/60875)
-
\[[`e1f6e7fc4d`](https://redirect.github.com/nodejs/node/commit/e1f6e7fc4d)]
- **doc**: update debuglog examples to use 'foo-bar' instead of 'foo'
(xiaoyao)
[#&#8203;60867](https://redirect.github.com/nodejs/node/pull/60867)
-
\[[`ccbb2d7300`](https://redirect.github.com/nodejs/node/commit/ccbb2d7300)]
- **doc**: fix typos in changelogs (Rich Trott)
[#&#8203;60855](https://redirect.github.com/nodejs/node/pull/60855)
-
\[[`1cb2fe8b35`](https://redirect.github.com/nodejs/node/commit/1cb2fe8b35)]
- **doc**: mark module.register as active development (Chengzhong Wu)
[#&#8203;60849](https://redirect.github.com/nodejs/node/pull/60849)
-
\[[`ceeb4968a6`](https://redirect.github.com/nodejs/node/commit/ceeb4968a6)]
- **doc**: add fullName property to SuiteContext (PaulyBearCoding)
[#&#8203;60762](https://redirect.github.com/nodejs/node/pull/60762)
-
\[[`56155909dd`](https://redirect.github.com/nodejs/node/commit/56155909dd)]
- **doc**: keep sidebar module visible when navigating docs (Botato)
[#&#8203;60410](https://redirect.github.com/nodejs/node/pull/60410)
-
\[[`6b637763d5`](https://redirect.github.com/nodejs/node/commit/6b637763d5)]
- **doc**: correct concurrency wording in test() documentation (Azad
Gupta)
[#&#8203;60773](https://redirect.github.com/nodejs/node/pull/60773)
-
\[[`7183e8ffa1`](https://redirect.github.com/nodejs/node/commit/7183e8ffa1)]
- **doc**: clarify that CQ only picks up PRs targeting `main` (René)
[#&#8203;60731](https://redirect.github.com/nodejs/node/pull/60731)
-
\[[`d5d94303be`](https://redirect.github.com/nodejs/node/commit/d5d94303be)]
- **doc**: clarify license section and add contributor note
(KaleruMadhu)
[#&#8203;60590](https://redirect.github.com/nodejs/node/pull/60590)
-
\[[`e0210c8f53`](https://redirect.github.com/nodejs/node/commit/e0210c8f53)]
- **doc**: correct tls ALPNProtocols types (René)
[#&#8203;60143](https://redirect.github.com/nodejs/node/pull/60143)
-
\[[`eff87b498a`](https://redirect.github.com/nodejs/node/commit/eff87b498a)]
- **doc**: remove mention of SMS 2FA (Antoine du Hamel)
[#&#8203;60707](https://redirect.github.com/nodejs/node/pull/60707)
-
\[[`e77ef94a51`](https://redirect.github.com/nodejs/node/commit/e77ef94a51)]
- **doc**: `domain.add()` does not accept timer objects (René)
[#&#8203;60675](https://redirect.github.com/nodejs/node/pull/60675)
-
\[[`4fe19c95ea`](https://redirect.github.com/nodejs/node/commit/4fe19c95ea)]
- **doc**: update Collaborators list to reflect hybrist handle change
(Antoine du Hamel)
[#&#8203;60650](https://redirect.github.com/nodejs/node/pull/60650)
-
\[[`eece59b6ce`](https://redirect.github.com/nodejs/node/commit/eece59b6ce)]
- **doc**: fix linter issues (Antoine du Hamel)
[#&#8203;60636](https://redirect.github.com/nodejs/node/pull/60636)
-
\[[`6e17e596e4`](https://redirect.github.com/nodejs/node/commit/6e17e596e4)]
- **doc**: correct values/references for buffer.kMaxLength (René)
[#&#8203;60305](https://redirect.github.com/nodejs/node/pull/60305)
-
\[[`ac327ae9a7`](https://redirect.github.com/nodejs/node/commit/ac327ae9a7)]
- **doc**: recommend events.once to manage 'close' event (Dan Fabulich)
[#&#8203;60017](https://redirect.github.com/nodejs/node/pull/60017)
-
\[[`d9b149ea42`](https://redirect.github.com/nodejs/node/commit/d9b149ea42)]
- **doc**: highlight module loading difference between import and
require (Ajay A)
[#&#8203;59815](https://redirect.github.com/nodejs/node/pull/59815)
-
\[[`f6d62cb22c`](https://redirect.github.com/nodejs/node/commit/f6d62cb22c)]
- **doc**: fix typo in `process.unref` documentation (우혁)
[#&#8203;59698](https://redirect.github.com/nodejs/node/pull/59698)
-
\[[`6d5078b196`](https://redirect.github.com/nodejs/node/commit/6d5078b196)]
- **doc**: add some entries to `glossary.md` (Mohataseem Khan)
[#&#8203;59277](https://redirect.github.com/nodejs/node/pull/59277)
-
\[[`b0a5820dea`](https://redirect.github.com/nodejs/node/commit/b0a5820dea)]
- **doc**: improve agent.createConnection docs for http and https agents
(JaeHo Jang)
[#&#8203;58205](https://redirect.github.com/nodejs/node/pull/58205)
-
\[[`b5db02fe67`](https://redirect.github.com/nodejs/node/commit/b5db02fe67)]
- **doc**: fix pseudo code in modules.md (chirsz)
[#&#8203;57677](https://redirect.github.com/nodejs/node/pull/57677)
-
\[[`e9b912d481`](https://redirect.github.com/nodejs/node/commit/e9b912d481)]
- **doc**: add missing variable in code snippet (Koushil Mankali)
[#&#8203;55478](https://redirect.github.com/nodejs/node/pull/55478)
-
\[[`44c06c7812`](https://redirect.github.com/nodejs/node/commit/44c06c7812)]
- **doc**: add missing word in `single-executable-applications.md`
(Konstantin Tsabolov)
[#&#8203;53864](https://redirect.github.com/nodejs/node/pull/53864)
-
\[[`482b43f160`](https://redirect.github.com/nodejs/node/commit/482b43f160)]
- **doc**: fix typo in http.md (Michael Solomon)
[#&#8203;59354](https://redirect.github.com/nodejs/node/pull/59354)
-
\[[`cd323bc718`](https://redirect.github.com/nodejs/node/commit/cd323bc718)]
- **doc**: update devcontainer.json and add documentation (Joyee Cheung)
[#&#8203;60472](https://redirect.github.com/nodejs/node/pull/60472)
-
\[[`c7c70f3a16`](https://redirect.github.com/nodejs/node/commit/c7c70f3a16)]
- **doc**: add haramj as triager (Haram Jeong)
[#&#8203;60348](https://redirect.github.com/nodejs/node/pull/60348)
-
\[[`04b8c4d14e`](https://redirect.github.com/nodejs/node/commit/04b8c4d14e)]
- **doc**: clarify require(esm) description (dynst)
[#&#8203;60520](https://redirect.github.com/nodejs/node/pull/60520)
-
\[[`de382dc832`](https://redirect.github.com/nodejs/node/commit/de382dc832)]
- **doc**: instantiate resolver object (Donghoon Nam)
[#&#8203;60476](https://redirect.github.com/nodejs/node/pull/60476)
-
\[[`b6845ce460`](https://redirect.github.com/nodejs/node/commit/b6845ce460)]
- **doc**: clarify --use-system-ca support status (Joyee Cheung)
[#&#8203;60340](https://redirect.github.com/nodejs/node/pull/60340)
-
\[[`0894dae9bc`](https://redirect.github.com/nodejs/node/commit/0894dae9bc)]
- **doc**: add missing CAA type to dns.resolveAny() &
dnsPromises.resolveAny() (Jimmy Leung)
[#&#8203;58899](https://redirect.github.com/nodejs/node/pull/58899)
-
\[[`c86a69f692`](https://redirect.github.com/nodejs/node/commit/c86a69f692)]
- **doc**: use `any` for `worker_threads.Worker` 'error' event argument
`err` (Jonas Geiler)
[#&#8203;60300](https://redirect.github.com/nodejs/node/pull/60300)
-
\[[`0c5031e233`](https://redirect.github.com/nodejs/node/commit/0c5031e233)]
- **doc**: update decorator documentation to reflect actual policy
(Muhammad Salman Aziz)
[#&#8203;60288](https://redirect.github.com/nodejs/node/pull/60288)
-
\[[`b01f710175`](https://redirect.github.com/nodejs/node/commit/b01f710175)]
- **doc**: document wildcard supported by tools/test.py (Joyee Cheung)
[#&#8203;60265](https://redirect.github.com/nodejs/node/pull/60265)
-
\[[`b4524dabcc`](https://redirect.github.com/nodejs/node/commit/b4524dabcc)]
- **doc**: fix `blob.bytes()` heading level (XTY)
[#&#8203;60252](https://redirect.github.com/nodejs/node/pull/60252)
-
\[[`5df02776e3`](https://redirect.github.com/nodejs/node/commit/5df02776e3)]
- **doc**: fix not working code example in vm docs (Artur Gawlik)
[#&#8203;60224](https://redirect.github.com/nodejs/node/pull/60224)
-
\[[`6a4359a0b5`](https://redirect.github.com/nodejs/node/commit/6a4359a0b5)]
- **doc**: improve code snippet alternative of url.parse() using WHATWG
URL (Steven)
[#&#8203;60209](https://redirect.github.com/nodejs/node/pull/60209)
-
\[[`ad06bee70d`](https://redirect.github.com/nodejs/node/commit/ad06bee70d)]
- **doc**: use markdown when branch-diff major release (Rafael Gonzaga)
[#&#8203;60179](https://redirect.github.com/nodejs/node/pull/60179)
-
\[[`c0d4b11ed4`](https://redirect.github.com/nodejs/node/commit/c0d4b11ed4)]
- **doc**: update teams in collaborator-guide.md and add links (Bart
Louwers)
[#&#8203;60065](https://redirect.github.com/nodejs/node/pull/60065)
-
\[[`20b5ffcac3`](https://redirect.github.com/nodejs/node/commit/20b5ffcac3)]
- **doc**: update previous version links in BUILDING (Mike McCready)
[#&#8203;61457](https://redirect.github.com/nodejs/node/pull/61457)
-
\[[`de345ea3a3`](https://redirect.github.com/nodejs/node/commit/de345ea3a3)]
- **doc**: correct description of `error.stack` accessor behavior (René)
[#&#8203;61090](https://redirect.github.com/nodejs/node/pull/61090)
-
\[[`d8418d9de7`](https://redirect.github.com/nodejs/node/commit/d8418d9de7)]
- **doc**: fix link in `--env-file=file` section (N. Bighetti)
[#&#8203;60563](https://redirect.github.com/nodejs/node/pull/60563)
-
\[[`1107bda21e`](https://redirect.github.com/nodejs/node/commit/1107bda21e)]
- **doc**: fix v22 changelog after security release (Marco Ippolito)
[#&#8203;61371](https://redirect.github.com/nodejs/node/pull/61371)
-
\[[`42aab9469a`](https://redirect.github.com/nodejs/node/commit/42aab9469a)]
- **doc**: add missing history entry for `sqlite.md` (Antoine du Hamel)
[#&#8203;60607](https://redirect.github.com/nodejs/node/pull/60607)
-
\[[`deb6d5deff`](https://redirect.github.com/nodejs/node/commit/deb6d5deff)]
- **doc, module**: change async customization hooks to experimental
(Gerhard Stöbich)
[#&#8203;60302](https://redirect.github.com/nodejs/node/pull/60302)
-
\[[`c659add7d1`](https://redirect.github.com/nodejs/node/commit/c659add7d1)]
- **doc,src,lib**: clarify experimental status of Web Storage support
(Antoine du Hamel)
[#&#8203;60708](https://redirect.github.com/nodejs/node/pull/60708)
-
\[[`dda95e91b9`](https://redirect.github.com/nodejs/node/commit/dda95e91b9)]
- **esm**: avoid throw when module specifier is not url (Craig Macomber
(Microsoft))
[#&#8203;61000](https://redirect.github.com/nodejs/node/pull/61000)
-
\[[`912945be89`](https://redirect.github.com/nodejs/node/commit/912945be89)]
- **events**: remove redundant todo (Gürgün Dayıoğlu)
[#&#8203;60595](https://redirect.github.com/nodejs/node/pull/60595)
-
\[[`22e156eb10`](https://redirect.github.com/nodejs/node/commit/22e156eb10)]
- **events**: remove eventtarget custom inspect branding (Efe)
[#&#8203;61128](https://redirect.github.com/nodejs/node/pull/61128)
-
\[[`df6fd9b03f`](https://redirect.github.com/nodejs/node/commit/df6fd9b03f)]
- **fs**: remove duplicate getValidatedPath calls (Mert Can Altin)
[#&#8203;61359](https://redirect.github.com/nodejs/node/pull/61359)
-
\[[`6ea3e4d850`](https://redirect.github.com/nodejs/node/commit/6ea3e4d850)]
- **fs**: fix errorOnExist behavior for directory copy in fs.cp
(Nicholas Paun)
[#&#8203;60946](https://redirect.github.com/nodejs/node/pull/60946)
-
\[[`dd918b9980`](https://redirect.github.com/nodejs/node/commit/dd918b9980)]
- **fs**: fix ENOTDIR in globSync when file is treated as dir (sangwook)
[#&#8203;61259](https://redirect.github.com/nodejs/node/pull/61259)
-
\[[`4908e67ba0`](https://redirect.github.com/nodejs/node/commit/4908e67ba0)]
- **fs**: remove duplicate fd validation in sync functions (Mert Can
Altin)
[#&#8203;61361](https://redirect.github.com/nodejs/node/pull/61361)
-
\[[`4a27bce3d9`](https://redirect.github.com/nodejs/node/commit/4a27bce3d9)]
- **fs**: detect dot files when using globstar (Robin van Wijngaarden)
[#&#8203;61012](https://redirect.github.com/nodejs/node/pull/61012)
-
\[[`b0186ff65c`](https://redirect.github.com/nodejs/node/commit/b0186ff65c)]
- **fs**: validate statfs path (Efe)
[#&#8203;61230](https://redirect.github.com/nodejs/node/pull/61230)
-
\[[`6689775023`](https://redirect.github.com/nodejs/node/commit/6689775023)]
- **gyp**: aix: change gcc version detection so CXX="ccache g++" works
(Stewart X Addison)
[#&#8203;61464](https://redirect.github.com/nodejs/node/pull/61464)
-
\[[`5c4f4db663`](https://redirect.github.com/nodejs/node/commit/5c4f4db663)]
- **http**: fix rawHeaders exceeding maxHeadersCount limit (Max Harari)
[#&#8203;61285](https://redirect.github.com/nodejs/node/pull/61285)
-
\[[`7599e2eccd`](https://redirect.github.com/nodejs/node/commit/7599e2eccd)]
- **http**: replace startsWith with strict equality (btea)
[#&#8203;59394](https://redirect.github.com/nodejs/node/pull/59394)
-
\[[`99a85213bf`](https://redirect.github.com/nodejs/node/commit/99a85213bf)]
- **http**: lazy allocate cookies array (Robert Nagy)
[#&#8203;59734](https://redirect.github.com/nodejs/node/pull/59734)
-
\[[`7669e6a5ad`](https://redirect.github.com/nodejs/node/commit/7669e6a5ad)]
- **http**: fix http client leaky with double response (theanarkh)
[#&#8203;60062](https://redirect.github.com/nodejs/node/pull/60062)
-
\[[`f074c126a8`](https://redirect.github.com/nodejs/node/commit/f074c126a8)]
- **http,https**: fix double ERR\_PROXY\_TUNNEL emission (Shima Ryuhei)
[#&#8203;60699](https://redirect.github.com/nodejs/node/pull/60699)
-
\[[`d8ac368363`](https://redirect.github.com/nodejs/node/commit/d8ac368363)]
- **http2**: add diagnostics channels for client stream request body
(Darshan Sen)
[#&#8203;60480](https://redirect.github.com/nodejs/node/pull/60480)
-
\[[`e26a7e464d`](https://redirect.github.com/nodejs/node/commit/e26a7e464d)]
- **http2**: rename variable to additionalPseudoHeaders (Tobias Nießen)
[#&#8203;60208](https://redirect.github.com/nodejs/node/pull/60208)
-
\[[`5df634f46e`](https://redirect.github.com/nodejs/node/commit/5df634f46e)]
- **http2**: validate initialWindowSize per HTTP/2 spec (Matteo Collina)
[#&#8203;61402](https://redirect.github.com/nodejs/node/pull/61402)
-
\[[`2ccc9a6205`](https://redirect.github.com/nodejs/node/commit/2ccc9a6205)]
- **http2**: do not crash on mismatched ping buffer length (René)
[#&#8203;60135](https://redirect.github.com/nodejs/node/pull/60135)
-
\[[`3e68a5f78a`](https://redirect.github.com/nodejs/node/commit/3e68a5f78a)]
- **inspector**: inspect HTTP response body (Chengzhong Wu)
[#&#8203;60572](https://redirect.github.com/nodejs/node/pull/60572)
-
\[[`a86ffa9a5d`](https://redirect.github.com/nodejs/node/commit/a86ffa9a5d)]
- **inspector**: add network payload buffer size limits (Chengzhong Wu)
[#&#8203;60236](https://redirect.github.com/nodejs/node/pull/60236)
-
\[[`ea60ef5d74`](https://redirect.github.com/nodejs/node/commit/ea60ef5d74)]
- **lib**: fix typo in `util.js` comment (Taejin Kim)
[#&#8203;61365](https://redirect.github.com/nodejs/node/pull/61365)
-
\[[`9d8d9322a4`](https://redirect.github.com/nodejs/node/commit/9d8d9322a4)]
- **lib**: fix TypeScript support check in jitless mode (sangwook)
[#&#8203;61382](https://redirect.github.com/nodejs/node/pull/61382)
-
\[[`fc26f5c78f`](https://redirect.github.com/nodejs/node/commit/fc26f5c78f)]
- **lib**: gbk decoder is gb18030 decoder per spec (Сковорода Никита
Андреевич)
[#&#8203;61099](https://redirect.github.com/nodejs/node/pull/61099)
-
\[[`3b87030012`](https://redirect.github.com/nodejs/node/commit/3b87030012)]
- **lib**: enforce use of `URLParse` (Antoine du Hamel)
[#&#8203;61016](https://redirect.github.com/nodejs/node/pull/61016)
-
\[[`2a7479d4fc`](https://redirect.github.com/nodejs/node/commit/2a7479d4fc)]
- **lib**: use `FastBuffer` for empty buffer allocation (Gürgün
Dayıoğlu)
[#&#8203;60558](https://redirect.github.com/nodejs/node/pull/60558)
-
\[[`7cf4c43582`](https://redirect.github.com/nodejs/node/commit/7cf4c43582)]
- **lib**: fix constructor in \_errnoException stack tree (SeokHun)
[#&#8203;60156](https://redirect.github.com/nodejs/node/pull/60156)
-
\[[`f9d87fbfaa`](https://redirect.github.com/nodejs/node/commit/f9d87fbfaa)]
- **lib**: fix typo in QuicSessionStats (SeokHun)
[#&#8203;60155](https://redirect.github.com/nodejs/node/pull/60155)
-
\[[`8d26ccc652`](https://redirect.github.com/nodejs/node/commit/8d26ccc652)]
- **lib**: remove redundant destroyHook checks (Gürgün Dayıoğlu)
[#&#8203;60120](https://redirect.github.com/nodejs/node/pull/60120)
-
\[[`705832a1be`](https://redirect.github.com/nodejs/node/commit/705832a1be)]
- **lib,src**: isInsideNodeModules should test on the first non-internal
frame (Chengzhong Wu)
[#&#8203;60991](https://redirect.github.com/nodejs/node/pull/60991)
-
\[[`6f39ad190b`](https://redirect.github.com/nodejs/node/commit/6f39ad190b)]
- **meta**: do not fast-track npm updates (Antoine du Hamel)
[#&#8203;61475](https://redirect.github.com/nodejs/node/pull/61475)
-
\[[`a6a0ff9486`](https://redirect.github.com/nodejs/node/commit/a6a0ff9486)]
- **meta**: fix typos in issue template config (Daijiro Wachi)
[#&#8203;61399](https://redirect.github.com/nodejs/node/pull/61399)
-
\[[`ec88c9b378`](https://redirect.github.com/nodejs/node/commit/ec88c9b378)]
- **meta**: label v8 module PRs (René)
[#&#8203;61325](https://redirect.github.com/nodejs/node/pull/61325)
-
\[[`83143835de`](https://redirect.github.com/nodejs/node/commit/83143835de)]
- **meta**: bump step-security/harden-runner from 2.13.2 to 2.14.0
(dependabot\[bot])
[#&#8203;61245](https://redirect.github.com/nodejs/node/pull/61245)
-
\[[`0802dc663a`](https://redirect.github.com/nodejs/node/commit/0802dc663a)]
- **meta**: bump actions/setup-node from 6.0.0 to 6.1.0
(dependabot\[bot])
[#&#8203;61244](https://redirect.github.com/nodejs/node/pull/61244)
-
\[[`587db55796`](https://redirect.github.com/nodejs/node/commit/587db55796)]
- **meta**: bump actions/cache from 4.3.0 to 5.0.1 (dependabot\[bot])
[#&#8203;61243](https://redirect.github.com/nodejs/node/pull/61243)
-
\[[`262c9d37a6`](https://redirect.github.com/nodejs/node/commit/262c9d37a6)]
- **meta**: bump github/codeql-action from 4.31.6 to 4.31.9
(dependabot\[bot])
[#&#8203;61241](https://redirect.github.com/nodejs/node/pull/61241)
-
\[[`d9763b5afd`](https://redirect.github.com/nodejs/node/commit/d9763b5afd)]
- **meta**: bump codecov/codecov-action from 5.5.1 to 5.5.2
(dependabot\[bot])
[#&#8203;61240](https://redirect.github.com/nodejs/node/pull/61240)
-
\[[`0af73d1811`](https://redirect.github.com/nodejs/node/commit/0af73d1811)]
- **meta**: bump peter-evans/create-pull-request from 7.0.9 to 8.0.0
(dependabot\[bot])
[#&#8203;61237](https://redirect.github.com/nodejs/node/pull/61237)
-
\[[`8be6afd239`](https://redirect.github.com/nodejs/node/commit/8be6afd239)]
- **meta**: move lukekarrys to emeritus (Node.js GitHub Bot)
[#&#8203;60985](https://redirect.github.com/nodejs/node/pull/60985)
-
\[[`c497de5c74`](https://redirect.github.com/nodejs/node/commit/c497de5c74)]
- **meta**: bump actions/setup-python from 6.0.0 to 6.1.0
(dependabot\[bot])
[#&#8203;60927](https://redirect.github.com/nodejs/node/pull/60927)
-
\[[`774920f169`](https://redirect.github.com/nodejs/node/commit/774920f169)]
- **meta**: bump github/codeql-action from 4.31.3 to 4.31.6
(dependabot\[bot])
[#&#8203;60926](https://redirect.github.com/nodejs/node/pull/60926)
-
\[[`ef3b1e5991`](https://redirect.github.com/nodejs/node/commit/ef3b1e5991)]
- **meta**: bump peter-evans/create-pull-request from 7.0.8 to 7.0.9
(dependabot\[bot])
[#&#8203;60924](https://redirect.github.com/nodejs/node/pull/60924)
-
\[[`3ed667379f`](https://redirect.github.com/nodejs/node/commit/3ed667379f)]
- **meta**: bump github/codeql-action from 4.31.2 to 4.31.3
(dependabot\[bot])
[#&#8203;60770](https://redirect.github.com/nodejs/node/pull/60770)
-
\[[`7c0cefb126`](https://redirect.github.com/nodejs/node/commit/7c0cefb126)]
- **meta**: bump step-security/harden-runner from 2.13.1 to 2.13.2
(dependabot\[bot])
[#&#8203;60769](https://redirect.github.com/nodejs/node/pull/60769)
-
\[[`5c6a076e5d`](https://redirect.github.com/nodejs/node/commit/5c6a076e5d)]
- **meta**: add Renegade334 to collaborators (Renegade334)
[#&#8203;60714](https://redirect.github.com/nodejs/node/pull/60714)
-
\[[`4f4dda2a18`](https://redirect.github.com/nodejs/node/commit/4f4dda2a18)]
- **meta**: bump actions/download-artifact from 5.0.0 to 6.0.0
(dependabot\[bot])
[#&#8203;60532](https://redirect.github.com/nodejs/node/pull/60532)
-
\[[`c436f8d57c`](https://redirect.github.com/nodejs/node/commit/c436f8d57c)]
- **meta**: bump actions/upload-artifact from 4.6.2 to 5.0.0
(dependabot\[bot])
[#&#8203;60531](https://redirect.github.com/nodejs/node/pull/60531)
-
\[[`402d9f87a6`](https://redirect.github.com/nodejs/node/commit/402d9f87a6)]
- **meta**: bump github/codeql-action from 3.30.5 to 4.31.2
(dependabot\[bot])
[#&#8203;60533](https://redirect.github.com/nodejs/node/pull/60533)
-
\[[`61be78e326`](https://redirect.github.com/nodejs/node/commit/61be78e326)]
- **meta**: bump actions/setup-node from 5.0.0 to 6.0.0
(dependabot\[bot])
[#&#8203;60529](https://redirect.github.com/nodejs/node/pull/60529)
-
\[[`7e4164a623`](https://redirect.github.com/nodejs/node/commit/7e4164a623)]
- **meta**: bump actions/stale from 10.0.0 to 10.1.0 (dependabot\[bot])
[#&#8203;60528](https://redirect.github.com/nodejs/node/pull/60528)
-
\[[`1bf6e1d010`](https://redirect.github.com/nodejs/node/commit/1bf6e1d010)]
- **meta**: move one or more collaborators to emeritus (Node.js GitHub
Bot) [#&#8203;60325](https://redirect.github.com/nodejs/node/pull/60325)
-
\[[`c66fc0e9cf`](https://redirect.github.com/nodejs/node/commit/c66fc0e9cf)]
- **meta**: loop userland-migrations in deprecations (Chengzhong Wu)
[#&#8203;60299](https://redirect.github.com/nodejs/node/pull/60299)
-
\[[`e4be0791e7`](https://redirect.github.com/nodejs/node/commit/e4be0791e7)]
- **meta**: call `create-release-post.yml` post release (Aviv Keller)
[#&#8203;60366](https://redirect.github.com/nodejs/node/pull/60366)
-
\[[`8674f6527f`](https://redirect.github.com/nodejs/node/commit/8674f6527f)]
- **module**: preserve URL in the parent created by createRequire()
(Joyee Cheung)
[#&#8203;60974](https://redirect.github.com/nodejs/node/pull/60974)
-
\[[`41db87a975`](https://redirect.github.com/nodejs/node/commit/41db87a975)]
- **msi**: fix WiX warnings (Stefan Stojanovic)
[#&#8203;60251](https://redirect.github.com/nodejs/node/pull/60251)
-
\[[`884f313f40`](https://redirect.github.com/nodejs/node/commit/884f313f40)]
- **node-api**: use Node-API in comments (Vladimir Morozov)
[#&#8203;61320](https://redirect.github.com/nodejs/node/pull/61320)
-
\[[`375164190b`](https://redirect.github.com/nodejs/node/commit/375164190b)]
- **node-api**: use local files for instanceof test (Vladimir Morozov)
[#&#8203;60190](https://redirect.github.com/nodejs/node/pull/60190)
-
\[[`972a1107c0`](https://redirect.github.com/nodejs/node/commit/972a1107c0)]
- **os**: freeze signals constant (Xavier Stouder)
[#&#8203;61038](https://redirect.github.com/nodejs/node/pull/61038)
-
\[[`e992057ab7`](https://redirect.github.com/nodejs/node/commit/e992057ab7)]
- **perf\_hooks**: fix stack overflow error (Antoine du Hamel)
[#&#8203;60084](https://redirect.github.com/nodejs/node/pull/60084)
-
\[[`0bb1814fdf`](https://redirect.github.com/nodejs/node/commit/0bb1814fdf)]
- **repl**: fix pasting after moving the cursor to the left (Ruben
Bridgewater)
[#&#8203;60470](https://redirect.github.com/nodejs/node/pull/60470)
-
\[[`35a12fb996`](https://redirect.github.com/nodejs/node/commit/35a12fb996)]
- **src**: replace `ranges::sort` for libc++13 compatibility on armhf
(Rebroad)
[#&#8203;61789](https://redirect.github.com/nodejs/node/pull/61789)
-
\[[`dbf00d4664`](https://redirect.github.com/nodejs/node/commit/dbf00d4664)]
- **src**: add missing override specifier to Clean() (Tobias Nießen)
[#&#8203;61429](https://redirect.github.com/nodejs/node/pull/61429)
-
\[[`140eba35d3`](https://redirect.github.com/nodejs/node/commit/140eba35d3)]
- **src**: cache context lookup in vectored io loops (Mert Can Altin)
[#&#8203;61387](https://redirect.github.com/nodejs/node/pull/61387)
-
\[[`93e7e1708b`](https://redirect.github.com/nodejs/node/commit/93e7e1708b)]
- **src**: use C++ nullptr in webstorage (Tobias Nießen)
[#&#8203;61407](https://redirect.github.com/nodejs/node/pull/61407)
-
\[[`ef868447bc`](https://redirect.github.com/nodejs/node/commit/ef868447bc)]
- **src**: fix pointer alignment (jhofstee)
[#&#8203;61336](https://redirect.github.com/nodejs/node/pull/61336)
-
\[[`a96256524c`](https://redirect.github.com/nodejs/node/commit/a96256524c)]
- **src**: dump snapshot source with
node:generate\_default\_snapshot\_source (Joyee Cheung)
[#&#8203;61101](https://redirect.github.com/nodejs/node/pull/61101)
-
\[[`ec051b9efd`](https://redirect.github.com/nodejs/node/commit/ec051b9efd)]
- **src**: add HandleScope to edge loop in heap\_utils (Mert Can Altin)
[#&#8203;60885](https://redirect.github.com/nodejs/node/pull/60885)
-
\[[`41749eb5d6`](https://redirect.github.com/nodejs/node/commit/41749eb5d6)]
- **src**: remove redundant CHECK (Tobias Nießen)
[#&#8203;61130](https://redirect.github.com/nodejs/node/pull/61130)
-
\[[`57c81e5af3`](https://redirect.github.com/nodejs/node/commit/57c81e5af3)]
- **src**: fix off-thread cert loading in bundled cert mode (Joyee
Cheung)
[#&#8203;60764](https://redirect.github.com/nodejs/node/pull/60764)
-
\[[`4b0616e024`](https://redirect.github.com/nodejs/node/commit/4b0616e024)]
- **src**: handle DER decoding errors from system certificates (Joyee
Cheung)
[#&#8203;60787](https://redirect.github.com/nodejs/node/pull/60787)
-
\[[`93393371f9`](https://redirect.github.com/nodejs/node/commit/93393371f9)]
- **src**: use static\_cast instead of C-style cast (Michaël Zasso)
[#&#8203;60868](https://redirect.github.com/nodejs/node/pull/60868)
-
\[[`900445b655`](https://redirect.github.com/nodejs/node/commit/900445b655)]
- **src**: move Node-API version detection to where it is used (Anna
Henningsen)
[#&#8203;60512](https://redirect.github.com/nodejs/node/pull/60512)
-
\[[`8353a6da2a`](https://redirect.github.com/nodejs/node/commit/8353a6da2a)]
- **src**: avoid C strings in more C++ exception throws (Anna
Henningsen)
[#&#8203;60592](https://redirect.github.com/nodejs/node/pull/60592)
-
\[[`27c860c51f`](https://redirect.github.com/nodejs/node/commit/27c860c51f)]
- **src**: move `napi_addon_register_func` to `node_api_types.h` (Anna
Henningsen)
[#&#8203;60512](https://redirect.github.com/nodejs/node/pull/60512)
-
\[[`e0517752e7`](https://redirect.github.com/nodejs/node/commit/e0517752e7)]
- **src**: remove unconditional NAPI\_EXPERIMENTAL in node.h (Chengzhong
Wu) [#&#8203;60345](https://redirect.github.com/nodejs/node/pull/60345)
-
\[[`21e2a52f8e`](https://redirect.github.com/nodejs/node/commit/21e2a52f8e)]
- **src**: clean up generic counter implementation (Anna Henningsen)
[#&#8203;60447](https://redirect.github.com/nodejs/node/pull/60447)
-
\[[`aed23cb8ca`](https://redirect.github.com/nodejs/node/commit/aed23cb8ca)]
- **src**: add enum handle for ToStringHelper + formatting (Burkov Egor)
[#&#8203;56829](https://redirect.github.com/nodejs/node/pull/56829)
-
\[[`2e93650ebc`](https://redirect.github.com/nodejs/node/commit/2e93650ebc)]
- **src**: fix timing of snapshot serialize callback (Joyee Cheung)
[#&#8203;60434](https://redirect.github.com/nodejs/node/pull/60434)
-
\[[`ece4acc18f`](https://redirect.github.com/nodejs/node/commit/ece4acc18f)]
- **src**: add COUNT\_GENERIC\_USAGE utility for tests (Joyee Cheung)
[#&#8203;60434](https://redirect.github.com/nodejs/node/pull/60434)
-
\[[`31c8e9d9ff`](https://redirect.github.com/nodejs/node/commit/31c8e9d9ff)]
- **src**: use cached primordials\_string (Sohyeon Kim)
[#&#8203;60255](https://redirect.github.com/nodejs/node/pull/60255)
-
\[[`7f0ffddc14`](https://redirect.github.com/nodejs/node/commit/7f0ffddc14)]
- **src**: implement Windows-1252 encoding support and update related
tests (Mert Can Altin)
[#&#8203;60893](https://redirect.github.com/nodejs/node/pull/60893)
-
\[[`c2ba56d6b2`](https://redirect.github.com/nodejs/node/commit/c2ba56d6b2)]
- **src,permission**: fix permission.has on empty param (Rafael Gonzaga)
[#&#8203;60674](https://redirect.github.com/nodejs/node/pull/60674)
-
\[[`e55a2b895a`](https://redirect.github.com/nodejs/node/commit/e55a2b895a)]
- **src,permission**: add debug log on is\_tree\_granted (Rafael
Gonzaga)
[#&#8203;60668](https://redirect.github.com/nodejs/node/pull/60668)
-
\[[`902a78b43c`](https://redirect.github.com/nodejs/node/commit/902a78b43c)]
- **stream**: fix isErrored/isWritable for WritableStreams (René)
[#&#8203;60905](https://redirect.github.com/nodejs/node/pull/60905)
-
\[[`221b77cf41`](https://redirect.github.com/nodejs/node/commit/221b77cf41)]
- **stream**: don't try to read more if reading (Robert Nagy)
[#&#8203;60454](https://redirect.github.com/nodejs/node/pull/60454)
-
\[[`46d12d826f`](https://redirect.github.com/nodejs/node/commit/46d12d826f)]
- **test**: skip strace test with shared openssl (Richard Lau)
[#&#8203;61987](https://redirect.github.com/nodejs/node/pull/61987)
-
\[[`52e6b01a44`](https://redirect.github.com/nodejs/node/commit/52e6b01a44)]
- **test**: mark `test-strace-openat-openssl` as flaky (Antoine du
Hamel)
[#&#8203;61921](https://redirect.github.com/nodejs/node/pull/61921)
-
\[[`4d7468d0e0`](https://redirect.github.com/nodejs/node/commit/4d7468d0e0)]
- **test**: skip --build-sea tests on platforms where SEA is flaky
(Joyee Cheung)
[#&#8203;61504](https://redirect.github.com/nodejs/node/pull/61504)
-
\[[`f604b7ae67`](https://redirect.github.com/nodejs/node/commit/f604b7ae67)]
- **test**: fix flaky debugger test (Ryuhei Shima)
[#&#8203;58324](https://redirect.github.com/nodejs/node/pull/58324)
-
\[[`fc2dc4024b`](https://redirect.github.com/nodejs/node/commit/fc2dc4024b)]
- **test**: ensure removeListener event fires for once() listeners
(sangwook)
[#&#8203;60137](https://redirect.github.com/nodejs/node/pull/60137)
-
\[[`5fba382816`](https://redirect.github.com/nodejs/node/commit/5fba382816)]
- **test**: delay writing the files only on macOS (Luigi Pinca)
[#&#8203;61532](https://redirect.github.com/nodejs/node/pull/61532)
- \[[`85cc9e20e4`](https://red

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My41OS4wIiwidXBkYXRlZEluVmVyIjoiNDMuNTkuMCIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 18:16:47 +08:00
Kenneth Wußmann
0b47f92134 fix(oidc): allow string boolean in email_verified userinfo schema (#14609)
## Why

When using AWS Cognito as OIDC provider, AFFiNE returns a zod parsing
error because AWS returns `email_verified` as a string in the userinfo
response.

```json
{
    "sub": "[UUID]",
    "email_verified": "true",
    "custom:mycustom1": "CustomValue",
    "phone_number_verified": "true",
    "phone_number": "+12065551212",
    "email": "bob@example.com",
    "username": "bob"
}
```

Reference:
https://docs.aws.amazon.com/cognito/latest/developerguide/userinfo-endpoint.html#get-userinfo-response-sample

Error returned in AFFiNE frontend:
```
Validation error, errors: [ { "code": "invalid_type", "expected": "boolean", "received": "string", "path": [ "email_verified" ], "message": "Expected boolean, received string" } ]
```

## What

I'm adjusting the existing `OIDCUserInfoSchema` to allow `z.boolean()`
and `z.enum(['true', 'false', '0', '1', 'yes', 'no'])`.
This matches with [our `extractBoolean` function in the
`OIDCProvider`](82e6239957/packages/backend/server/src/plugins/oauth/providers/oidc.ts (L269-L285)),
which already parses string as booleans in `email_verified`. But because
the userinfo response is parsed with zod first, it's failing before
reaching our `extractBoolean`.

> [!NOTE]
> We are using zod v3. In zod v4 they [added support for
`z.stringbool()`](https://zod.dev/api?id=stringbool) which would make
this easier.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Enhanced OpenID Connect provider authentication to accept flexible
formats for email verification status, including various string
representations alongside boolean values.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-09 02:53:43 +00:00
DarkSky
9c55edeb62 feat(server): adapt gemini3.1 preview (#14583)
#### PR Dependency Tree


* **PR #14583** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Gemini 3.1 Pro Preview support (text, image, audio) and new
GPT‑5 variants as defaults; centralized persistent telemetry state for
more reliable client identity.

* **UX**
  * Improved model submenu placement in chat preferences.
* More robust mindmap parsing, preview, regeneration and replace
behavior.

* **Chores**
  * Bumped AI SDK and related dependencies.

* **Tests**
  * Expanded/updated tests and increased timeouts for flaky flows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-08 00:53:16 +08:00
DarkSky
9742e9735e feat(editor): improve edgeless perf & memory usage (#14591)
#### PR Dependency Tree


* **PR #14591** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* New canvas renderer debug metrics and controls for runtime inspection.
* Mindmap/group reordering now normalizes group targets, improving
reorder consistency.

* **Bug Fixes**
  * Fixed connector behavior for empty/degenerate paths.
* More aggressive viewport invalidation so structural changes display
correctly.
* Improved z-index synchronization during transforms and layer updates.

* **Performance**
* Retained DOM caching for brushes, shapes, and connectors to reduce DOM
churn.
* Targeted canvas refreshes, pooling, and reuse to lower redraw and
memory overhead.

* **Tests**
* Added canvas renderer performance benchmarks and curve edge-case unit
tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-07 09:12:14 +08:00
DarkSky
86d65b2f64 feat(server): add image resize support (#14588)
fix #13842 


#### PR Dependency Tree


* **PR #14588** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Images are now processed natively and converted to WebP for smaller,
optimized files; Copilot and avatar attachments use the processed WebP
output.
* Avatar uploads accept BMP, GIF, JPEG, PNG, WebP (5MB max) and are
downscaled to a standard edge.

* **Error Messages / i18n**
  * Added localized error "Image format not supported: {format}".

* **Tests**
* Added end-to-end and unit tests for conversion, EXIF preservation, and
upload limits.

* **Chores**
  * Added native image-processing dependencies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-07 04:42:12 +08:00
DarkSky
f34e25e122 test: migrate test & utils (#14569)
#### PR Dependency Tree


* **PR #14569** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Upgraded development test tooling to Vitest v4 and added Playwright
browser test integration; normalized test configurations and CI shard
matrix.

* **Tests**
* Added a large suite of new integration tests covering editor flows
(edgeless, database, embeds, images, latex, code, clipboard,
multi-editor, presentation, undo/redo, etc.).
* Removed numerous end-to-end Playwright test suites across the same
feature areas.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-07 04:12:27 +08:00
DarkSky
b5d5b71f95 feat(server): improve markdown parse (#14580)
#### PR Dependency Tree


* **PR #14580** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Markdown conversion now reports lists of known-unsupported and unknown
block identifiers encountered during parsing, and separates them from
the main markdown output.

* **Bug Fixes**
  * Improved error handling and logging around markdown parsing.

* **Tests**
* Updated tests and snapshots to reflect the new block-list fields and
the adjusted markdown output.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-07 00:14:27 +08:00
renovate[bot]
09fa1a8e4e chore: bump up dompurify version to v3.3.2 [SECURITY] (#14581)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [dompurify](https://redirect.github.com/cure53/DOMPurify) | [`3.3.0` →
`3.3.2`](https://renovatebot.com/diffs/npm/dompurify/3.3.0/3.3.2) |
![age](https://developer.mend.io/api/mc/badges/age/npm/dompurify/3.3.2?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/dompurify/3.3.0/3.3.2?slim=true)
|

### GitHub Vulnerability Alerts

#### [CVE-2026-0540](https://nvd.nist.gov/vuln/detail/CVE-2026-0540)

DOMPurify 3.1.3 through 3.3.1 and 2.5.3 through 2.5.8, fixed in 2.5.9
and 3.3.2, contain a cross-site scripting vulnerability that allows
attackers to bypass attribute sanitization by exploiting five missing
rawtext elements (noscript, xmp, noembed, noframes, iframe) in the
`SAFE_FOR_XML` regex. Attackers can include payloads like
`</noscript><img src=x onerror=alert(1)>` in attribute values to execute
JavaScript when sanitized output is placed inside these unprotected
rawtext contexts.

---

### Release Notes

<details>
<summary>cure53/DOMPurify (dompurify)</summary>

###
[`v3.3.2`](https://redirect.github.com/cure53/DOMPurify/releases/tag/3.3.2):
DOMPurify 3.3.2

[Compare
Source](https://redirect.github.com/cure53/DOMPurify/compare/3.3.1...3.3.2)

- Fixed a possible bypass caused by jsdom's faulty raw-text tag parsing,
thanks multiple reporters
- Fixed a prototype pollution issue when working with custom elements,
thanks [@&#8203;christos-eth](https://redirect.github.com/christos-eth)
- Fixed a lenient config parsing in `_isValidAttribute`, thanks
[@&#8203;christos-eth](https://redirect.github.com/christos-eth)
- Bumped and removed several dependencies, thanks
[@&#8203;Rotzbua](https://redirect.github.com/Rotzbua)
- Fixed the test suite after bumping dependencies, thanks
[@&#8203;Rotzbua](https://redirect.github.com/Rotzbua)

###
[`v3.3.1`](https://redirect.github.com/cure53/DOMPurify/releases/tag/3.3.1):
DOMPurify 3.3.1

[Compare
Source](https://redirect.github.com/cure53/DOMPurify/compare/3.3.0...3.3.1)

- Updated `ADD_FORBID_CONTENTS` setting to extend default list, thanks
[@&#8203;MariusRumpf](https://redirect.github.com/MariusRumpf)
- Updated the ESM import syntax to be more correct, thanks
[@&#8203;binhpv](https://redirect.github.com/binhpv)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no
schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My41Ni4wIiwidXBkYXRlZEluVmVyIjoiNDMuNTYuMCIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 19:04:08 +08:00
congzhou09
c249011238 fix(editor): note-edgeless-block dimensions mismatch content at non-100% scale (#14577)
### Problem
●In edgeless mode, when the `note-edgeless-block` is scaled below 100%,
its outer dimension becomes larger than its content region. This extra
invisible region will block some user interactions such as clicks and
hovers on editing elements underneath.

<img width="1060" height="541" alt="note-elem-block-click"
src="https://github.com/user-attachments/assets/860d7a4f-d159-437b-bbe8-4560e2463e3d"
/>

●The following video demonstrates this issue:


https://github.com/user-attachments/assets/3b719b25-0d7e-496b-9507-6aa65ed0a797


### Solution
●The root cause is that `transform: scale(...)` CSS property (which
implements the scale) is currently applyed to its **inner root element**
instead of itself, and the solution is to move this CSS property to the
proper place.

### After
●The video below shows the behavior after this fix.



https://github.com/user-attachments/assets/e2dbd75d-c2ea-460d-90a1-5cc13e12d5b8



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Centralized CSS scaling for graphics and edgeless note blocks into
dedicated public methods; rendering now uses these methods instead of
inline transform calculations.
* **Tests**
* Updated end-to-end checks to read scale directly from the edgeless
note element and use a more flexible transform-matching pattern.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-06 19:03:36 +08:00
DarkSky
7f5f7e79df feat(server): refactor mcp (#14579)
#### PR Dependency Tree


* **PR #14579** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Full JSON-RPC MCP endpoint with batch requests, per-message
validation, method dispatch (initialize, ping, tools/list, tools/call)
and request cancellation
* Tool listing and execution with input validation, standardized
results, and improved error responses

* **Chores**
  * Removed an external protocol dependency
  * Bumped MCP server version to 1.0.1
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-06 06:35:34 +08:00
DarkSky
fff04395bc chore: update docs 2026-03-06 01:51:09 +08:00
renovate[bot]
bbc01533d7 chore: bump up multer version to v2.1.1 [SECURITY] (#14576)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [multer](https://redirect.github.com/expressjs/multer) | [`2.1.0` →
`2.1.1`](https://renovatebot.com/diffs/npm/multer/2.1.0/2.1.1) |
![age](https://developer.mend.io/api/mc/badges/age/npm/multer/2.1.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/multer/2.1.0/2.1.1?slim=true)
|

### GitHub Vulnerability Alerts

####
[CVE-2026-2359](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-v52c-386h-88mc)

### Impact

A vulnerability in Multer versions < 2.1.0 allows an attacker to trigger
a Denial of Service (DoS) by dropping connection during file upload,
potentially causing resource exhaustion.

### Patches

Users should upgrade to `2.1.0`

### Workarounds

None

####
[CVE-2026-3304](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-xf7r-hgr6-v32p)

### Impact

A vulnerability in Multer versions < 2.1.0 allows an attacker to trigger
a Denial of Service (DoS) by sending malformed requests, potentially
causing resource exhaustion.

### Patches

Users should upgrade to `2.1.0`

### Workarounds

None

####
[CVE-2026-3520](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-5528-5vmv-3xc2)

### Impact

A vulnerability in Multer versions < 2.1.1 allows an attacker to trigger
a Denial of Service (DoS) by sending malformed requests, potentially
causing stack overflow.

### Patches

Users should upgrade to `2.1.1`

### Workarounds

None

### Resources

-
https://github.com/expressjs/multer/security/advisories/GHSA-5528-5vmv-3xc2
- https://www.cve.org/CVERecord?id=CVE-2026-3520
-
7e66481f8b
- https://cna.openjsf.org/security-advisories.html

---

### Release Notes

<details>
<summary>expressjs/multer (multer)</summary>

###
[`v2.1.1`](https://redirect.github.com/expressjs/multer/blob/HEAD/CHANGELOG.md#211)

[Compare
Source](https://redirect.github.com/expressjs/multer/compare/v2.1.0...v2.1.1)

- Fix [CVE-2026-3520](https://www.cve.org/CVERecord?id=CVE-2026-3520)
([GHSA-5528-5vmv-3xc2](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-5528-5vmv-3xc2))
- fix error/abort handling

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no
schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My41NS40IiwidXBkYXRlZEluVmVyIjoiNDMuNTUuNCIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 01:04:48 +08:00
congzhou09
e31cca3354 fix(editor): non-canvas block size/position in embed-edgeless-doc at non-1 zoom (#14074)
### Problem
●Similar to
[PR#14015](https://github.com/toeverything/AFFiNE/pull/14015), the
container's own scaling factor (`viewScale`) was not taken into account.
This time the issue affects **non-canvas blocks** (e.g. `edgeless-note`,
`edgeless-image`, and any component extending `GfxBlockComponent`).
●The follwing image and video show the case when zoom is 0.5.

<img width="822" height="414" alt="图片"
src="https://github.com/user-attachments/assets/cee1cb88-2764-443c-aa7a-0443308b0e29"
/>


https://github.com/user-attachments/assets/3c744579-16c4-4f10-b421-e0606da1269f

### Solution
●Incorporated `viewScale` into the CSS `translate` calculation for all
`GfxBlockComponent` instances.

### Additional Improvement
●Minor refactor: the class returned by `toGfxBlockComponent()` now
reuses the original `getCSSTransform()` implementation from
`GfxBlockComponent.prototype` via `.call(this)`, eliminating duplicated
code.

### After
●The refined is as follows.


https://github.com/user-attachments/assets/24de0429-63a3-45a7-9b31-d91a4279e233


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Improved viewport scaling so visual transforms (translation and zoom)
correctly account for view scale, yielding more consistent rendering
during zoom and pan.
* Centralized transform calculation to a shared implementation, reducing
duplication and ensuring uniform behavior across views.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2026-03-04 11:38:09 +00:00
DarkSky
11bc333714 Merge commit '99b07c2ee14dea1383ca6cd63633c3df542f2955' into canary 2026-03-04 01:18:30 +08:00
renovate[bot]
fc9b99cd17 chore: bump up ava version to v7 (#14563)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [ava](https://avajs.dev)
([source](https://redirect.github.com/avajs/ava)) | [`^6.4.1` →
`^7.0.0`](https://renovatebot.com/diffs/npm/ava/6.4.1/7.0.0) |
![age](https://developer.mend.io/api/mc/badges/age/npm/ava/7.0.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ava/6.4.1/7.0.0?slim=true)
|
| [ava](https://avajs.dev)
([source](https://redirect.github.com/avajs/ava)) | [`^6.4.0` →
`^7.0.0`](https://renovatebot.com/diffs/npm/ava/6.4.1/7.0.0) |
![age](https://developer.mend.io/api/mc/badges/age/npm/ava/7.0.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ava/6.4.1/7.0.0?slim=true)
|

---

### Release Notes

<details>
<summary>avajs/ava (ava)</summary>

###
[`v7.0.0`](https://redirect.github.com/avajs/ava/releases/tag/v7.0.0)

[Compare
Source](https://redirect.github.com/avajs/ava/compare/v6.4.1...v7.0.0)

##### What's Changed

- Replace `strip-ansi` with `node:util.stripVTControlCharacters` by
[@&#8203;fisker](https://redirect.github.com/fisker) in
[#&#8203;3403](https://redirect.github.com/avajs/ava/pull/3403)
- Remove support for Node.js 18 and 23; require 20.19 or newer, 22.20 or
newer or 24,12 or newer; update dependencies including transitive `glob`
by [@&#8203;novemberborn](https://redirect.github.com/novemberborn) in
[#&#8203;3416](https://redirect.github.com/avajs/ava/pull/3416)

**Full Changelog**:
<https://github.com/avajs/ava/compare/v6.4.1...v7.0.0>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40OC4xIiwidXBkYXRlZEluVmVyIjoiNDMuNDguMSIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 17:47:47 +08:00
DarkSky
2137f68871 fix(server): normalize mail server name (#14564)
fix #14562 
fix #14226 
fix #14192


#### PR Dependency Tree


* **PR #14564** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* SMTP and fallback SMTP name now default to empty and will use the
system hostname when not set.
* HELO hostname resolution includes stricter normalization/validation
for more reliable mail handshakes.

* **Documentation**
* Updated admin and config descriptions to explain hostname/HELO
behavior and fallback.

* **Tests**
* Added tests covering hostname normalization and rejection of invalid
HELO values.

* **Chores**
  * Updated example env and ignore rules.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-03 15:51:32 +08:00
renovate[bot]
75efa854bf chore: bump up apple-actions/import-codesign-certs action to v6 (#14561)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[apple-actions/import-codesign-certs](https://redirect.github.com/apple-actions/import-codesign-certs)
| action | major | `v5` → `v6` |

---

### Release Notes

<details>
<summary>apple-actions/import-codesign-certs
(apple-actions/import-codesign-certs)</summary>

###
[`v6`](https://redirect.github.com/apple-actions/import-codesign-certs/compare/v5...v6)

[Compare
Source](https://redirect.github.com/apple-actions/import-codesign-certs/compare/v5...v6)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 12:14:10 +08:00
renovate[bot]
c0139abf79 chore: bump up actions/setup-java action to v5 (#14554)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-java](https://redirect.github.com/actions/setup-java) |
action | major | `v4` → `v5` |

---

### Release Notes

<details>
<summary>actions/setup-java (actions/setup-java)</summary>

###
[`v5`](https://redirect.github.com/actions/setup-java/compare/v4...v5)

[Compare
Source](https://redirect.github.com/actions/setup-java/compare/v4...v5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 20:31:47 +08:00
renovate[bot]
5a38e765bd chore: bump up actions/setup-node action to v6 (#14555)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-node](https://redirect.github.com/actions/setup-node) |
action | major | `v4` → `v6` |

---

### Release Notes

<details>
<summary>actions/setup-node (actions/setup-node)</summary>

###
[`v6`](https://redirect.github.com/actions/setup-node/compare/v5...v6)

[Compare
Source](https://redirect.github.com/actions/setup-node/compare/v5...v6)

###
[`v5`](https://redirect.github.com/actions/setup-node/compare/v4...v5)

[Compare
Source](https://redirect.github.com/actions/setup-node/compare/v4...v5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 20:31:23 +08:00
renovate[bot]
d3dcdd47ee chore: bump up actions/setup-python action to v6 (#14556)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/setup-python](https://redirect.github.com/actions/setup-python)
| action | major | `v5` → `v6` |

---

### Release Notes

<details>
<summary>actions/setup-python (actions/setup-python)</summary>

###
[`v6`](https://redirect.github.com/actions/setup-python/compare/v5...v6)

[Compare
Source](https://redirect.github.com/actions/setup-python/compare/v5...v6)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 20:31:01 +08:00
DarkSky
727c9d6d71 fix: server build env 2026-03-02 20:23:00 +08:00
DarkSky
274f491e49 fix: aarch64 build 2026-03-02 19:49:52 +08:00
congzhou09
478138493a fix(editor): invalid caret in note-edgeless-block on focus (#14229)
### Problem
●In edgeless mode, when starting to edit, `note-block` exhibits two
types of invalid caret behavior:
(1)**Title Region Misalignment**: Clicking on the title region
incorrectly generates the caret in the first line of the note content,
rather than in the title itself.
(2)**Vanishing Caret at Line End**: When clicking in the empty space
beyond the end of a text section, the caret appears momentarily at the
line's end but disappears immediately.
●The following video demonstrates these issues:


https://github.com/user-attachments/assets/db9c2c50-709f-4d32-912c-0f01841d2024


### Solution
●**Title Click Interception**: Added a check to determine if the click
coordinates fall in the title region. If so, the caret positioning is
now handled by a dedicated logic path. Otherwise, it falls back to the
existing note-content logic as before.
●**Range Normalization**: When the generated `range.startContainer` is
not a `TextNode`, try to find a most appropriate `TextNode` and update
the `range` accordingly.

### After
●The video below shows the behavior after this fix.


https://github.com/user-attachments/assets/b2f70b64-1fc6-4049-8379-8bcf3a488a05



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Clicking a page block title no longer creates unwanted paragraphs and
reliably focuses the title.
* Paragraph creation now occurs only when needed and focus is applied
only after successful creation.
* Click coordinates are clamped to container bounds to prevent misplaced
cursors or focus.

* **Improvements**
* Caret normalization: clicks place the caret at the last meaningful
text position for consistent single-cursor behavior.

* **Tests**
  * Added end-to-end coverage for caret placement and focus transitions.
* New ratio-based click/double-click test utilities and a helper for
double-clicking note bodies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 18:51:23 +08:00
renovate[bot]
5464d1a9ce chore: bump up multer version to v2.1.0 [SECURITY] (#14544)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [multer](https://redirect.github.com/expressjs/multer) | [`2.0.2` →
`2.1.0`](https://renovatebot.com/diffs/npm/multer/2.0.2/2.1.0) |
![age](https://developer.mend.io/api/mc/badges/age/npm/multer/2.1.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/multer/2.0.2/2.1.0?slim=true)
|

### GitHub Vulnerability Alerts

####
[CVE-2026-2359](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-v52c-386h-88mc)

### Impact

A vulnerability in Multer versions < 2.1.0 allows an attacker to trigger
a Denial of Service (DoS) by dropping connection during file upload,
potentially causing resource exhaustion.

### Patches

Users should upgrade to `2.1.0`

### Workarounds

None

####
[CVE-2026-3304](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-xf7r-hgr6-v32p)

### Impact

A vulnerability in Multer versions < 2.1.0 allows an attacker to trigger
a Denial of Service (DoS) by sending malformed requests, potentially
causing resource exhaustion.

### Patches

Users should upgrade to `2.1.0`

### Workarounds

None

---

### Release Notes

<details>
<summary>expressjs/multer (multer)</summary>

###
[`v2.1.0`](https://redirect.github.com/expressjs/multer/blob/HEAD/CHANGELOG.md#210)

[Compare
Source](https://redirect.github.com/expressjs/multer/compare/v2.0.2...v2.1.0)

- Add `defParamCharset` option for UTF-8 filename support
([#&#8203;1210](https://redirect.github.com/expressjs/multer/pull/1210))
- Fix [CVE-2026-2359](https://www.cve.org/CVERecord?id=CVE-2026-2359)
([GHSA-v52c-386h-88mc](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-v52c-386h-88mc))
- Fix [CVE-2026-3304](https://www.cve.org/CVERecord?id=CVE-2026-3304)
([GHSA-xf7r-hgr6-v32p](https://redirect.github.com/expressjs/multer/security/advisories/GHSA-xf7r-hgr6-v32p))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no
schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 13:59:38 +08:00
renovate[bot]
4c40dcacd9 chore: bump up actions/setup-go action to v6 (#14553)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-go](https://redirect.github.com/actions/setup-go) |
action | major | `v5` → `v6` |

---

### Release Notes

<details>
<summary>actions/setup-go (actions/setup-go)</summary>

### [`v6`](https://redirect.github.com/actions/setup-go/compare/v5...v6)

[Compare
Source](https://redirect.github.com/actions/setup-go/compare/v5...v6)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 13:59:16 +08:00
renovate[bot]
76d28aaa38 chore: bump up @types/supertest version to v7 (#14546)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[@types/supertest](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/supertest)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/supertest))
| [`^6.0.2` →
`^7.0.0`](https://renovatebot.com/diffs/npm/@types%2fsupertest/6.0.3/7.2.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fsupertest/7.2.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fsupertest/6.0.3/7.2.0?slim=true)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 13:58:48 +08:00
renovate[bot]
86f48240ce chore: bump up actions/github-script action to v8 (#14551)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/github-script](https://redirect.github.com/actions/github-script)
| action | major | `v7` → `v8` |

---

### Release Notes

<details>
<summary>actions/github-script (actions/github-script)</summary>

###
[`v8`](https://redirect.github.com/actions/github-script/releases/tag/v8):
.0.0

[Compare
Source](https://redirect.github.com/actions/github-script/compare/v7...v8)

##### What's Changed

- Update Node.js version support to 24.x by
[@&#8203;salmanmkc](https://redirect.github.com/salmanmkc) in
[#&#8203;637](https://redirect.github.com/actions/github-script/pull/637)
- README for updating actions/github-script from v7 to v8 by
[@&#8203;sneha-krip](https://redirect.github.com/sneha-krip) in
[#&#8203;653](https://redirect.github.com/actions/github-script/pull/653)

##### ⚠️ Minimum Compatible Runner Version

**v2.327.1**\
[Release
Notes](https://redirect.github.com/actions/runner/releases/tag/v2.327.1)

Make sure your runner is updated to this version or newer to use this
release.

##### New Contributors

- [@&#8203;salmanmkc](https://redirect.github.com/salmanmkc) made their
first contribution in
[#&#8203;637](https://redirect.github.com/actions/github-script/pull/637)
- [@&#8203;sneha-krip](https://redirect.github.com/sneha-krip) made
their first contribution in
[#&#8203;653](https://redirect.github.com/actions/github-script/pull/653)

**Full Changelog**:
<https://github.com/actions/github-script/compare/v7.1.0...v8.0.0>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 13:58:23 +08:00
DarkSky
c5d622531c feat: refactor copilot module (#14537) 2026-03-02 13:57:55 +08:00
renovate[bot]
60acd81d4b chore: bump up actions/labeler action to v6 (#14552)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/labeler](https://redirect.github.com/actions/labeler) |
action | major | `v5` → `v6` |

---

### Release Notes

<details>
<summary>actions/labeler (actions/labeler)</summary>

### [`v6`](https://redirect.github.com/actions/labeler/compare/v5...v6)

[Compare
Source](https://redirect.github.com/actions/labeler/compare/v5...v6)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 11:59:25 +08:00
renovate[bot]
78f567a178 chore: bump up actions/checkout action to v6 (#14550)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://redirect.github.com/actions/checkout) |
action | major | `v4` → `v6` |

---

### Release Notes

<details>
<summary>actions/checkout (actions/checkout)</summary>

### [`v6`](https://redirect.github.com/actions/checkout/compare/v5...v6)

[Compare
Source](https://redirect.github.com/actions/checkout/compare/v5...v6)

### [`v5`](https://redirect.github.com/actions/checkout/compare/v4...v5)

[Compare
Source](https://redirect.github.com/actions/checkout/compare/v4...v5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 10:58:53 +08:00
renovate[bot]
784382cfb1 chore: bump up actions/cache action to v5 (#14549)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/cache](https://redirect.github.com/actions/cache) | action |
major | `v4` → `v5` |

---

### Release Notes

<details>
<summary>actions/cache (actions/cache)</summary>

### [`v5`](https://redirect.github.com/actions/cache/compare/v4...v5)

[Compare
Source](https://redirect.github.com/actions/cache/compare/v4...v5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 10:58:27 +08:00
renovate[bot]
342451be1b chore: bump up actions/attest-build-provenance action to v4 (#14547)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/attest-build-provenance](https://redirect.github.com/actions/attest-build-provenance)
| action | major | `v2` → `v4` |

---

### Release Notes

<details>
<summary>actions/attest-build-provenance
(actions/attest-build-provenance)</summary>

###
[`v4`](https://redirect.github.com/actions/attest-build-provenance/compare/v3...v4)

[Compare
Source](https://redirect.github.com/actions/attest-build-provenance/compare/v3...v4)

###
[`v3`](https://redirect.github.com/actions/attest-build-provenance/compare/v2...v3)

[Compare
Source](https://redirect.github.com/actions/attest-build-provenance/compare/v2...v3)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 10:44:37 +08:00
renovate[bot]
2b6146727b chore: bump up RevenueCat/purchases-ios-spm version to from: "5.60.0" (#14545) 2026-03-02 08:48:25 +08:00
renovate[bot]
d5245a3273 chore: bump up Recouse/EventSource version to from: "0.1.7" (#14541)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [Recouse/EventSource](https://redirect.github.com/Recouse/EventSource)
| patch | `from: "0.1.5"` → `from: "0.1.7"` |

---

### Release Notes

<details>
<summary>Recouse/EventSource (Recouse/EventSource)</summary>

###
[`v0.1.7`](https://redirect.github.com/Recouse/EventSource/releases/tag/0.1.7)

[Compare
Source](https://redirect.github.com/Recouse/EventSource/compare/0.1.6...0.1.7)

#### What's Changed

- Separate timeout interval values for request and resource by
[@&#8203;Recouse](https://redirect.github.com/Recouse) in
[#&#8203;46](https://redirect.github.com/Recouse/EventSource/pull/46)

**Full Changelog**:
<https://github.com/Recouse/EventSource/compare/0.1.6...0.1.7>

###
[`v0.1.6`](https://redirect.github.com/Recouse/EventSource/releases/tag/0.1.6)

[Compare
Source](https://redirect.github.com/Recouse/EventSource/compare/0.1.5...0.1.6)

#### What's Changed

- Fix visionOS availability error for split(by:) method by
[@&#8203;danielseidl](https://redirect.github.com/danielseidl) in
[#&#8203;45](https://redirect.github.com/Recouse/EventSource/pull/45)

#### New Contributors

- [@&#8203;danielseidl](https://redirect.github.com/danielseidl) made
their first contribution in
[#&#8203;45](https://redirect.github.com/Recouse/EventSource/pull/45)

**Full Changelog**:
<https://github.com/Recouse/EventSource/compare/0.1.5...0.1.6>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 06:26:20 +08:00
renovate[bot]
fff63562b1 chore: bump up Lakr233/MarkdownView version to from: "3.6.3" (#14540)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
|
[Lakr233/MarkdownView](https://redirect.github.com/Lakr233/MarkdownView)
| patch | `from: "3.6.2"` → `from: "3.6.3"` |

---

### Release Notes

<details>
<summary>Lakr233/MarkdownView (Lakr233/MarkdownView)</summary>

###
[`v3.6.3`](https://redirect.github.com/Lakr233/MarkdownView/compare/3.6.2...3.6.3)

[Compare
Source](https://redirect.github.com/Lakr233/MarkdownView/compare/3.6.2...3.6.3)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 06:26:03 +08:00
renovate[bot]
4136abdd97 chore: bump up rustc version to v1.93.1 (#14542)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [rustc](https://redirect.github.com/rust-lang/rust) | patch | `1.93.0`
→ `1.93.1` |

---

### Release Notes

<details>
<summary>rust-lang/rust (rustc)</summary>

###
[`v1.93.1`](https://redirect.github.com/rust-lang/rust/blob/HEAD/RELEASES.md#Version-1931-2026-02-12)

[Compare
Source](https://redirect.github.com/rust-lang/rust/compare/1.93.0...1.93.1)

\===========================

<a id="1.93.1"></a>

- [Don't try to recover keyword as non-keyword
identifier](https://redirect.github.com/rust-lang/rust/pull/150590),
fixing an ICE that especially [affected
rustfmt](https://redirect.github.com/rust-lang/rustfmt/issues/6739).
- [Fix `clippy::panicking_unwrap` false-positive on field access with
implicit
deref](https://redirect.github.com/rust-lang/rust-clippy/pull/16196).
- [Revert "Update wasm-related dependencies in
CI"](https://redirect.github.com/rust-lang/rust/pull/152259), fixing
file descriptor leaks on the `wasm32-wasip2` target.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 06:25:51 +08:00
renovate[bot]
e249e2e884 chore: bump up opentelemetry (#14543)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[@opentelemetry/exporter-prometheus](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-prometheus)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js))
| [`^0.211.0` →
`^0.212.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fexporter-prometheus/0.211.0/0.212.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fexporter-prometheus/0.212.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fexporter-prometheus/0.211.0/0.212.0?slim=true)
|
|
[@opentelemetry/exporter-zipkin](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-exporter-zipkin)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js))
| [`2.5.0` →
`2.5.1`](https://renovatebot.com/diffs/npm/@opentelemetry%2fexporter-zipkin/2.5.0/2.5.1)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fexporter-zipkin/2.5.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fexporter-zipkin/2.5.0/2.5.1?slim=true)
|
|
[@opentelemetry/host-metrics](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/host-metrics#readme)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/host-metrics))
| [`0.38.2` →
`0.38.3`](https://renovatebot.com/diffs/npm/@opentelemetry%2fhost-metrics/0.38.2/0.38.3)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fhost-metrics/0.38.3?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fhost-metrics/0.38.2/0.38.3?slim=true)
|
|
[@opentelemetry/instrumentation](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js))
| [`^0.211.0` →
`^0.212.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation/0.211.0/0.212.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation/0.212.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation/0.211.0/0.212.0?slim=true)
|
|
[@opentelemetry/instrumentation-graphql](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-graphql#readme)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-graphql))
| [`^0.58.0` →
`^0.60.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-graphql/0.58.0/0.60.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-graphql/0.60.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-graphql/0.58.0/0.60.0?slim=true)
|
|
[@opentelemetry/instrumentation-http](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js))
| [`^0.211.0` →
`^0.212.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-http/0.211.0/0.212.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-http/0.212.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-http/0.211.0/0.212.0?slim=true)
|
|
[@opentelemetry/instrumentation-ioredis](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-ioredis#readme)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-ioredis))
| [`^0.59.0` →
`^0.60.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-ioredis/0.59.0/0.60.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-ioredis/0.60.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-ioredis/0.59.0/0.60.0?slim=true)
|
|
[@opentelemetry/instrumentation-nestjs-core](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-nestjs-core#readme)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-nestjs-core))
| [`^0.57.0` →
`^0.58.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-nestjs-core/0.57.0/0.58.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-nestjs-core/0.58.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-nestjs-core/0.57.0/0.58.0?slim=true)
|
|
[@opentelemetry/instrumentation-socket.io](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-socket.io#readme)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-socket.io))
| [`^0.57.0` →
`^0.59.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-socket.io/0.57.0/0.59.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-socket.io/0.59.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-socket.io/0.57.0/0.59.0?slim=true)
|
|
[@opentelemetry/sdk-metrics](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/packages/sdk-metrics)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js))
| [`2.5.0` →
`2.5.1`](https://renovatebot.com/diffs/npm/@opentelemetry%2fsdk-metrics/2.5.0/2.5.1)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fsdk-metrics/2.5.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fsdk-metrics/2.5.0/2.5.1?slim=true)
|
|
[@opentelemetry/sdk-node](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-node)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js))
| [`^0.211.0` →
`^0.212.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fsdk-node/0.211.0/0.212.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fsdk-node/0.212.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fsdk-node/0.211.0/0.212.0?slim=true)
|
|
[@opentelemetry/sdk-trace-node](https://redirect.github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node)
([source](https://redirect.github.com/open-telemetry/opentelemetry-js))
| [`2.5.0` →
`2.5.1`](https://renovatebot.com/diffs/npm/@opentelemetry%2fsdk-trace-node/2.5.0/2.5.1)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fsdk-trace-node/2.5.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fsdk-trace-node/2.5.0/2.5.1?slim=true)
|

---

### Release Notes

<details>
<summary>open-telemetry/opentelemetry-js
(@&#8203;opentelemetry/exporter-prometheus)</summary>

###
[`v0.212.0`](38924cbff2...ad92be4c2c)

[Compare
Source](38924cbff2...ad92be4c2c)

</details>

<details>
<summary>open-telemetry/opentelemetry-js-contrib
(@&#8203;opentelemetry/host-metrics)</summary>

###
[`v0.38.3`](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/host-metrics/CHANGELOG.md#0383-2026-02-25)

[Compare
Source](7a5f3c0a09...630937db15)

##### Bug Fixes

- **instrumentation-host-metrics:** unpin and update to
systeminformation@^5.31.1
([#&#8203;3392](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/issues/3392))
([e4ffdb4](e4ffdb43d1))

</details>

<details>
<summary>open-telemetry/opentelemetry-js-contrib
(@&#8203;opentelemetry/instrumentation-graphql)</summary>

###
[`v0.60.0`](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-graphql/CHANGELOG.md#0600-2026-02-25)

[Compare
Source](0b33a118f2...630937db15)

##### Features

- **instrumentation-graphql:** add parent name in attributes of resolver
span
([#&#8203;3287](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/issues/3287))
([ea2a90a](ea2a90a87b))

###
[`v0.59.0`](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-graphql/CHANGELOG.md#0590-2026-02-16)

[Compare
Source](7a5f3c0a09...0b33a118f2)

##### Features

- **deps:** update deps matching "@&#8203;opentelemetry/\*"
([#&#8203;3383](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/issues/3383))
([d3ac785](d3ac7851d6))

</details>

<details>
<summary>open-telemetry/opentelemetry-js-contrib
(@&#8203;opentelemetry/instrumentation-ioredis)</summary>

###
[`v0.60.0`](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-ioredis/CHANGELOG.md#0600-2026-02-16)

[Compare
Source](7a5f3c0a09...0b33a118f2)

##### Features

- **deps:** update deps matching "@&#8203;opentelemetry/\*"
([#&#8203;3383](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/issues/3383))
([d3ac785](d3ac7851d6))

##### Dependencies

- The following workspace dependencies were updated
  - devDependencies
-
[@&#8203;opentelemetry/contrib-test-utils](https://redirect.github.com/opentelemetry/contrib-test-utils)
bumped from ^0.58.0 to ^0.59.0

</details>

<details>
<summary>open-telemetry/opentelemetry-js-contrib
(@&#8203;opentelemetry/instrumentation-nestjs-core)</summary>

###
[`v0.58.0`](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-nestjs-core/CHANGELOG.md#0580-2026-02-16)

[Compare
Source](7a5f3c0a09...0b33a118f2)

##### Features

- **deps:** update deps matching "@&#8203;opentelemetry/\*"
([#&#8203;3383](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/issues/3383))
([d3ac785](d3ac7851d6))

</details>

<details>
<summary>open-telemetry/opentelemetry-js-contrib
(@&#8203;opentelemetry/instrumentation-socket.io)</summary>

###
[`v0.59.0`](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-socket.io/CHANGELOG.md#0590-2026-02-25)

[Compare
Source](0b33a118f2...630937db15)

##### Features

- **deps:** lock file maintenance
([#&#8203;3261](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/issues/3261))
([540926b](540926bffe))

###
[`v0.58.0`](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/blob/HEAD/packages/instrumentation-socket.io/CHANGELOG.md#0580-2026-02-16)

[Compare
Source](7a5f3c0a09...0b33a118f2)

##### Features

- **deps:** update deps matching "@&#8203;opentelemetry/\*"
([#&#8203;3383](https://redirect.github.com/open-telemetry/opentelemetry-js-contrib/issues/3383))
([d3ac785](d3ac7851d6))

##### Dependencies

- The following workspace dependencies were updated
  - devDependencies
-
[@&#8203;opentelemetry/contrib-test-utils](https://redirect.github.com/opentelemetry/contrib-test-utils)
bumped from ^0.58.0 to ^0.59.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-01 21:19:09 +00:00
GyeongSu Han
2e95d91093 fix: restore Accept header and prevent encoded response in GitHub OAuth token request (regression from #14061) (#14538)
## 📝 Summary

This PR fixes a regression that caused the following error during GitHub
OAuth login:

> Unable to parse JSON response from
[https://github.com/login/oauth/access_token](https://github.com/login/oauth/access_token)

Related issue:
[https://github.com/toeverything/AFFiNE/issues/14334](https://github.com/toeverything/AFFiNE/issues/14334)
Regression introduced in:
[https://github.com/toeverything/AFFiNE/pull/14061](https://github.com/toeverything/AFFiNE/pull/14061)

---

## 🎯 Background

GitHub’s OAuth access token endpoint returns different response formats
depending on the request headers.

To receive a JSON response, the request must include:

```
Accept: application/json
```

If the `Accept` header is missing, GitHub responds with:

```
application/x-www-form-urlencoded
```

The current implementation assumes a JSON response and parses it
directly.
When a non-JSON response is returned, JSON parsing fails,
breaking the OAuth login flow.

---

## 🔍 Traffic Analysis (tcpdump)

Network path:

affine-graphql → (HTTPS) → envoy → (HTTP, tcpdump) → envoy → GitHub

### Observed Request

```
POST /login/oauth/access_token HTTP/1.1
host: github-proxy.com
content-type: application/x-www-form-urlencoded
accept: */*
...
```

### Observed Response

```
HTTP/1.1 200 OK
date: Sat, 28 Feb 2026 14:47:43 GMT
content-type: application/x-www-form-urlencoded; charset=utf-8
...
```

The `Accept` header was `*/*` instead of `application/json`,
causing GitHub to return a form-urlencoded response.

---

## 🐛 Root Cause

PR #14061 introduced a side effect in the request configuration.

Although the `Accept` header was initially defined,
the request options were later overwritten by the `init` parameter.

Because `init.headers` replaced the previously defined headers object,
the required header was lost.

Resulting in:

* Missing `Accept: application/json`
* GitHub returning `application/x-www-form-urlencoded`
* JSON parsing failure
* OAuth login failure

---

## 🔧 Changes

### 1️⃣ Fix header overwrite order

* Process the incoming `init` parameter first
* Explicitly overwrite required headers afterward
* Ensure `Accept: application/json` is always enforced

---

## 💥 Breaking Changes

None.

---

## 🧪 How to Test

1. Configure GitHub OAuth.
2. Attempt login via GitHub.
3. Verify that:

   * The request contains `Accept: application/json`
   * The response content-type is `application/json`
   * No JSON parsing error occurs
   * OAuth login completes successfully

---

## 📌 Notes

This change restores correct OAuth behavior and prevents regression
caused by header overwriting introduced in #14061.

The same header overwrite pattern identified in this issue
was also found in the calendar module and has been corrected there as
well.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Improved backend HTTP header handling for external integrations to
avoid unintended header overrides, ensuring content-type and encoding
hints are applied consistently and improving reliability of service
requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-01 14:32:28 +00:00
DarkSky
2cb171f553 feat: cleanup webpack deps (#14530)
#### PR Dependency Tree


* **PR #14530** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Breaking Changes**
  * Webpack bundler support removed from the build system
* Bundler selection parameter removed from build and development
commands

* **Refactor**
  * Build configuration consolidated to a single bundler approach
* Webpack-specific build paths and workflows removed; development server
simplified

* **Chores**
  * Removed webpack-related dev dependencies and tooling
  * Updated package build scripts for a unified bundle command

* **Dependencies**
* Upgraded Sentry packages across frontend packages
(react/electron/esbuild plugin)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-28 00:24:08 +08:00
DarkSky
a4e2242b8d chore: bump playwright (#13947)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Updated Playwright test tooling to 1.58.2 across the repository and
test packages.

* **Tests**
* Improved end-to-end robustness: replaced fragile timing/coordinate
logic with element-based interactions, added polling/retry checks for
flaky asserts and async state, and simplified input/rename flows to
reduce test flakiness.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-27 22:56:43 +08:00
DarkSky
c90f173821 chore: bump deps (#14526)
#### PR Dependency Tree


* **PR #14526** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated Storybook component development tooling to version 10.2.13 for
improved stability and performance
  * Removed Chromatic integration from the component preview system

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-27 20:17:06 +08:00
315 changed files with 16474 additions and 8566 deletions

View File

@@ -19,3 +19,8 @@ rustflags = [
# pthread_key_create() destructors and segfault after a DSO unloading
[target.'cfg(all(target_env = "gnu", not(target_os = "windows")))']
rustflags = ["-C", "link-args=-Wl,-z,nodelete"]
# Temporary local llm_adapter override.
# Uncomment when verifying AFFiNE against the sibling llm_adapter workspace.
# [patch.crates-io]
# llm_adapter = { path = "../llm_adapter" }

View File

@@ -197,8 +197,8 @@
"properties": {
"SMTP.name": {
"type": "string",
"description": "Name of the email server (e.g. your domain name)\n@default \"AFFiNE Server\"\n@environment `MAILER_SERVERNAME`",
"default": "AFFiNE Server"
"description": "Hostname used for SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.\n@default \"\"\n@environment `MAILER_SERVERNAME`",
"default": ""
},
"SMTP.host": {
"type": "string",
@@ -237,8 +237,8 @@
},
"fallbackSMTP.name": {
"type": "string",
"description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"",
"default": "AFFiNE Server"
"description": "Hostname used for fallback SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.\n@default \"\"",
"default": ""
},
"fallbackSMTP.host": {
"type": "string",
@@ -971,7 +971,7 @@
},
"scenarios": {
"type": "object",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4-5@20250929\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"coding\":\"claude-sonnet-4-5@20250929\",\"complex_text_generation\":\"gpt-5-mini\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"default": {
"override_enabled": false,
"scenarios": {
@@ -979,15 +979,24 @@
"chat": "gemini-2.5-flash",
"embedding": "gemini-embedding-001",
"image": "gpt-image-1",
"rerank": "gpt-4.1",
"coding": "claude-sonnet-4-5@20250929",
"complex_text_generation": "gpt-4o-2024-08-06",
"complex_text_generation": "gpt-5-mini",
"quick_decision_making": "gpt-5-mini",
"quick_text_generation": "gemini-2.5-flash",
"polish_and_summarize": "gemini-2.5-flash"
}
}
},
"providers.profiles": {
"type": "array",
"description": "The profile list for copilot providers.\n@default []",
"default": []
},
"providers.defaults": {
"type": "object",
"description": "The default provider ids for model output types and global fallback.\n@default {}",
"default": {}
},
"providers.openai": {
"type": "object",
"description": "The config for the openai provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.openai.com/v1\"}\n@link https://github.com/openai/openai-node",

View File

@@ -50,8 +50,14 @@ runs:
# https://github.com/tree-sitter/tree-sitter/issues/4186
# pass -D_BSD_SOURCE to clang to fix the tree-sitter build issue
run: |
echo "CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
echo "TARGET_CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
if [[ "${{ inputs.target }}" == "aarch64-unknown-linux-gnu" ]]; then
# napi cross-toolchain 1.0.3 headers miss AT_HWCAP2 in elf.h
echo "CC=clang -D_BSD_SOURCE -DAT_HWCAP2=26" >> "$GITHUB_ENV"
echo "TARGET_CC=clang -D_BSD_SOURCE -DAT_HWCAP2=26" >> "$GITHUB_ENV"
else
echo "CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
echo "TARGET_CC=clang -D_BSD_SOURCE" >> "$GITHUB_ENV"
fi
- name: Cache cargo
uses: Swatinem/rust-cache@v2

View File

@@ -53,7 +53,7 @@ runs:
fi
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
registry-url: https://npm.pkg.github.com
@@ -93,7 +93,7 @@ runs:
run: node -e "const p = $(yarn config cacheFolder --json).effective; console.log('yarn_global_cache=' + p)" >> $GITHUB_OUTPUT
- name: Cache non-full yarn cache on Linux
uses: actions/cache@v4
uses: actions/cache@v5
if: ${{ inputs.full-cache != 'true' && runner.os == 'Linux' }}
with:
path: |
@@ -105,7 +105,7 @@ runs:
# and the decompression performance on Windows is very terrible
# so we reduce the number of cached files on non-Linux systems by remove node_modules from cache path.
- name: Cache non-full yarn cache on non-Linux
uses: actions/cache@v4
uses: actions/cache@v5
if: ${{ inputs.full-cache != 'true' && runner.os != 'Linux' }}
with:
path: |
@@ -113,7 +113,7 @@ runs:
key: node_modules-cache-${{ github.job }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
- name: Cache full yarn cache on Linux
uses: actions/cache@v4
uses: actions/cache@v5
if: ${{ inputs.full-cache == 'true' && runner.os == 'Linux' }}
with:
path: |
@@ -122,7 +122,7 @@ runs:
key: node_modules-cache-full-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
- name: Cache full yarn cache on non-Linux
uses: actions/cache@v4
uses: actions/cache@v5
if: ${{ inputs.full-cache == 'true' && runner.os != 'Linux' }}
with:
path: |
@@ -154,7 +154,7 @@ runs:
# Note: Playwright's cache directory is hard coded because that's what it
# says to do in the docs. There doesn't appear to be a command that prints
# it out for us.
- uses: actions/cache@v4
- uses: actions/cache@v5
id: playwright-cache
if: ${{ inputs.playwright-install == 'true' }}
with:
@@ -189,7 +189,7 @@ runs:
run: |
echo "version=$(yarn why --json electron | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://' | head -n 1)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v5
id: electron-cache
if: ${{ inputs.electron-install == 'true' }}
with:

View File

@@ -31,10 +31,10 @@ podSecurityContext:
resources:
limits:
cpu: '1'
memory: 4Gi
memory: 6Gi
requests:
cpu: '1'
memory: 2Gi
memory: 4Gi
probe:
initialDelaySeconds: 20

View File

@@ -13,5 +13,5 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/labeler@v5
- uses: actions/checkout@v6
- uses: actions/labeler@v6

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -57,7 +57,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -89,7 +89,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -118,7 +118,7 @@ jobs:
build-server-native:
name: Build Server native - ${{ matrix.targets.name }}
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
environment: ${{ inputs.build-type }}
strategy:
fail-fast: false
@@ -132,7 +132,7 @@ jobs:
file: server-native.armv7.node
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -166,7 +166,7 @@ jobs:
needs:
- build-server-native
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -202,7 +202,7 @@ jobs:
- build-mobile
- build-admin
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download server dist
uses: actions/download-artifact@v4
with:
@@ -222,7 +222,7 @@ jobs:
# setup node without cache configuration
# Prisma cache is not compatible with docker build cache
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
registry-url: https://npm.pkg.github.com

View File

@@ -46,7 +46,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
@@ -67,9 +67,9 @@ jobs:
name: Lint
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Go (for actionlint)
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 'stable'
- name: Install actionlint
@@ -111,7 +111,7 @@ jobs:
env:
NODE_OPTIONS: --max-old-space-size=14384
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -138,7 +138,7 @@ jobs:
outputs:
run-rust: ${{ steps.rust-filter.outputs.rust }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: rust-filter
@@ -159,7 +159,7 @@ jobs:
needs:
- rust-test-filter
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: ./.github/actions/build-rust
with:
target: x86_64-unknown-linux-gnu
@@ -182,7 +182,7 @@ jobs:
needs:
- build-server-native
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -212,7 +212,7 @@ jobs:
name: Check yarn binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Run check
run: |
set -euo pipefail
@@ -226,9 +226,9 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2]
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -256,7 +256,7 @@ jobs:
name: E2E BlockSuite Cross Browser Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -282,52 +282,6 @@ jobs:
path: ./test-results
if-no-files-found: ignore
bundler-matrix:
name: Bundler Matrix (${{ matrix.bundler }})
runs-on: ubuntu-24.04-arm
strategy:
fail-fast: false
matrix:
bundler: [webpack, rspack]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: false
electron-install: false
full-cache: true
- name: Run frontend build matrix
env:
AFFINE_BUNDLER: ${{ matrix.bundler }}
run: |
set -euo pipefail
packages=(
"@affine/web"
"@affine/mobile"
"@affine/ios"
"@affine/android"
"@affine/admin"
"@affine/electron-renderer"
)
summary="test-results-bundler-${AFFINE_BUNDLER}.txt"
: > "$summary"
for pkg in "${packages[@]}"; do
start=$(date +%s)
yarn affine "$pkg" build
end=$(date +%s)
echo "${pkg},$((end-start))" >> "$summary"
done
- name: Upload bundler timing
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-bundler-${{ matrix.bundler }}
path: ./test-results-bundler-${{ matrix.bundler }}.txt
if-no-files-found: ignore
e2e-test:
name: E2E Test
runs-on: ubuntu-24.04-arm
@@ -340,7 +294,7 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -372,7 +326,7 @@ jobs:
matrix:
shard: [1, 2]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -402,9 +356,9 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -437,7 +391,7 @@ jobs:
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -476,7 +430,7 @@ jobs:
- { os: macos-latest, target: aarch64-apple-darwin }
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -517,7 +471,7 @@ jobs:
- { os: windows-latest, target: aarch64-pc-windows-msvc }
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: samypr100/setup-dev-drive@v3
with:
workspace-copy: true
@@ -557,7 +511,7 @@ jobs:
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -580,7 +534,7 @@ jobs:
name: Build @affine/electron renderer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -607,7 +561,7 @@ jobs:
needs:
- build-native-linux
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -661,7 +615,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -742,7 +696,7 @@ jobs:
stack-version: 9.0.1
security-enabled: false
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -805,7 +759,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -846,7 +800,7 @@ jobs:
CARGO_TERM_COLOR: always
MIRIFLAGS: -Zmiri-backtrace=full -Zmiri-tree-borrows
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
@@ -874,7 +828,7 @@ jobs:
RUST_BACKTRACE: full
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
@@ -898,7 +852,7 @@ jobs:
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
@@ -937,7 +891,7 @@ jobs:
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Rust
uses: ./.github/actions/build-rust
with:
@@ -960,7 +914,7 @@ jobs:
run-api: ${{ steps.decision.outputs.run_api }}
run-e2e: ${{ steps.decision.outputs.run_e2e }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: copilot-filter
@@ -1029,7 +983,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -1102,7 +1056,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -1185,7 +1139,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -1266,7 +1220,7 @@ jobs:
test: true,
}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10

View File

@@ -10,7 +10,7 @@ jobs:
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -64,7 +64,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -134,7 +134,7 @@ jobs:
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -167,7 +167,7 @@ jobs:
runs-on: ubuntu-latest
name: Post test result message
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js

View File

@@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event.action != 'edited' || github.event.changes.title != null }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
cache: 'yarn'
node-version-file: '.nvmrc'

View File

@@ -35,7 +35,7 @@ jobs:
- build-images
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Deploy to ${{ inputs.build-type }}
uses: ./.github/actions/deploy
with:

View File

@@ -69,7 +69,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ inputs.app_version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
@@ -101,7 +101,7 @@ jobs:
- name: Signing By Apple Developer ID
if: ${{ inputs.platform == 'darwin' && inputs.apple_codesign }}
uses: apple-actions/import-codesign-certs@v5
uses: apple-actions/import-codesign-certs@v6
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
@@ -178,14 +178,14 @@ jobs:
mv packages/frontend/apps/electron/out/*/make/deb/${{ inputs.arch }}/*.deb ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.deb
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ inputs.arch }}.flatpak
- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@v4
if: ${{ inputs.platform == 'darwin' }}
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.zip
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ inputs.arch }}.dmg
- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@v4
if: ${{ inputs.platform == 'linux' }}
with:
subject-path: |

View File

@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -187,7 +187,7 @@ jobs:
FILES_TO_BE_SIGNED_x64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_x64 }}
FILES_TO_BE_SIGNED_arm64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_arm64 }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -344,7 +344,7 @@ jobs:
mv packages/frontend/apps/electron/out/*/make/squirrel.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
mv packages/frontend/apps/electron/out/*/make/nsis.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@v4
with:
subject-path: |
./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
@@ -369,7 +369,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v4
with:
@@ -395,7 +395,7 @@ jobs:
with:
name: affine-linux-x64-builds
path: ./release
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 20
- name: Copy Selfhost Release Files

View File

@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.build-type }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -54,7 +54,7 @@ jobs:
build-android-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -83,7 +83,7 @@ jobs:
needs:
- build-ios-web
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -114,7 +114,7 @@ jobs:
- name: Cap sync
run: yarn workspace @affine/ios sync
- name: Signing By Apple Developer ID
uses: apple-actions/import-codesign-certs@v5
uses: apple-actions/import-codesign-certs@v6
id: import-codesign-certs
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12_MOBILE }}
@@ -147,7 +147,7 @@ jobs:
needs:
- build-android-web
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup Version
uses: ./.github/actions/setup-version
with:
@@ -180,7 +180,7 @@ jobs:
no-build: 'true'
- name: Cap sync
run: yarn workspace @affine/android cap sync
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Auth gcloud
@@ -192,7 +192,7 @@ jobs:
token_format: 'access_token'
project_id: '${{ secrets.GCP_PROJECT_ID }}'
access_token_scopes: 'https://www.googleapis.com/auth/androidpublisher'
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'

View File

@@ -55,7 +55,7 @@ jobs:
GIT_SHORT_HASH: ${{ steps.prepare.outputs.GIT_SHORT_HASH }}
BUILD_TYPE: ${{ steps.prepare.outputs.BUILD_TYPE }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Prepare Release
id: prepare
uses: ./.github/actions/prepare-release
@@ -72,7 +72,7 @@ jobs:
steps:
- name: Decide whether to release
id: decide
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const buildType = '${{ needs.prepare.outputs.BUILD_TYPE }}'

1
.gitignore vendored
View File

@@ -48,6 +48,7 @@ testem.log
/typings
tsconfig.tsbuildinfo
.context
/*.md
# System Files
.DS_Store

2
.nvmrc
View File

@@ -1 +1 @@
22.22.0
22.22.1

327
Cargo.lock generated
View File

@@ -178,9 +178,15 @@ name = "affine_server_native"
version = "1.0.0"
dependencies = [
"affine_common",
"anyhow",
"chrono",
"file-format",
"image",
"infer",
"libwebp-sys",
"little_exif",
"llm_adapter",
"matroska",
"mimalloc",
"mp4parse",
"napi",
@@ -188,6 +194,8 @@ dependencies = [
"napi-derive",
"rand 0.9.2",
"rayon",
"serde",
"serde_json",
"sha3",
"tiktoken-rs",
"tokio",
@@ -232,6 +240,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@@ -245,7 +268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
dependencies = [
"alsa-sys",
"bitflags 2.10.0",
"bitflags 2.11.0",
"cfg-if",
"libc",
]
@@ -533,7 +556,7 @@ version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"cexpr",
"clang-sys",
"itertools 0.13.0",
@@ -583,9 +606,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
dependencies = [
"serde_core",
]
@@ -599,6 +622,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "bitstream-io"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680575de65ce8b916b82a447458b94a48776707d9c2681a9d8da351c06886a1f"
dependencies = [
"core2",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -641,6 +673,27 @@ version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473976d7a8620bb1e06dcdd184407c2363fe4fec8e983ee03ed9197222634a31"
[[package]]
name = "brotli"
version = "8.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.12.1"
@@ -676,6 +729,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -904,6 +963,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.4"
@@ -983,7 +1048,7 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"core-foundation",
"core-graphics-types",
"foreign-types",
@@ -996,7 +1061,7 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"core-foundation",
"core-graphics-types",
"foreign-types",
@@ -1009,7 +1074,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"core-foundation",
"libc",
]
@@ -1379,7 +1444,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"block2",
"libc",
"objc2",
@@ -1562,6 +1627,15 @@ dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "file-format"
version = "0.28.0"
@@ -1833,6 +1907,16 @@ dependencies = [
"wasip2",
]
[[package]]
name = "gif"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "glob"
version = "0.3.3"
@@ -1880,7 +1964,7 @@ version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c43e7c3212bd992c11b6b9796563388170950521ae8487f5cdf6f6e792f1c8"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -2003,6 +2087,22 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "iana-time-zone"
version = "0.1.64"
@@ -2175,6 +2275,34 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"moxcms",
"num-traits",
"png",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
dependencies = [
"byteorder-lite",
"quick-error 2.0.1",
]
[[package]]
name = "include-flate"
version = "0.3.1"
@@ -2376,9 +2504,9 @@ dependencies = [
[[package]]
name = "keccak"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures",
]
@@ -2490,7 +2618,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"libc",
"redox_syscall 0.7.0",
]
@@ -2506,6 +2634,17 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "libwebp-sys"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375ca3fbd6d89769361c5d505c9da676eb4128ee471b9fd763144d377a2d30e6"
dependencies = [
"cc",
"glob",
"pkg-config",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -2518,6 +2657,33 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "little_exif"
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21eeb58b22d31be8dc5c625004fcd4b9b385cd3c05df575f523bcca382c51122"
dependencies = [
"brotli",
"crc",
"log",
"miniz_oxide",
"paste",
"quick-xml",
]
[[package]]
name = "llm_adapter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e98485dda5180cc89b993a001688bed93307be6bd8fedcde445b69bbca4f554d"
dependencies = [
"base64",
"serde",
"serde_json",
"thiserror 2.0.17",
"ureq",
]
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -2555,7 +2721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fa2559e99ba0f26a12458aabc754432c805bbb8cba516c427825a997af1fb7"
dependencies = [
"aes",
"bitflags 2.10.0",
"bitflags 2.11.0",
"cbc",
"ecb",
"encoding_rs",
@@ -2642,6 +2808,16 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "matroska"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde85cd7fb5cf875c4a46fac0cbd6567d413bea2538cef6788e3a0e52a902b45"
dependencies = [
"bitstream-io",
"phf 0.11.3",
]
[[package]]
name = "md-5"
version = "0.10.6"
@@ -2711,6 +2887,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moxcms"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "mp4parse"
version = "0.17.0"
@@ -2741,7 +2927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000f205daae6646003fdc38517be6232af2b150bad4b67bdaf4c5aadb119d738"
dependencies = [
"anyhow",
"bitflags 2.10.0",
"bitflags 2.11.0",
"chrono",
"ctor",
"futures",
@@ -2801,7 +2987,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"jni-sys",
"log",
"ndk-sys",
@@ -2836,7 +3022,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -3000,7 +3186,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"dispatch2",
"objc2",
]
@@ -3017,7 +3203,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"block2",
"libc",
"objc2",
@@ -3123,6 +3309,12 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "path-ext"
version = "0.1.2"
@@ -3369,6 +3561,19 @@ dependencies = [
"plotters-backend",
]
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags 2.11.0",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "pom"
version = "1.1.0"
@@ -3469,7 +3674,7 @@ checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40"
dependencies = [
"bit-set 0.8.0",
"bit-vec 0.8.0",
"bitflags 2.10.0",
"bitflags 2.11.0",
"num-traits",
"rand 0.9.2",
"rand_chacha 0.9.0",
@@ -3497,7 +3702,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"getopts",
"memchr",
"pulldown-cmark-escape",
@@ -3510,12 +3715,33 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "pxfm"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.43"
@@ -3663,7 +3889,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
]
[[package]]
@@ -3672,7 +3898,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
]
[[package]]
@@ -3831,7 +4057,7 @@ version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags 2.10.0",
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys",
@@ -3844,6 +4070,7 @@ version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
@@ -3885,7 +4112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
dependencies = [
"fnv",
"quick-error",
"quick-error 1.2.3",
"tempfile",
"wait-timeout",
]
@@ -4269,7 +4496,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"bitflags 2.10.0",
"bitflags 2.11.0",
"byteorder",
"bytes",
"chrono",
@@ -4312,7 +4539,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"bitflags 2.10.0",
"bitflags 2.11.0",
"byteorder",
"chrono",
"crc",
@@ -4912,9 +5139,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.6+spec-1.1.0"
version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
"winnow",
]
@@ -5345,6 +5572,35 @@ version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "ureq"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc"
dependencies = [
"base64",
"flate2",
"log",
"percent-encoding",
"rustls",
"rustls-pki-types",
"ureq-proto",
"utf-8",
"webpki-roots 1.0.5",
]
[[package]]
name = "ureq-proto"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
dependencies = [
"base64",
"http",
"httparse",
"log",
]
[[package]]
name = "url"
version = "2.5.8"
@@ -6310,3 +6566,18 @@ dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zune-core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-jpeg"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
dependencies = [
"zune-core",
]

View File

@@ -40,13 +40,24 @@ resolver = "3"
dotenvy = "0.15"
file-format = { version = "0.28", features = ["reader"] }
homedir = "0.3"
image = { version = "0.25.9", default-features = false, features = [
"bmp",
"gif",
"jpeg",
"png",
"webp",
] }
infer = { version = "0.19.0" }
lasso = { version = "0.7", features = ["multi-threaded"] }
lib0 = { version = "0.16", features = ["lib0-serde"] }
libc = "0.2"
libwebp-sys = "0.14.2"
little_exif = "0.6.23"
llm_adapter = { version = "0.1.3", default-features = false }
log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] }
lru = "0.16"
matroska = "0.30"
memory-indexer = "0.3.0"
mimalloc = "0.1"
mp4parse = "0.17"

View File

@@ -23,4 +23,6 @@ We welcome you to provide us with bug reports via and email at [security@toevery
Since we are an open source project, we also welcome you to provide corresponding fix PRs, we will determine specific rewards based on the evaluation results.
Due to limited resources, we do not accept and will not review any AI-generated security reports.
If the vulnerability is caused by a library we depend on, we encourage you to submit a security report to the corresponding dependent library at the same time to benefit more users.

View File

@@ -300,6 +300,6 @@
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"msw": "^2.12.4",
"vitest": "^3.2.4"
"vitest": "^4.0.18"
}
}

View File

@@ -11,7 +11,7 @@ export default defineConfig({
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 1000,
coverage: {
provider: 'istanbul', // or 'c8'
provider: 'istanbul',
reporter: ['lcov'],
reportsDirectory: '../../../.coverage/blocksuite-affine',
},

View File

@@ -31,7 +31,8 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"@vitest/browser-playwright": "^4.0.18",
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -108,7 +108,9 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
}
open = () => {
window.open(this.link, '_blank');
const link = this.link;
if (!link) return;
window.open(link, '_blank', 'noopener,noreferrer');
};
refreshData = () => {

View File

@@ -1,3 +1,4 @@
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
@@ -8,10 +9,9 @@ export default defineConfig({
browser: {
enabled: true,
headless: true,
name: 'chromium',
provider: 'playwright',
instances: [{ browser: 'chromium' }],
provider: playwright(),
isolate: false,
providerOptions: {},
},
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 500,

View File

@@ -45,8 +45,10 @@ export class AffineCodeUnit extends ShadowlessElement {
if (!codeBlock || !vElement) return plainContent;
const tokens = codeBlock.highlightTokens$.value;
if (tokens.length === 0) return plainContent;
const line = tokens[vElement.lineIndex];
if (!line) return plainContent;
// copy the tokens to avoid modifying the original tokens
const lineTokens = structuredClone(tokens[vElement.lineIndex]);
const lineTokens = structuredClone(line);
if (lineTokens.length === 0) return plainContent;
const startOffset = vElement.startOffset;

View File

@@ -35,7 +35,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -35,7 +35,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -31,7 +31,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -221,6 +221,12 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
}
}
override getCSSScaleVal(): number {
const baseScale = super.getCSSScaleVal();
const extraScale = this.model.props.edgeless?.scale ?? 1;
return baseScale * extraScale;
}
override getRenderingRect() {
const { xywh, edgeless } = this.model.props;
const { collapse, scale = 1 } = edgeless;
@@ -255,7 +261,6 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
const style = {
borderRadius: borderRadius + 'px',
transform: `scale(${scale})`,
};
const extra = this._editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
@@ -454,6 +459,28 @@ export const EdgelessNoteInteraction =
return;
}
let isClickOnTitle = false;
const titleRect = view
.querySelector('edgeless-page-block-title')
?.getBoundingClientRect();
if (titleRect) {
const titleBound = new Bound(
titleRect.x,
titleRect.y,
titleRect.width,
titleRect.height
);
if (titleBound.isPointInBound([e.clientX, e.clientY])) {
isClickOnTitle = true;
}
}
if (isClickOnTitle) {
handleNativeRangeAtPoint(e.clientX, e.clientY);
return;
}
if (model.children.length === 0) {
const blockId = std.store.addBlock(
'affine:paragraph',

View File

@@ -22,6 +22,7 @@ import {
FrameBlockModel,
ImageBlockModel,
isExternalEmbedModel,
MindmapElementModel,
NoteBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
@@ -401,7 +402,17 @@ function reorderElements(
) {
if (!models.length) return;
for (const model of models) {
const normalizedModels = Array.from(
new Map(
models.map(model => {
const reorderTarget =
model.group instanceof MindmapElementModel ? model.group : model;
return [reorderTarget.id, reorderTarget];
})
).values()
);
for (const model of normalizedModels) {
const index = ctx.gfx.layer.getReorderedIndex(model, type);
// block should be updated in transaction

View File

@@ -33,7 +33,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -2,16 +2,24 @@ import { type Color, ColorScheme } from '@blocksuite/affine-model';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { IBound } from '@blocksuite/global/gfx';
import { getBoundWithRotation, intersects } from '@blocksuite/global/gfx';
import {
Bound,
getBoundWithRotation,
type IBound,
intersects,
} from '@blocksuite/global/gfx';
import type { BlockStdScope } from '@blocksuite/std';
import type {
GfxCompatibleInterface,
GfxController,
GfxLocalElementModel,
GridManager,
LayerManager,
SurfaceBlockModel,
Viewport,
} from '@blocksuite/std/gfx';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { effect } from '@preact/signals-core';
import last from 'lodash-es/last';
import { Subject } from 'rxjs';
@@ -40,11 +48,82 @@ type RendererOptions = {
surfaceModel: SurfaceBlockModel;
};
export type CanvasRenderPassMetrics = {
overlayCount: number;
placeholderElementCount: number;
renderByBoundCallCount: number;
renderedElementCount: number;
visibleElementCount: number;
};
export type CanvasMemorySnapshot = {
bytes: number;
datasetLayerId: string | null;
height: number;
kind: 'main' | 'stacking';
width: number;
zIndex: string;
};
export type CanvasRendererDebugMetrics = {
canvasLayerCount: number;
canvasMemoryBytes: number;
canvasMemorySnapshots: CanvasMemorySnapshot[];
canvasMemoryMegabytes: number;
canvasPixelCount: number;
coalescedRefreshCount: number;
dirtyLayerRenderCount: number;
fallbackElementCount: number;
lastRenderDurationMs: number;
lastRenderMetrics: CanvasRenderPassMetrics;
maxRenderDurationMs: number;
pooledStackingCanvasCount: number;
refreshCount: number;
renderCount: number;
stackingCanvasCount: number;
totalLayerCount: number;
totalRenderDurationMs: number;
visibleStackingCanvasCount: number;
};
type MutableCanvasRendererDebugMetrics = Omit<
CanvasRendererDebugMetrics,
| 'canvasLayerCount'
| 'canvasMemoryBytes'
| 'canvasMemoryMegabytes'
| 'canvasPixelCount'
| 'canvasMemorySnapshots'
| 'pooledStackingCanvasCount'
| 'stackingCanvasCount'
| 'totalLayerCount'
| 'visibleStackingCanvasCount'
>;
type RenderPassStats = CanvasRenderPassMetrics;
type StackingCanvasState = {
bound: Bound | null;
layerId: string | null;
};
type RefreshTarget =
| { type: 'all' }
| { type: 'main' }
| { type: 'element'; element: SurfaceElementModel | GfxLocalElementModel }
| {
type: 'elements';
elements: Array<SurfaceElementModel | GfxLocalElementModel>;
};
const STACKING_CANVAS_PADDING = 32;
export class CanvasRenderer {
private _container!: HTMLElement;
private readonly _disposables = new DisposableGroup();
private readonly _gfx: GfxController;
private readonly _turboEnabled: () => boolean;
private readonly _overlays = new Set<Overlay>();
@@ -53,6 +132,37 @@ export class CanvasRenderer {
private _stackingCanvas: HTMLCanvasElement[] = [];
private readonly _stackingCanvasPool: HTMLCanvasElement[] = [];
private readonly _stackingCanvasState = new WeakMap<
HTMLCanvasElement,
StackingCanvasState
>();
private readonly _dirtyStackingCanvasIndexes = new Set<number>();
private _mainCanvasDirty = true;
private _needsFullRender = true;
private _debugMetrics: MutableCanvasRendererDebugMetrics = {
refreshCount: 0,
coalescedRefreshCount: 0,
renderCount: 0,
totalRenderDurationMs: 0,
lastRenderDurationMs: 0,
maxRenderDurationMs: 0,
lastRenderMetrics: {
renderByBoundCallCount: 0,
visibleElementCount: 0,
renderedElementCount: 0,
placeholderElementCount: 0,
overlayCount: 0,
},
dirtyLayerRenderCount: 0,
fallbackElementCount: 0,
};
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
@@ -89,6 +199,7 @@ export class CanvasRenderer {
this.layerManager = options.layerManager;
this.grid = options.gridManager;
this.provider = options.provider ?? {};
this._gfx = this.std.get(GfxControllerIdentifier);
this._turboEnabled = () => {
const featureFlagService = options.std.get(FeatureFlagService);
@@ -132,15 +243,199 @@ export class CanvasRenderer {
};
}
private _applyStackingCanvasLayout(
canvas: HTMLCanvasElement,
bound: Bound | null,
dpr = window.devicePixelRatio
) {
const state =
this._stackingCanvasState.get(canvas) ??
({
bound: null,
layerId: canvas.dataset.layerId ?? null,
} satisfies StackingCanvasState);
if (!bound || bound.w <= 0 || bound.h <= 0) {
canvas.style.display = 'none';
canvas.style.left = '0px';
canvas.style.top = '0px';
canvas.style.width = '0px';
canvas.style.height = '0px';
canvas.style.transform = '';
canvas.width = 0;
canvas.height = 0;
state.bound = null;
state.layerId = canvas.dataset.layerId ?? null;
this._stackingCanvasState.set(canvas, state);
return;
}
const { viewportBounds, zoom, viewScale } = this.viewport;
const width = bound.w * zoom;
const height = bound.h * zoom;
const left = (bound.x - viewportBounds.x) * zoom;
const top = (bound.y - viewportBounds.y) * zoom;
const actualWidth = Math.max(1, Math.ceil(width * dpr));
const actualHeight = Math.max(1, Math.ceil(height * dpr));
const transform = `translate(${left}px, ${top}px) scale(${1 / viewScale})`;
if (canvas.style.display !== 'block') {
canvas.style.display = 'block';
}
if (canvas.style.left !== '0px') {
canvas.style.left = '0px';
}
if (canvas.style.top !== '0px') {
canvas.style.top = '0px';
}
if (canvas.style.width !== `${width}px`) {
canvas.style.width = `${width}px`;
}
if (canvas.style.height !== `${height}px`) {
canvas.style.height = `${height}px`;
}
if (canvas.style.transform !== transform) {
canvas.style.transform = transform;
}
if (canvas.style.transformOrigin !== 'top left') {
canvas.style.transformOrigin = 'top left';
}
if (canvas.width !== actualWidth) {
canvas.width = actualWidth;
}
if (canvas.height !== actualHeight) {
canvas.height = actualHeight;
}
state.bound = bound;
state.layerId = canvas.dataset.layerId ?? null;
this._stackingCanvasState.set(canvas, state);
}
private _clampBoundToViewport(bound: Bound, viewportBounds: Bound) {
const minX = Math.max(bound.x, viewportBounds.x);
const minY = Math.max(bound.y, viewportBounds.y);
const maxX = Math.min(bound.maxX, viewportBounds.maxX);
const maxY = Math.min(bound.maxY, viewportBounds.maxY);
if (maxX <= minX || maxY <= minY) {
return null;
}
return new Bound(minX, minY, maxX - minX, maxY - minY);
}
private _createCanvasForLayer(
onCreated?: (canvas: HTMLCanvasElement) => void
) {
const reused = this._stackingCanvasPool.pop();
if (reused) {
return reused;
}
const created = document.createElement('canvas');
onCreated?.(created);
return created;
}
private _findLayerIndexByElement(
element: SurfaceElementModel | GfxLocalElementModel
) {
const canvasLayers = this.layerManager.getCanvasLayers();
const index = canvasLayers.findIndex(layer =>
layer.elements.some(layerElement => layerElement.id === element.id)
);
return index === -1 ? null : index;
}
private _getLayerRenderBound(
elements: SurfaceElementModel[],
viewportBounds: Bound
) {
let layerBound: Bound | null = null;
for (const element of elements) {
const display = (element.display ?? true) && !element.hidden;
if (!display) {
continue;
}
const elementBound = Bound.from(getBoundWithRotation(element));
if (!intersects(elementBound, viewportBounds)) {
continue;
}
layerBound = layerBound ? layerBound.unite(elementBound) : elementBound;
}
if (!layerBound) {
return null;
}
return this._clampBoundToViewport(
layerBound.expand(STACKING_CANVAS_PADDING),
viewportBounds
);
}
private _getResolvedStackingCanvasBound(
canvas: HTMLCanvasElement,
bound: Bound | null
) {
if (!bound || !this._gfx.tool.dragging$.peek()) {
return bound;
}
const previousBound = this._stackingCanvasState.get(canvas)?.bound;
return previousBound ? previousBound.unite(bound) : bound;
}
private _invalidate(target: RefreshTarget = { type: 'all' }) {
if (target.type === 'all') {
this._needsFullRender = true;
this._mainCanvasDirty = true;
this._dirtyStackingCanvasIndexes.clear();
return;
}
if (this._needsFullRender) {
return;
}
if (target.type === 'main') {
this._mainCanvasDirty = true;
return;
}
const elements =
target.type === 'element' ? [target.element] : target.elements;
for (const element of elements) {
const layerIndex = this._findLayerIndexByElement(element);
if (layerIndex === null || layerIndex >= this._stackingCanvas.length) {
this._mainCanvasDirty = true;
continue;
}
this._dirtyStackingCanvasIndexes.add(layerIndex);
}
}
private _resetPooledCanvas(canvas: HTMLCanvasElement) {
canvas.dataset.layerId = '';
this._applyStackingCanvasLayout(canvas, null);
}
private _initStackingCanvas(onCreated?: (canvas: HTMLCanvasElement) => void) {
const layer = this.layerManager;
const updateStackingCanvasSize = (canvases: HTMLCanvasElement[]) => {
this._stackingCanvas = canvases;
const sizeUpdater = this._canvasSizeUpdater();
canvases.filter(sizeUpdater.filter).forEach(sizeUpdater.update);
};
const updateStackingCanvas = () => {
/**
* we already have a main canvas, so the last layer should be skipped
@@ -159,11 +454,7 @@ export class CanvasRenderer {
const created = i < currentCanvases.length;
const canvas = created
? currentCanvases[i]
: document.createElement('canvas');
if (!created) {
onCreated?.(canvas);
}
: this._createCanvasForLayer(onCreated);
canvas.dataset.layerId = `[${layer.indexes[0]}--${layer.indexes[1]}]`;
canvas.style.zIndex = layer.zIndex.toString();
@@ -171,7 +462,6 @@ export class CanvasRenderer {
}
this._stackingCanvas = canvases;
updateStackingCanvasSize(canvases);
if (currentCanvases.length !== canvases.length) {
const diff = canvases.length - currentCanvases.length;
@@ -189,12 +479,16 @@ export class CanvasRenderer {
payload.added = canvases.slice(-diff);
} else {
payload.removed = currentCanvases.slice(diff);
payload.removed.forEach(canvas => {
this._resetPooledCanvas(canvas);
this._stackingCanvasPool.push(canvas);
});
}
this.stackingCanvasUpdated.next(payload);
}
this.refresh();
this.refresh({ type: 'all' });
};
this._disposables.add(
@@ -211,7 +505,7 @@ export class CanvasRenderer {
this._disposables.add(
this.viewport.viewportUpdated.subscribe(() => {
this.refresh();
this.refresh({ type: 'all' });
})
);
@@ -222,7 +516,6 @@ export class CanvasRenderer {
sizeUpdatedRafId = null;
this._resetSize();
this._render();
this.refresh();
}, this._container);
})
);
@@ -233,69 +526,212 @@ export class CanvasRenderer {
if (this.usePlaceholder !== shouldRenderPlaceholders) {
this.usePlaceholder = shouldRenderPlaceholders;
this.refresh();
this.refresh({ type: 'all' });
}
})
);
let wasDragging = false;
this._disposables.add(
effect(() => {
const isDragging = this._gfx.tool.dragging$.value;
if (wasDragging && !isDragging) {
this.refresh({ type: 'all' });
}
wasDragging = isDragging;
})
);
this.usePlaceholder = false;
}
private _createRenderPassStats(): RenderPassStats {
return {
renderByBoundCallCount: 0,
visibleElementCount: 0,
renderedElementCount: 0,
placeholderElementCount: 0,
overlayCount: 0,
};
}
private _getCanvasMemorySnapshots(): CanvasMemorySnapshot[] {
return [this.canvas, ...this._stackingCanvas].map((canvas, index) => {
return {
kind: index === 0 ? 'main' : 'stacking',
width: canvas.width,
height: canvas.height,
bytes: canvas.width * canvas.height * 4,
zIndex: canvas.style.zIndex,
datasetLayerId: canvas.dataset.layerId ?? null,
};
});
}
private _render() {
const renderStart = performance.now();
const { viewportBounds, zoom } = this.viewport;
const { ctx } = this;
const dpr = window.devicePixelRatio;
const scale = zoom * dpr;
const matrix = new DOMMatrix().scaleSelf(scale);
const renderStats = this._createRenderPassStats();
const fullRender = this._needsFullRender;
const stackingIndexesToRender = fullRender
? this._stackingCanvas.map((_, idx) => idx)
: [...this._dirtyStackingCanvasIndexes];
/**
* if a layer does not have a corresponding canvas
* its element will be add to this array and drawing on the
* main canvas
*/
let fallbackElement: SurfaceElementModel[] = [];
const allCanvasLayers = this.layerManager.getCanvasLayers();
const viewportBound = Bound.from(viewportBounds);
this.layerManager.getCanvasLayers().forEach((layer, idx) => {
if (!this._stackingCanvas[idx]) {
fallbackElement = fallbackElement.concat(layer.elements);
return;
for (const idx of stackingIndexesToRender) {
const layer = allCanvasLayers[idx];
const canvas = this._stackingCanvas[idx];
if (!layer || !canvas) {
continue;
}
const canvas = this._stackingCanvas[idx];
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
const rc = new RoughCanvas(ctx.canvas);
const layerRenderBound = this._getLayerRenderBound(
layer.elements,
viewportBound
);
const resolvedLayerRenderBound = this._getResolvedStackingCanvasBound(
canvas,
layerRenderBound
);
ctx.clearRect(0, 0, canvas.width, canvas.height);
this._applyStackingCanvasLayout(canvas, resolvedLayerRenderBound);
if (
!resolvedLayerRenderBound ||
canvas.width === 0 ||
canvas.height === 0
) {
continue;
}
const layerCtx = canvas.getContext('2d') as CanvasRenderingContext2D;
const layerRc = new RoughCanvas(layerCtx.canvas);
layerCtx.clearRect(0, 0, canvas.width, canvas.height);
layerCtx.save();
layerCtx.setTransform(matrix);
this._renderByBound(
layerCtx,
matrix,
layerRc,
resolvedLayerRenderBound,
layer.elements,
false,
renderStats
);
}
if (fullRender || this._mainCanvasDirty) {
allCanvasLayers.forEach((layer, idx) => {
if (!this._stackingCanvas[idx]) {
fallbackElement = fallbackElement.concat(layer.elements);
}
});
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.save();
ctx.setTransform(matrix);
this._renderByBound(ctx, matrix, rc, viewportBounds, layer.elements);
});
this._renderByBound(
ctx,
matrix,
new RoughCanvas(ctx.canvas),
viewportBounds,
fallbackElement,
true,
renderStats
);
}
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.save();
ctx.setTransform(matrix);
this._renderByBound(
ctx,
matrix,
new RoughCanvas(ctx.canvas),
viewportBounds,
fallbackElement,
true
const canvasMemorySnapshots = this._getCanvasMemorySnapshots();
const canvasMemoryBytes = canvasMemorySnapshots.reduce(
(sum, snapshot) => sum + snapshot.bytes,
0
);
const layerTypes = this.layerManager.layers.map(layer => layer.type);
const renderDurationMs = performance.now() - renderStart;
this._debugMetrics.renderCount += 1;
this._debugMetrics.totalRenderDurationMs += renderDurationMs;
this._debugMetrics.lastRenderDurationMs = renderDurationMs;
this._debugMetrics.maxRenderDurationMs = Math.max(
this._debugMetrics.maxRenderDurationMs,
renderDurationMs
);
this._debugMetrics.lastRenderMetrics = renderStats;
this._debugMetrics.fallbackElementCount = fallbackElement.length;
this._debugMetrics.dirtyLayerRenderCount = stackingIndexesToRender.length;
this._lastDebugSnapshot = {
canvasMemorySnapshots,
canvasMemoryBytes,
canvasPixelCount: canvasMemorySnapshots.reduce(
(sum, snapshot) => sum + snapshot.width * snapshot.height,
0
),
stackingCanvasCount: this._stackingCanvas.length,
canvasLayerCount: layerTypes.filter(type => type === 'canvas').length,
totalLayerCount: layerTypes.length,
pooledStackingCanvasCount: this._stackingCanvasPool.length,
visibleStackingCanvasCount: this._stackingCanvas.filter(
canvas => canvas.width > 0 && canvas.height > 0
).length,
};
this._needsFullRender = false;
this._mainCanvasDirty = false;
this._dirtyStackingCanvasIndexes.clear();
}
private _lastDebugSnapshot: Pick<
CanvasRendererDebugMetrics,
| 'canvasMemoryBytes'
| 'canvasMemorySnapshots'
| 'canvasPixelCount'
| 'canvasLayerCount'
| 'pooledStackingCanvasCount'
| 'stackingCanvasCount'
| 'totalLayerCount'
| 'visibleStackingCanvasCount'
> = {
canvasMemoryBytes: 0,
canvasMemorySnapshots: [],
canvasPixelCount: 0,
canvasLayerCount: 0,
pooledStackingCanvasCount: 0,
stackingCanvasCount: 0,
totalLayerCount: 0,
visibleStackingCanvasCount: 0,
};
private _renderByBound(
ctx: CanvasRenderingContext2D | null,
matrix: DOMMatrix,
rc: RoughCanvas,
bound: IBound,
surfaceElements?: SurfaceElementModel[],
overLay: boolean = false
overLay: boolean = false,
renderStats?: RenderPassStats
) {
if (!ctx) return;
renderStats && (renderStats.renderByBoundCallCount += 1);
const elements =
surfaceElements ??
(this.grid.search(bound, {
@@ -305,10 +741,12 @@ export class CanvasRenderer {
for (const element of elements) {
const display = (element.display ?? true) && !element.hidden;
if (display && intersects(getBoundWithRotation(element), bound)) {
renderStats && (renderStats.visibleElementCount += 1);
if (
this.usePlaceholder &&
!(element as GfxCompatibleInterface).forceFullRender
) {
renderStats && (renderStats.placeholderElementCount += 1);
ctx.save();
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
const drawX = element.x - bound.x;
@@ -316,6 +754,7 @@ export class CanvasRenderer {
ctx.fillRect(drawX, drawY, element.w, element.h);
ctx.restore();
} else {
renderStats && (renderStats.renderedElementCount += 1);
ctx.save();
const renderFn = this.std.getOptional<ElementRenderer>(
ElementRendererIdentifier(element.type)
@@ -333,6 +772,7 @@ export class CanvasRenderer {
}
if (overLay) {
renderStats && (renderStats.overlayCount += this._overlays.size);
for (const overlay of this._overlays) {
ctx.save();
ctx.translate(-bound.x, -bound.y);
@@ -348,33 +788,38 @@ export class CanvasRenderer {
const sizeUpdater = this._canvasSizeUpdater();
sizeUpdater.update(this.canvas);
this._stackingCanvas.forEach(sizeUpdater.update);
this.refresh();
this._invalidate({ type: 'all' });
}
private _watchSurface(surfaceModel: SurfaceBlockModel) {
this._disposables.add(
surfaceModel.elementAdded.subscribe(() => this.refresh())
surfaceModel.elementAdded.subscribe(() => this.refresh({ type: 'all' }))
);
this._disposables.add(
surfaceModel.elementRemoved.subscribe(() => this.refresh())
surfaceModel.elementRemoved.subscribe(() => this.refresh({ type: 'all' }))
);
this._disposables.add(
surfaceModel.localElementAdded.subscribe(() => this.refresh())
surfaceModel.localElementAdded.subscribe(() =>
this.refresh({ type: 'all' })
)
);
this._disposables.add(
surfaceModel.localElementDeleted.subscribe(() => this.refresh())
surfaceModel.localElementDeleted.subscribe(() =>
this.refresh({ type: 'all' })
)
);
this._disposables.add(
surfaceModel.localElementUpdated.subscribe(() => this.refresh())
surfaceModel.localElementUpdated.subscribe(({ model }) => {
this.refresh({ type: 'element', element: model });
})
);
this._disposables.add(
surfaceModel.elementUpdated.subscribe(payload => {
// ignore externalXYWH update cause it's updated by the renderer
if (payload.props['externalXYWH']) return;
this.refresh();
const element = surfaceModel.getElementById(payload.id);
this.refresh(element ? { type: 'element', element } : { type: 'all' });
})
);
}
@@ -382,7 +827,7 @@ export class CanvasRenderer {
addOverlay(overlay: Overlay) {
overlay.setRenderer(this);
this._overlays.add(overlay);
this.refresh();
this.refresh({ type: 'main' });
}
/**
@@ -394,7 +839,7 @@ export class CanvasRenderer {
container.append(this.canvas);
this._resetSize();
this.refresh();
this.refresh({ type: 'all' });
}
dispose(): void {
@@ -453,8 +898,46 @@ export class CanvasRenderer {
return this.provider.getPropertyValue?.(property) ?? '';
}
refresh() {
if (this._refreshRafId !== null) return;
getDebugMetrics(): CanvasRendererDebugMetrics {
return {
...this._debugMetrics,
...this._lastDebugSnapshot,
canvasMemoryMegabytes:
this._lastDebugSnapshot.canvasMemoryBytes / 1024 / 1024,
};
}
resetDebugMetrics() {
this._debugMetrics = {
refreshCount: 0,
coalescedRefreshCount: 0,
renderCount: 0,
totalRenderDurationMs: 0,
lastRenderDurationMs: 0,
maxRenderDurationMs: 0,
lastRenderMetrics: this._createRenderPassStats(),
dirtyLayerRenderCount: 0,
fallbackElementCount: 0,
};
this._lastDebugSnapshot = {
canvasMemoryBytes: 0,
canvasMemorySnapshots: [],
canvasPixelCount: 0,
canvasLayerCount: 0,
pooledStackingCanvasCount: 0,
stackingCanvasCount: 0,
totalLayerCount: 0,
visibleStackingCanvasCount: 0,
};
}
refresh(target: RefreshTarget = { type: 'all' }) {
this._debugMetrics.refreshCount += 1;
this._invalidate(target);
if (this._refreshRafId !== null) {
this._debugMetrics.coalescedRefreshCount += 1;
return;
}
this._refreshRafId = requestConnectedFrame(() => {
this._refreshRafId = null;
@@ -469,6 +952,6 @@ export class CanvasRenderer {
overlay.setRenderer(null);
this._overlays.delete(overlay);
this.refresh();
this.refresh({ type: 'main' });
}
}

View File

@@ -354,30 +354,37 @@ export class DomRenderer {
this._disposables.add(
surfaceModel.elementAdded.subscribe(payload => {
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
this._markViewportDirty();
this.refresh();
})
);
this._disposables.add(
surfaceModel.elementRemoved.subscribe(payload => {
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
this._markViewportDirty();
this.refresh();
})
);
this._disposables.add(
surfaceModel.localElementAdded.subscribe(payload => {
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
this._markViewportDirty();
this.refresh();
})
);
this._disposables.add(
surfaceModel.localElementDeleted.subscribe(payload => {
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
this._markViewportDirty();
this.refresh();
})
);
this._disposables.add(
surfaceModel.localElementUpdated.subscribe(payload => {
this._markElementDirty(payload.model.id, UpdateType.ELEMENT_UPDATED);
if (payload.props['index'] || payload.props['groupId']) {
this._markViewportDirty();
}
this.refresh();
})
);
@@ -387,6 +394,9 @@ export class DomRenderer {
// ignore externalXYWH update cause it's updated by the renderer
if (payload.props['externalXYWH']) return;
this._markElementDirty(payload.id, UpdateType.ELEMENT_UPDATED);
if (payload.props['index'] || payload.props['childIds']) {
this._markViewportDirty();
}
this.refresh();
})
);

View File

@@ -1,4 +1,8 @@
import { getHostName } from '@blocksuite/affine-shared/utils';
import {
getHostName,
isValidUrl,
normalizeUrl,
} from '@blocksuite/affine-shared/utils';
import { PropTypes, requiredProperties } from '@blocksuite/std';
import { css, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
@@ -44,15 +48,27 @@ export class LinkPreview extends LitElement {
override render() {
const { url } = this;
const normalizedUrl = normalizeUrl(url);
const safeUrl =
normalizedUrl && isValidUrl(normalizedUrl) ? normalizedUrl : null;
const hostName = getHostName(safeUrl ?? url);
if (!safeUrl) {
return html`
<span class="affine-link-preview">
<span>${hostName}</span>
</span>
`;
}
return html`
<a
class="affine-link-preview"
rel="noopener noreferrer"
target="_blank"
href=${url}
href=${safeUrl}
>
<span>${getHostName(url)}</span>
<span>${hostName}</span>
</a>
`;
}

View File

@@ -32,7 +32,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -15,7 +15,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts"

View File

@@ -8,7 +8,7 @@ export default defineConfig({
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 500,
coverage: {
provider: 'istanbul', // or 'c8'
provider: 'istanbul',
reporter: ['lcov'],
reportsDirectory: '../../../.coverage/ext-loader',
},

View File

@@ -5,6 +5,8 @@ import {
import type { BrushElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
import { renderBrushLikeDom } from './shared';
export const BrushDomRendererExtension = DomElementRendererExtension(
'brush',
(
@@ -12,58 +14,11 @@ export const BrushDomRendererExtension = DomElementRendererExtension(
domElement: HTMLElement,
renderer: DomRenderer
) => {
const { zoom } = renderer.viewport;
const [, , w, h] = model.deserializedXYWH;
// Early return if invalid dimensions
if (w <= 0 || h <= 0) {
return;
}
// Early return if no commands
if (!model.commands) {
return;
}
// Clear previous content
domElement.innerHTML = '';
// Get color value
const color = renderer.getColorValue(model.color, DefaultTheme.black, true);
// Create SVG element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.position = 'absolute';
svg.style.left = '0';
svg.style.top = '0';
svg.style.width = `${w * zoom}px`;
svg.style.height = `${h * zoom}px`;
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
// Apply rotation transform
if (model.rotate !== 0) {
svg.style.transform = `rotate(${model.rotate}deg)`;
svg.style.transformOrigin = 'center';
}
// Create path element for the brush stroke
const pathElement = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
pathElement.setAttribute('d', model.commands);
pathElement.setAttribute('fill', color);
pathElement.setAttribute('stroke', 'none');
svg.append(pathElement);
domElement.replaceChildren(svg);
// Set element size and position
domElement.style.width = `${w * zoom}px`;
domElement.style.height = `${h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
renderBrushLikeDom({
model,
domElement,
renderer,
color: renderer.getColorValue(model.color, DefaultTheme.black, true),
});
}
);

View File

@@ -5,6 +5,8 @@ import {
import type { HighlighterElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
import { renderBrushLikeDom } from './shared';
export const HighlighterDomRendererExtension = DomElementRendererExtension(
'highlighter',
(
@@ -12,62 +14,15 @@ export const HighlighterDomRendererExtension = DomElementRendererExtension(
domElement: HTMLElement,
renderer: DomRenderer
) => {
const { zoom } = renderer.viewport;
const [, , w, h] = model.deserializedXYWH;
// Early return if invalid dimensions
if (w <= 0 || h <= 0) {
return;
}
// Early return if no commands
if (!model.commands) {
return;
}
// Clear previous content
domElement.innerHTML = '';
// Get color value
const color = renderer.getColorValue(
model.color,
DefaultTheme.hightlighterColor,
true
);
// Create SVG element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.position = 'absolute';
svg.style.left = '0';
svg.style.top = '0';
svg.style.width = `${w * zoom}px`;
svg.style.height = `${h * zoom}px`;
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
// Apply rotation transform
if (model.rotate !== 0) {
svg.style.transform = `rotate(${model.rotate}deg)`;
svg.style.transformOrigin = 'center';
}
// Create path element for the highlighter stroke
const pathElement = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
pathElement.setAttribute('d', model.commands);
pathElement.setAttribute('fill', color);
pathElement.setAttribute('stroke', 'none');
svg.append(pathElement);
domElement.replaceChildren(svg);
// Set element size and position
domElement.style.width = `${w * zoom}px`;
domElement.style.height = `${h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
renderBrushLikeDom({
model,
domElement,
renderer,
color: renderer.getColorValue(
model.color,
DefaultTheme.hightlighterColor,
true
),
});
}
);

View File

@@ -0,0 +1,82 @@
import type { DomRenderer } from '@blocksuite/affine-block-surface';
import type {
BrushElementModel,
HighlighterElementModel,
} from '@blocksuite/affine-model';
const SVG_NS = 'http://www.w3.org/2000/svg';
type BrushLikeModel = BrushElementModel | HighlighterElementModel;
type RetainedBrushDom = {
path: SVGPathElement;
svg: SVGSVGElement;
};
const retainedBrushDom = new WeakMap<HTMLElement, RetainedBrushDom>();
function clearBrushLikeDom(domElement: HTMLElement) {
retainedBrushDom.delete(domElement);
domElement.replaceChildren();
}
function getRetainedBrushDom(domElement: HTMLElement) {
const existing = retainedBrushDom.get(domElement);
if (existing) {
return existing;
}
const svg = document.createElementNS(SVG_NS, 'svg');
svg.style.position = 'absolute';
svg.style.left = '0';
svg.style.top = '0';
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
const path = document.createElementNS(SVG_NS, 'path');
path.setAttribute('stroke', 'none');
svg.append(path);
const retained = { svg, path };
retainedBrushDom.set(domElement, retained);
domElement.replaceChildren(svg);
return retained;
}
export function renderBrushLikeDom({
color,
domElement,
model,
renderer,
}: {
color: string;
domElement: HTMLElement;
model: BrushLikeModel;
renderer: DomRenderer;
}) {
const { zoom } = renderer.viewport;
const [, , w, h] = model.deserializedXYWH;
if (w <= 0 || h <= 0 || !model.commands) {
clearBrushLikeDom(domElement);
return;
}
const { path, svg } = getRetainedBrushDom(domElement);
svg.style.width = `${w * zoom}px`;
svg.style.height = `${h * zoom}px`;
svg.style.transform = model.rotate === 0 ? '' : `rotate(${model.rotate}deg)`;
svg.style.transformOrigin = model.rotate === 0 ? '' : 'center';
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
path.setAttribute('d', model.commands);
path.setAttribute('fill', color);
domElement.style.width = `${w * zoom}px`;
domElement.style.height = `${h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
}

View File

@@ -14,6 +14,8 @@ import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx';
import { isConnectorWithLabel } from '../connector-manager';
import { DEFAULT_ARROW_SIZE } from './utils';
const SVG_NS = 'http://www.w3.org/2000/svg';
interface PathBounds {
minX: number;
minY: number;
@@ -21,6 +23,15 @@ interface PathBounds {
maxY: number;
}
type RetainedConnectorDom = {
defs: SVGDefsElement;
label: HTMLDivElement | null;
path: SVGPathElement;
svg: SVGSVGElement;
};
const retainedConnectorDom = new WeakMap<HTMLElement, RetainedConnectorDom>();
function calculatePathBounds(path: PointLocation[]): PathBounds {
if (path.length === 0) {
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
@@ -81,10 +92,7 @@ function createArrowMarker(
strokeWidth: number,
isStart: boolean = false
): SVGMarkerElement {
const marker = document.createElementNS(
'http://www.w3.org/2000/svg',
'marker'
);
const marker = document.createElementNS(SVG_NS, 'marker');
const size = DEFAULT_ARROW_SIZE * (strokeWidth / 2);
marker.id = id;
@@ -98,10 +106,7 @@ function createArrowMarker(
switch (style) {
case 'Arrow': {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
const path = document.createElementNS(SVG_NS, 'path');
path.setAttribute(
'd',
isStart ? 'M 20 5 L 10 10 L 20 15 Z' : 'M 0 5 L 10 10 L 0 15 Z'
@@ -112,10 +117,7 @@ function createArrowMarker(
break;
}
case 'Triangle': {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
const path = document.createElementNS(SVG_NS, 'path');
path.setAttribute(
'd',
isStart ? 'M 20 7 L 12 10 L 20 13 Z' : 'M 0 7 L 8 10 L 0 13 Z'
@@ -126,10 +128,7 @@ function createArrowMarker(
break;
}
case 'Circle': {
const circle = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
const circle = document.createElementNS(SVG_NS, 'circle');
circle.setAttribute('cx', '10');
circle.setAttribute('cy', '10');
circle.setAttribute('r', '4');
@@ -139,10 +138,7 @@ function createArrowMarker(
break;
}
case 'Diamond': {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
const path = document.createElementNS(SVG_NS, 'path');
path.setAttribute('d', 'M 10 6 L 14 10 L 10 14 L 6 10 Z');
path.setAttribute('fill', color);
path.setAttribute('stroke', color);
@@ -154,13 +150,64 @@ function createArrowMarker(
return marker;
}
function clearRetainedConnectorDom(element: HTMLElement) {
retainedConnectorDom.delete(element);
element.replaceChildren();
}
function getRetainedConnectorDom(element: HTMLElement): RetainedConnectorDom {
const existing = retainedConnectorDom.get(element);
if (existing) {
return existing;
}
const svg = document.createElementNS(SVG_NS, 'svg');
svg.style.position = 'absolute';
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
const defs = document.createElementNS(SVG_NS, 'defs');
const path = document.createElementNS(SVG_NS, 'path');
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
path.setAttribute('stroke-linejoin', 'round');
svg.append(defs, path);
element.replaceChildren(svg);
const retained = {
svg,
defs,
path,
label: null,
};
retainedConnectorDom.set(element, retained);
return retained;
}
function getOrCreateLabelElement(retained: RetainedConnectorDom) {
if (retained.label) {
return retained.label;
}
const label = document.createElement('div');
retained.svg.insertAdjacentElement('afterend', label);
retained.label = label;
return label;
}
function renderConnectorLabel(
model: ConnectorElementModel,
container: HTMLElement,
retained: RetainedConnectorDom,
renderer: DomRenderer,
zoom: number
) {
if (!isConnectorWithLabel(model) || !model.labelXYWH) {
retained.label?.remove();
retained.label = null;
return;
}
@@ -176,8 +223,7 @@ function renderConnectorLabel(
},
} = model;
// Create label element
const labelElement = document.createElement('div');
const labelElement = getOrCreateLabelElement(retained);
labelElement.style.position = 'absolute';
labelElement.style.left = `${lx * zoom}px`;
labelElement.style.top = `${ly * zoom}px`;
@@ -210,11 +256,7 @@ function renderConnectorLabel(
labelElement.style.wordWrap = 'break-word';
// Add text content
if (model.text) {
labelElement.textContent = model.text.toString();
}
container.append(labelElement);
labelElement.textContent = model.text ? model.text.toString() : '';
}
/**
@@ -241,14 +283,13 @@ export const connectorBaseDomRenderer = (
stroke,
} = model;
// Clear previous content
element.innerHTML = '';
// Early return if no path points
if (!points || points.length < 2) {
clearRetainedConnectorDom(element);
return;
}
const retained = getRetainedConnectorDom(element);
// Calculate bounds for the SVG viewBox
const pathBounds = calculatePathBounds(points);
const padding = Math.max(strokeWidth * 2, 20); // Add padding for arrows
@@ -257,8 +298,7 @@ export const connectorBaseDomRenderer = (
const offsetX = pathBounds.minX - padding;
const offsetY = pathBounds.minY - padding;
// Create SVG element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const { defs, path, svg } = retained;
svg.style.position = 'absolute';
svg.style.left = `${offsetX * zoom}px`;
svg.style.top = `${offsetY * zoom}px`;
@@ -268,49 +308,43 @@ export const connectorBaseDomRenderer = (
svg.style.pointerEvents = 'none';
svg.setAttribute('viewBox', `0 0 ${svgWidth / zoom} ${svgHeight / zoom}`);
// Create defs for markers
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svg.append(defs);
const strokeColor = renderer.getColorValue(
stroke,
DefaultTheme.connectorColor,
true
);
// Create markers for endpoints
const markers: SVGMarkerElement[] = [];
let startMarkerId = '';
let endMarkerId = '';
if (frontEndpointStyle !== 'None') {
startMarkerId = `start-marker-${model.id}`;
const startMarker = createArrowMarker(
startMarkerId,
frontEndpointStyle,
strokeColor,
strokeWidth,
true
markers.push(
createArrowMarker(
startMarkerId,
frontEndpointStyle,
strokeColor,
strokeWidth,
true
)
);
defs.append(startMarker);
}
if (rearEndpointStyle !== 'None') {
endMarkerId = `end-marker-${model.id}`;
const endMarker = createArrowMarker(
endMarkerId,
rearEndpointStyle,
strokeColor,
strokeWidth,
false
markers.push(
createArrowMarker(
endMarkerId,
rearEndpointStyle,
strokeColor,
strokeWidth,
false
)
);
defs.append(endMarker);
}
// Create path element
const pathElement = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
defs.replaceChildren(...markers);
// Adjust points relative to the SVG coordinate system
const adjustedPoints = points.map(point => {
@@ -334,29 +368,25 @@ export const connectorBaseDomRenderer = (
});
const pathData = createConnectorPath(adjustedPoints, mode);
pathElement.setAttribute('d', pathData);
pathElement.setAttribute('stroke', strokeColor);
pathElement.setAttribute('stroke-width', String(strokeWidth));
pathElement.setAttribute('fill', 'none');
pathElement.setAttribute('stroke-linecap', 'round');
pathElement.setAttribute('stroke-linejoin', 'round');
// Apply stroke style
path.setAttribute('d', pathData);
path.setAttribute('stroke', strokeColor);
path.setAttribute('stroke-width', String(strokeWidth));
if (strokeStyle === 'dash') {
pathElement.setAttribute('stroke-dasharray', '12,12');
path.setAttribute('stroke-dasharray', '12,12');
} else {
path.removeAttribute('stroke-dasharray');
}
// Apply markers
if (startMarkerId) {
pathElement.setAttribute('marker-start', `url(#${startMarkerId})`);
path.setAttribute('marker-start', `url(#${startMarkerId})`);
} else {
path.removeAttribute('marker-start');
}
if (endMarkerId) {
pathElement.setAttribute('marker-end', `url(#${endMarkerId})`);
path.setAttribute('marker-end', `url(#${endMarkerId})`);
} else {
path.removeAttribute('marker-end');
}
svg.append(pathElement);
element.append(svg);
// Set element size and position
element.style.width = `${model.w * zoom}px`;
element.style.height = `${model.h * zoom}px`;
@@ -370,7 +400,11 @@ export const connectorDomRenderer = (
renderer: DomRenderer
): void => {
connectorBaseDomRenderer(model, element, renderer);
renderConnectorLabel(model, element, renderer, renderer.viewport.zoom);
const retained = retainedConnectorDom.get(element);
if (!retained) return;
renderConnectorLabel(model, retained, renderer, renderer.viewport.zoom);
};
/**

View File

@@ -34,7 +34,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -32,7 +32,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -6,6 +6,37 @@ import { SVGShapeBuilder } from '@blocksuite/global/gfx';
import { manageClassNames, setStyles } from './utils';
const SVG_NS = 'http://www.w3.org/2000/svg';
type RetainedShapeDom = {
polygon: SVGPolygonElement | null;
svg: SVGSVGElement | null;
text: HTMLDivElement | null;
};
type RetainedShapeSvg = {
polygon: SVGPolygonElement;
svg: SVGSVGElement;
};
const retainedShapeDom = new WeakMap<HTMLElement, RetainedShapeDom>();
function getRetainedShapeDom(element: HTMLElement): RetainedShapeDom {
const existing = retainedShapeDom.get(element);
if (existing) {
return existing;
}
const retained = {
svg: null,
polygon: null,
text: null,
};
retainedShapeDom.set(element, retained);
return retained;
}
function applyShapeSpecificStyles(
model: ShapeElementModel,
element: HTMLElement,
@@ -14,10 +45,6 @@ function applyShapeSpecificStyles(
// Reset properties that might be set by different shape types
element.style.removeProperty('clip-path');
element.style.removeProperty('border-radius');
// Clear DOM for shapes that don't use SVG, or if type changes from SVG-based to non-SVG-based
if (model.shapeType !== 'diamond' && model.shapeType !== 'triangle') {
while (element.firstChild) element.firstChild.remove();
}
switch (model.shapeType) {
case 'rect': {
@@ -42,6 +69,54 @@ function applyShapeSpecificStyles(
// No 'else' needed to clear styles, as they are reset at the beginning of the function.
}
function getOrCreateSvg(
retained: RetainedShapeDom,
element: HTMLElement
): RetainedShapeSvg {
if (retained.svg && retained.polygon) {
return {
svg: retained.svg,
polygon: retained.polygon,
};
}
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('preserveAspectRatio', 'none');
const polygon = document.createElementNS(SVG_NS, 'polygon');
svg.append(polygon);
retained.svg = svg;
retained.polygon = polygon;
element.prepend(svg);
return { svg, polygon };
}
function removeSvg(retained: RetainedShapeDom) {
retained.svg?.remove();
retained.svg = null;
retained.polygon = null;
}
function getOrCreateText(retained: RetainedShapeDom, element: HTMLElement) {
if (retained.text) {
return retained.text;
}
const text = document.createElement('div');
retained.text = text;
element.append(text);
return text;
}
function removeText(retained: RetainedShapeDom) {
retained.text?.remove();
retained.text = null;
}
function applyBorderStyles(
model: ShapeElementModel,
element: HTMLElement,
@@ -99,8 +174,7 @@ export const shapeDomRenderer = (
const { zoom } = renderer.viewport;
const unscaledWidth = model.w;
const unscaledHeight = model.h;
const newChildren: Element[] = [];
const retained = getRetainedShapeDom(element);
const fillColor = renderer.getColorValue(
model.fillColor,
@@ -124,6 +198,7 @@ export const shapeDomRenderer = (
// For diamond and triangle, fill and border are handled by inline SVG
element.style.border = 'none'; // Ensure no standard CSS border interferes
element.style.backgroundColor = 'transparent'; // Host element is transparent
const { polygon, svg } = getOrCreateSvg(retained, element);
const strokeW = model.strokeWidth;
@@ -155,37 +230,30 @@ export const shapeDomRenderer = (
// Determine fill color
const finalFillColor = model.filled ? fillColor : 'transparent';
// Build SVG safely with DOM-API
const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
svg.setAttribute('preserveAspectRatio', 'none');
const polygon = document.createElementNS(SVG_NS, 'polygon');
polygon.setAttribute('points', svgPoints);
polygon.setAttribute('fill', finalFillColor);
polygon.setAttribute('stroke', finalStrokeColor);
polygon.setAttribute('stroke-width', String(strokeW));
if (finalStrokeDasharray !== 'none') {
polygon.setAttribute('stroke-dasharray', finalStrokeDasharray);
} else {
polygon.removeAttribute('stroke-dasharray');
}
svg.append(polygon);
newChildren.push(svg);
} else {
// Standard rendering for other shapes (e.g., rect, ellipse)
// innerHTML was already cleared by applyShapeSpecificStyles if necessary
removeSvg(retained);
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border
}
if (model.textDisplay && model.text) {
const str = model.text.toString();
const textElement = document.createElement('div');
const textElement = getOrCreateText(retained, element);
if (isRTL(str)) {
textElement.dir = 'rtl';
} else {
textElement.removeAttribute('dir');
}
textElement.style.position = 'absolute';
textElement.style.inset = '0';
@@ -210,12 +278,10 @@ export const shapeDomRenderer = (
true
);
textElement.textContent = str;
newChildren.push(textElement);
} else {
removeText(retained);
}
// Replace existing children to avoid memory leaks
element.replaceChildren(...newChildren);
applyTransformStyles(model, element);
manageClassNames(model, element);

View File

@@ -29,7 +29,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -34,7 +34,8 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"@vitest/browser-playwright": "^4.0.18",
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -4,6 +4,7 @@ import type { FootNote } from '@blocksuite/affine-model';
import { CitationProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { isValidUrl, normalizeUrl } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit';
import {
BlockSelection,
@@ -152,7 +153,9 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
};
private readonly _handleUrlReference = (url: string) => {
window.open(url, '_blank');
const normalizedUrl = normalizeUrl(url);
if (!normalizedUrl || !isValidUrl(normalizedUrl)) return;
window.open(normalizedUrl, '_blank', 'noopener,noreferrer');
};
private readonly _updateFootnoteAttributes = (footnote: FootNote) => {

View File

@@ -1,3 +1,4 @@
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
@@ -8,10 +9,9 @@ export default defineConfig({
browser: {
enabled: true,
headless: true,
name: 'chromium',
provider: 'playwright',
instances: [{ browser: 'chromium' }],
provider: playwright(),
isolate: false,
providerOptions: {},
},
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 500,

View File

@@ -177,6 +177,11 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
override getNearestPoint(point: IVec): IVec {
const { mode, absolutePath: path } = this;
if (path.length === 0) {
const { x, y } = this;
return [x, y];
}
if (mode === ConnectorMode.Straight) {
const first = path[0];
const last = path[path.length - 1];
@@ -213,6 +218,10 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
h = bounds.h;
}
if (path.length === 0) {
return 0.5;
}
point[0] = Vec.clamp(point[0], x, x + w);
point[1] = Vec.clamp(point[1], y, y + h);
@@ -258,6 +267,10 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
h = bounds.h;
}
if (path.length === 0) {
return [x + w / 2, y + h / 2];
}
if (mode === ConnectorMode.Orthogonal) {
const points = path.map<IVec>(p => [p[0], p[1]]);
const point = Polyline.pointAt(points, offsetDistance);
@@ -300,6 +313,10 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
const { mode, strokeWidth, absolutePath: path } = this;
if (path.length === 0) {
return false;
}
const point =
mode === ConnectorMode.Curve
? getBezierNearestPoint(getBezierParameters(path), currentPoint)

View File

@@ -74,7 +74,7 @@
],
"devDependencies": {
"@types/pdfmake": "^0.2.12",
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"version": "0.26.3"
}

View File

@@ -0,0 +1,108 @@
import {
beforeEach,
describe,
expect,
it,
type MockInstance,
vi,
} from 'vitest';
import * as PointToRangeUtils from '../../utils/dom/point-to-range';
import { handleNativeRangeAtPoint } from '../../utils/dom/point-to-range';
describe('test handleNativeRangeAtPoint', () => {
let caretRangeFromPointSpy: MockInstance<
(clientX: number, clientY: number) => Range | null
>;
let resetNativeSelectionSpy: MockInstance<(range: Range | null) => void>;
beforeEach(() => {
caretRangeFromPointSpy = vi.spyOn(
PointToRangeUtils.api,
'caretRangeFromPoint'
);
resetNativeSelectionSpy = vi.spyOn(
PointToRangeUtils.api,
'resetNativeSelection'
);
});
it('does nothing if caretRangeFromPoint returns null', () => {
caretRangeFromPointSpy.mockReturnValue(null);
handleNativeRangeAtPoint(10, 10);
expect(resetNativeSelectionSpy).not.toHaveBeenCalled();
});
it('keeps range untouched if startContainer is a Text node', () => {
const div = document.createElement('div');
div.textContent = 'hello';
const text = div.firstChild!;
const range = document.createRange();
range.setStart(text, 2);
range.collapse(true);
caretRangeFromPointSpy.mockReturnValue(range);
handleNativeRangeAtPoint(10, 10);
expect(range.startContainer).toBe(text);
expect(range.startOffset).toBe(2);
expect(resetNativeSelectionSpy).toHaveBeenCalled();
});
it('moves caret into direct text child when clicking element', () => {
const div = document.createElement('div');
div.append('hello');
const range = document.createRange();
range.setStart(div, 1);
range.collapse(true);
caretRangeFromPointSpy.mockReturnValue(range);
handleNativeRangeAtPoint(10, 10);
expect(range.startContainer.nodeType).toBe(Node.TEXT_NODE);
expect(range.startContainer.textContent).toBe('hello');
expect(range.startOffset).toBe(5);
expect(resetNativeSelectionSpy).toHaveBeenCalled();
});
it('moves caret to last meaningful text inside nested element', () => {
const div = document.createElement('div');
div.innerHTML = `<span>a</span><span><em>b</em>c</span>`;
const range = document.createRange();
range.setStart(div, 2);
range.collapse(true);
caretRangeFromPointSpy.mockReturnValue(range);
handleNativeRangeAtPoint(10, 10);
expect(range.startContainer.nodeType).toBe(Node.TEXT_NODE);
expect(range.startContainer.textContent).toBe('c');
expect(range.startOffset).toBe(1);
expect(resetNativeSelectionSpy).toHaveBeenCalled();
});
it('falls back to searching startContainer when offset element has no text', () => {
const div = document.createElement('div');
div.innerHTML = `<span></span><span>ok</span>`;
const range = document.createRange();
range.setStart(div, 1);
range.collapse(true);
caretRangeFromPointSpy.mockReturnValue(range);
handleNativeRangeAtPoint(10, 10);
expect(range.startContainer.textContent).toBe('ok');
expect(range.startOffset).toBe(2);
expect(resetNativeSelectionSpy).toHaveBeenCalled();
});
});

View File

@@ -88,11 +88,73 @@ export function getCurrentNativeRange(selection = window.getSelection()) {
return selection.getRangeAt(0);
}
// functions need to be mocked in unit-test
export const api = {
caretRangeFromPoint,
resetNativeSelection,
};
export function handleNativeRangeAtPoint(x: number, y: number) {
const range = caretRangeFromPoint(x, y);
const range = api.caretRangeFromPoint(x, y);
if (range) {
normalizeCaretRange(range);
}
const startContainer = range?.startContainer;
// click on rich text
if (startContainer instanceof Node) {
resetNativeSelection(range);
api.resetNativeSelection(range);
}
}
function lastMeaningfulTextNode(node: Node) {
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
return node.textContent && node.textContent?.trim().length > 0
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
});
let last = null;
while (walker.nextNode()) {
last = walker.currentNode;
}
return last;
}
function normalizeCaretRange(range: Range) {
let { startContainer, startOffset } = range;
if (startContainer.nodeType === Node.TEXT_NODE) return;
// Try to find text in the element at `startOffset`
const offsetEl =
startOffset > 0
? startContainer.childNodes[startOffset - 1]
: startContainer.childNodes[0];
if (offsetEl) {
if (offsetEl.nodeType === Node.TEXT_NODE) {
range.setStart(
offsetEl,
startOffset > 0 ? (offsetEl.textContent?.length ?? 0) : 0
);
range.collapse(true);
return;
}
const text = lastMeaningfulTextNode(offsetEl);
if (text) {
range.setStart(text, text.textContent?.length ?? 0);
range.collapse(true);
return;
}
}
// Fallback, try to find text in startContainer
const text = lastMeaningfulTextNode(startContainer);
if (text) {
range.setStart(text, text.textContent?.length ?? 0);
range.collapse(true);
return;
}
}

View File

@@ -17,7 +17,14 @@ export async function printToPdf(
return new Promise<void>((resolve, reject) => {
const iframe = document.createElement('iframe');
document.body.append(iframe);
iframe.style.display = 'none';
// Use a hidden but rendering-enabled state instead of display: none
Object.assign(iframe.style, {
visibility: 'hidden',
position: 'absolute',
width: '0',
height: '0',
border: 'none',
});
iframe.srcdoc = '<!DOCTYPE html>';
iframe.onload = async () => {
if (!iframe.contentWindow) {
@@ -28,6 +35,44 @@ export async function printToPdf(
reject(new Error('Root element not defined, unable to print pdf'));
return;
}
const doc = iframe.contentWindow.document;
doc.write(`<!DOCTYPE html><html><head><style>@media print {
html, body {
height: initial !important;
overflow: initial !important;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
color: #000 !important;
background: #fff !important;
color-scheme: light !important;
}
::-webkit-scrollbar {
display: none;
}
:root, body {
--affine-text-primary: #000 !important;
--affine-text-secondary: #111 !important;
--affine-text-tertiary: #333 !important;
--affine-background-primary: #fff !important;
--affine-background-secondary: #fff !important;
--affine-background-tertiary: #fff !important;
}
body, [data-theme='dark'] {
color: #000 !important;
background: #fff !important;
}
body * {
color: #000 !important;
-webkit-text-fill-color: #000 !important;
}
:root {
--affine-note-shadow-box: none !important;
--affine-note-shadow-sticker: none !important;
}
}</style></head><body></body></html>`);
doc.close();
iframe.contentWindow.document
.write(`<!DOCTYPE html><html><head><style>@media print {
html, body {
@@ -49,6 +94,9 @@ export async function printToPdf(
--affine-background-primary: #fff !important;
--affine-background-secondary: #fff !important;
--affine-background-tertiary: #fff !important;
--affine-background-code-block: #f5f5f5 !important;
--affine-quote-color: #e3e3e3 !important;
--affine-border-color: #e3e3e3 !important;
}
body, [data-theme='dark'] {
color: #000 !important;
@@ -68,7 +116,7 @@ export async function printToPdf(
for (const element of document.styleSheets) {
try {
for (const cssRule of element.cssRules) {
const target = iframe.contentWindow.document.styleSheets[0];
const target = doc.styleSheets[0];
target.insertRule(cssRule.cssText, target.cssRules.length);
}
} catch (e) {
@@ -83,12 +131,33 @@ export async function printToPdf(
}
}
// Recursive function to find all canvases, including those in shadow roots
const findAllCanvases = (root: Node): HTMLCanvasElement[] => {
const canvases: HTMLCanvasElement[] = [];
const traverse = (node: Node) => {
if (node instanceof HTMLCanvasElement) {
canvases.push(node);
}
if (node instanceof HTMLElement || node instanceof ShadowRoot) {
node.childNodes.forEach(traverse);
}
if (node instanceof HTMLElement && node.shadowRoot) {
traverse(node.shadowRoot);
}
};
traverse(root);
return canvases;
};
// convert all canvas to image
const canvasImgObjectUrlMap = new Map<string, string>();
const allCanvas = rootElement.getElementsByTagName('canvas');
const allCanvas = findAllCanvases(rootElement);
let canvasKey = 1;
const canvasToKeyMap = new Map<HTMLCanvasElement, string>();
for (const canvas of allCanvas) {
canvas.dataset['printToPdfCanvasKey'] = canvasKey.toString();
const key = canvasKey.toString();
canvasToKeyMap.set(canvas, key);
canvasKey++;
const canvasImgObjectUrl = await new Promise<Blob | null>(resolve => {
try {
@@ -103,20 +172,42 @@ export async function printToPdf(
);
continue;
}
canvasImgObjectUrlMap.set(
canvas.dataset['printToPdfCanvasKey'],
URL.createObjectURL(canvasImgObjectUrl)
);
canvasImgObjectUrlMap.set(key, URL.createObjectURL(canvasImgObjectUrl));
}
const importedRoot = iframe.contentWindow.document.importNode(
rootElement,
true
) as HTMLDivElement;
// Recursive deep clone that flattens Shadow DOM into Light DOM
const deepCloneWithShadows = (node: Node): Node => {
const clone = doc.importNode(node, false);
if (
clone instanceof HTMLCanvasElement &&
node instanceof HTMLCanvasElement
) {
const key = canvasToKeyMap.get(node);
if (key) {
clone.dataset['printToPdfCanvasKey'] = key;
}
}
const appendChildren = (source: Node) => {
source.childNodes.forEach(child => {
(clone as Element).append(deepCloneWithShadows(child));
});
};
if (node instanceof HTMLElement && node.shadowRoot) {
appendChildren(node.shadowRoot);
}
appendChildren(node);
return clone;
};
const importedRoot = deepCloneWithShadows(rootElement) as HTMLDivElement;
// force light theme in print iframe
iframe.contentWindow.document.documentElement.dataset.theme = 'light';
iframe.contentWindow.document.body.dataset.theme = 'light';
doc.documentElement.dataset.theme = 'light';
doc.body.dataset.theme = 'light';
importedRoot.dataset.theme = 'light';
// draw saved canvas image to canvas
@@ -135,17 +226,67 @@ export async function printToPdf(
}
}
// append to iframe and print
iframe.contentWindow.document.body.append(importedRoot);
// Remove lazy loading from all images and force reload
const allImages = importedRoot.querySelectorAll('img');
allImages.forEach(img => {
img.removeAttribute('loading');
const src = img.getAttribute('src');
if (src) img.setAttribute('src', src);
});
// append to iframe
doc.body.append(importedRoot);
await options.beforeprint?.(iframe);
// browser may take some time to load font
await new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
// Robust image waiting logic
const waitForImages = async (container: HTMLElement) => {
const images: HTMLImageElement[] = [];
const view = container.ownerDocument.defaultView;
if (!view) return;
const findImages = (root: Node) => {
if (root instanceof view.HTMLImageElement) {
images.push(root);
}
if (
root instanceof view.HTMLElement ||
root instanceof view.ShadowRoot
) {
root.childNodes.forEach(findImages);
}
if (root instanceof view.HTMLElement && root.shadowRoot) {
findImages(root.shadowRoot);
}
};
findImages(container);
await Promise.all(
images.map(img => {
if (img.complete) {
if (img.naturalWidth === 0) {
console.warn('Image failed to load:', img.src);
}
return Promise.resolve();
}
return new Promise(resolve => {
img.onload = resolve;
img.onerror = resolve;
});
})
);
};
await waitForImages(importedRoot);
// browser may take some time to load font or other resources
await (doc.fonts?.ready ??
new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, 1000);
}));
iframe.contentWindow.onafterprint = async () => {
iframe.remove();

View File

@@ -24,6 +24,11 @@ const toURL = (str: string) => {
}
};
const hasAllowedScheme = (url: URL) => {
const protocol = url.protocol.slice(0, -1).toLowerCase();
return ALLOWED_SCHEMES.has(protocol);
};
function resolveURL(str: string, baseUrl: string, padded = false) {
const url = toURL(str);
if (!url) return null;
@@ -61,6 +66,7 @@ export function normalizeUrl(str: string) {
// Formatted
if (url) {
if (!hasAllowedScheme(url)) return '';
if (!str.endsWith('/') && url.href.endsWith('/')) {
return url.href.substring(0, url.href.length - 1);
}

View File

@@ -9,7 +9,7 @@ export default defineConfig({
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 1000,
coverage: {
provider: 'istanbul', // or 'c8'
provider: 'istanbul', // or 'istanbul'
reporter: ['lcov'],
reportsDirectory: '../../../.coverage/affine-shared',
},

View File

@@ -62,7 +62,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"version": "0.26.3"
}

View File

@@ -0,0 +1,22 @@
import { describe, expect, test } from 'vitest';
import { getBezierParameters } from '../gfx/curve.js';
import { PointLocation } from '../gfx/model/index.js';
describe('getBezierParameters', () => {
test('should handle empty path', () => {
expect(() => getBezierParameters([])).not.toThrow();
expect(getBezierParameters([])).toEqual([
new PointLocation(),
new PointLocation(),
new PointLocation(),
new PointLocation(),
]);
});
test('should handle single-point path', () => {
const point = new PointLocation([10, 20]);
expect(getBezierParameters([point])).toEqual([point, point, point, point]);
});
});

View File

@@ -142,6 +142,11 @@ export function getBezierNearestPoint(
export function getBezierParameters(
points: PointLocation[]
): BezierCurveParameters {
if (points.length === 0) {
const point = new PointLocation();
return [point, point, point, point];
}
// Fallback for degenerate Bezier curve (all points are at the same position)
if (points.length === 1) {
const point = points[0];

View File

@@ -5,7 +5,7 @@ export default defineConfig({
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 500,
coverage: {
provider: 'istanbul', // or 'c8'
provider: 'istanbul',
reporter: ['lcov'],
reportsDirectory: '../../../.coverage/global',
},

View File

@@ -33,7 +33,8 @@
"zod": "^3.25.76"
},
"devDependencies": {
"vitest": "^3.2.4"
"@vitest/browser-playwright": "^4.0.18",
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -596,7 +596,7 @@ export class LayerManager extends GfxExtension {
private _updateLayer(
element: GfxModel | GfxLocalElementModel,
props?: Record<string, unknown>,
oldValues?: Record<string, unknown>
_oldValues?: Record<string, unknown>
) {
const modelType = this._getModelType(element);
const isLocalElem = element instanceof GfxLocalElementModel;
@@ -613,16 +613,7 @@ export class LayerManager extends GfxExtension {
};
if (shouldUpdateGroupChildren) {
const group = element as GfxModel & GfxGroupCompatibleInterface;
const oldChildIds = childIdsChanged
? Array.isArray(oldValues?.['childIds'])
? (oldValues['childIds'] as string[])
: this._groupChildSnapshot.get(group.id)
: undefined;
const relatedElements = this._getRelatedGroupElements(group, oldChildIds);
this._refreshElementsInLayer(relatedElements);
this._syncGroupChildSnapshot(group);
this._reset();
return true;
}

View File

@@ -31,6 +31,13 @@ function updateTransform(element: GfxBlockComponent) {
element.style.transform = element.getCSSTransform();
}
function updateZIndex(element: GfxBlockComponent) {
const zIndex = element.toZIndex();
if (element.style.zIndex !== zIndex) {
element.style.zIndex = zIndex;
}
}
function updateBlockVisibility(view: GfxBlockComponent) {
if (view.transformState$.value === 'active') {
view.style.visibility = 'visible';
@@ -58,14 +65,22 @@ function handleGfxConnection(instance: GfxBlockComponent) {
instance.store.slots.blockUpdated.subscribe(({ type, id }) => {
if (id === instance.model.id && type === 'update') {
updateTransform(instance);
updateZIndex(instance);
}
})
);
instance.disposables.add(
instance.gfx.layer.slots.layerUpdated.subscribe(() => {
updateZIndex(instance);
})
);
instance.disposables.add(
effect(() => {
updateBlockVisibility(instance);
updateTransform(instance);
updateZIndex(instance);
})
);
}
@@ -105,17 +120,23 @@ export abstract class GfxBlockComponent<
onBoxSelected(_: BoxSelectionContext) {}
getCSSScaleVal(): number {
const viewport = this.gfx.viewport;
const { zoom, viewScale } = viewport;
return zoom / viewScale;
}
getCSSTransform() {
const viewport = this.gfx.viewport;
const { translateX, translateY, zoom } = viewport;
const { translateX, translateY, zoom, viewScale } = viewport;
const bound = Bound.deserialize(this.model.xywh);
const scaledX = bound.x * zoom;
const scaledY = bound.y * zoom;
const scaledX = (bound.x * zoom) / viewScale;
const scaledY = (bound.y * zoom) / viewScale;
const deltaX = scaledX - bound.x;
const deltaY = scaledY - bound.y;
return `translate(${translateX + deltaX}px, ${translateY + deltaY}px) scale(${zoom})`;
return `translate(${translateX / viewScale + deltaX}px, ${translateY / viewScale + deltaY}px) scale(${this.getCSSScaleVal()})`;
}
getRenderingRect() {
@@ -219,18 +240,12 @@ export function toGfxBlockComponent<
handleGfxConnection(this);
}
// eslint-disable-next-line sonarjs/no-identical-functions
getCSSScaleVal(): number {
return GfxBlockComponent.prototype.getCSSScaleVal.call(this);
}
getCSSTransform() {
const viewport = this.gfx.viewport;
const { translateX, translateY, zoom } = viewport;
const bound = Bound.deserialize(this.model.xywh);
const scaledX = bound.x * zoom;
const scaledY = bound.y * zoom;
const deltaX = scaledX - bound.x;
const deltaY = scaledY - bound.y;
return `translate(${translateX + deltaX}px, ${translateY + deltaY}px) scale(${zoom})`;
return GfxBlockComponent.prototype.getCSSTransform.call(this);
}
// eslint-disable-next-line sonarjs/no-identical-functions

View File

@@ -1,3 +1,4 @@
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
@@ -8,15 +9,14 @@ export default defineConfig({
browser: {
enabled: true,
headless: true,
name: 'chromium',
provider: 'playwright',
instances: [{ browser: 'chromium' }],
provider: playwright(),
isolate: false,
providerOptions: {},
},
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 500,
coverage: {
provider: 'istanbul', // or 'c8'
provider: 'istanbul',
reporter: ['lcov'],
reportsDirectory: '../../../.coverage/std',
},

View File

@@ -29,7 +29,7 @@
"devDependencies": {
"@types/lodash.clonedeep": "^4.5.9",
"@types/lodash.merge": "^4.6.9",
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"exports": {
".": "./src/index.ts",

View File

@@ -7,15 +7,11 @@ export * from './transformer';
export { type IdGenerator, nanoid, uuidv4 } from './utils/id-generator';
export * from './yjs';
const env = (
typeof globalThis !== 'undefined'
? globalThis
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {}
) as Record<string, boolean>;
const env = (typeof globalThis !== 'undefined'
? globalThis
: typeof window !== 'undefined'
? window
: {}) as unknown as Record<string, boolean>;
const importIdentifier = '__ $BLOCKSUITE_STORE$ __';
if (env[importIdentifier] === true) {

View File

@@ -8,7 +8,7 @@ export default defineConfig({
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 500,
coverage: {
provider: 'istanbul', // or 'c8'
provider: 'istanbul',
reporter: ['lcov'],
reportsDirectory: '../../../.coverage/store',
},

View File

@@ -19,7 +19,7 @@
"y-protocols": "^1.0.6"
},
"devDependencies": {
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"peerDependencies": {
"yjs": "*"

View File

@@ -5,7 +5,7 @@ export default defineConfig({
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 500,
coverage: {
provider: 'istanbul', // or 'c8'
provider: 'istanbul',
reporter: ['lcov'],
reportsDirectory: '../../../.coverage/sync',
},

View File

@@ -6,7 +6,7 @@
"dev": "vite",
"build": "tsc",
"test:unit": "vitest --browser.headless --run",
"test:debug": "PWDEBUG=1 npx vitest"
"test:debug": "PWDEBUG=1 npx vitest --browser.headless=false"
},
"sideEffects": false,
"keywords": [],
@@ -41,10 +41,11 @@
],
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"@vitest/browser-playwright": "^4.0.18",
"vite": "^7.2.7",
"vite-plugin-istanbul": "^7.2.1",
"vite-plugin-wasm": "^3.5.0",
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"version": "0.26.3"
}

View File

@@ -6,6 +6,7 @@ import type {
import { ungroupCommand } from '@blocksuite/affine/gfx/group';
import type {
GroupElementModel,
MindmapElementModel,
NoteBlockModel,
} from '@blocksuite/affine/model';
import { generateKeyBetween } from '@blocksuite/affine/std/gfx';
@@ -253,6 +254,40 @@ test('blocks should rerender when their z-index changed', async () => {
assertBlocksContent();
});
test('block host z-index should update after reordering', async () => {
const backId = addNote(doc);
const frontId = addNote(doc);
await wait();
const getBlockHost = (id: string) =>
document.querySelector<HTMLElement>(
`affine-edgeless-root gfx-viewport > [data-block-id="${id}"]`
);
const backHost = getBlockHost(backId);
const frontHost = getBlockHost(frontId);
expect(backHost).not.toBeNull();
expect(frontHost).not.toBeNull();
expect(Number(backHost!.style.zIndex)).toBeLessThan(
Number(frontHost!.style.zIndex)
);
service.crud.updateElement(backId, {
index: service.layer.getReorderedIndex(
service.crud.getElementById(backId)!,
'front'
),
});
await wait();
expect(Number(backHost!.style.zIndex)).toBeGreaterThan(
Number(frontHost!.style.zIndex)
);
});
describe('layer reorder functionality', () => {
let ids: string[] = [];
@@ -428,14 +463,17 @@ describe('group related functionality', () => {
const elements = [
service.crud.addElement('shape', {
shapeType: 'rect',
xywh: '[0,0,100,100]',
})!,
addNote(doc),
service.crud.addElement('shape', {
shapeType: 'rect',
xywh: '[120,0,100,100]',
})!,
addNote(doc),
service.crud.addElement('shape', {
shapeType: 'rect',
xywh: '[240,0,100,100]',
})!,
];
@@ -528,6 +566,35 @@ describe('group related functionality', () => {
expect(service.layer.layers[1].elements[0]).toBe(group);
});
test("change mindmap index should update its nodes' layer", async () => {
const noteId = addNote(doc);
const mindmapId = service.crud.addElement('mindmap', {
children: {
text: 'root',
children: [{ text: 'child' }],
},
})!;
await wait();
const note = service.crud.getElementById(noteId)!;
const mindmap = service.crud.getElementById(
mindmapId
)! as MindmapElementModel;
const root = mindmap.tree.element;
expect(service.layer.getZIndex(root)).toBeGreaterThan(
service.layer.getZIndex(note)
);
mindmap.index = service.layer.getReorderedIndex(mindmap, 'back');
await wait();
expect(service.layer.getZIndex(root)).toBeLessThan(
service.layer.getZIndex(note)
);
});
test('should keep relative index order of elements after group, ungroup, undo, redo', () => {
const edgeless = getDocRootBlock(doc, editor, 'edgeless');
const elementIds = [
@@ -769,6 +836,7 @@ test('indexed canvas should be inserted into edgeless portal when switch to edge
service.crud.addElement('shape', {
shapeType: 'rect',
xywh: '[0,0,100,100]',
})!;
addNote(doc);
@@ -777,6 +845,7 @@ test('indexed canvas should be inserted into edgeless portal when switch to edge
service.crud.addElement('shape', {
shapeType: 'rect',
xywh: '[120,0,100,100]',
})!;
editor.mode = 'page';
@@ -792,10 +861,10 @@ test('indexed canvas should be inserted into edgeless portal when switch to edge
'.indexable-canvas'
)[0] as HTMLCanvasElement;
expect(indexedCanvas.width).toBe(
expect(indexedCanvas.width).toBeLessThanOrEqual(
(surface.renderer as CanvasRenderer).canvas.width
);
expect(indexedCanvas.height).toBe(
expect(indexedCanvas.height).toBeLessThanOrEqual(
(surface.renderer as CanvasRenderer).canvas.height
);
expect(indexedCanvas.width).not.toBe(0);

View File

@@ -1,4 +1,5 @@
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig(_configEnv =>
@@ -18,13 +19,13 @@ export default defineConfig(_configEnv =>
retry: process.env.CI === 'true' ? 3 : 0,
browser: {
enabled: true,
headless: process.env.CI === 'true',
headless: true,
instances: [
{ browser: 'chromium' },
{ browser: 'firefox' },
{ browser: 'webkit' },
],
provider: 'playwright',
provider: playwright(),
isolate: false,
viewport: {
width: 1024,
@@ -32,16 +33,13 @@ export default defineConfig(_configEnv =>
},
},
coverage: {
provider: 'istanbul', // or 'c8'
provider: 'istanbul',
reporter: ['lcov'],
reportsDirectory: '../../.coverage/integration-test',
},
deps: {
interopDefault: true,
},
testTransformMode: {
web: ['src/__tests__/**/*.spec.ts'],
},
},
})
);

View File

@@ -22,7 +22,7 @@
"af": "r affine.ts",
"dev": "yarn affine dev",
"build": "yarn affine build",
"lint:eslint": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" eslint --report-unused-disable-directives-severity=off . --cache",
"lint:eslint": "cross-env NODE_OPTIONS=\"--max-old-space-size=16384\" eslint --report-unused-disable-directives-severity=off . --cache",
"lint:eslint:fix": "yarn lint:eslint --fix --fix-type problem,suggestion,layout",
"lint:prettier": "prettier --ignore-unknown --cache --check .",
"lint:prettier:fix": "prettier --ignore-unknown --cache --write .",
@@ -51,12 +51,12 @@
},
"devDependencies": {
"@affine-tools/cli": "workspace:*",
"@capacitor/cli": "^7.0.0",
"@capacitor/cli": "^8.0.0",
"@eslint/js": "^9.39.2",
"@faker-js/faker": "^10.1.0",
"@istanbuljs/schema": "^0.1.3",
"@magic-works/i18n-codegen": "^0.6.1",
"@playwright/test": "=1.52.0",
"@playwright/test": "=1.58.2",
"@smarttools/eslint-plugin-rxjs": "^1.0.8",
"@taplo/cli": "^0.7.0",
"@toeverything/infra": "workspace:*",
@@ -64,9 +64,9 @@
"@types/node": "^22.0.0",
"@typescript-eslint/parser": "^8.55.0",
"@vanilla-extract/vite-plugin": "^5.0.0",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-istanbul": "^3.2.4",
"@vitest/ui": "^3.2.4",
"@vitest/browser": "^4.0.18",
"@vitest/coverage-istanbul": "^4.0.18",
"@vitest/ui": "^4.0.18",
"cross-env": "^10.1.0",
"electron": "^39.0.0",
"eslint": "^9.39.2",
@@ -90,7 +90,7 @@
"typescript-eslint": "^8.55.0",
"unplugin-swc": "^1.5.9",
"vite": "^7.2.7",
"vitest": "^3.2.4"
"vitest": "^4.0.18"
},
"packageManager": "yarn@4.12.0",
"resolutions": {

View File

@@ -14,13 +14,23 @@ affine_common = { workspace = true, features = [
"napi",
"ydoc-loader",
] }
anyhow = { workspace = true }
chrono = { workspace = true }
file-format = { workspace = true }
image = { workspace = true }
infer = { workspace = true }
libwebp-sys = { workspace = true }
little_exif = { workspace = true }
llm_adapter = { workspace = true, default-features = false, features = [
"ureq-client",
] }
matroska = { workspace = true }
mp4parse = { workspace = true }
napi = { workspace = true, features = ["async"] }
napi-derive = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha3 = { workspace = true }
tiktoken-rs = { workspace = true }
v_htmlescape = { workspace = true }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,9 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
export declare class LlmStreamHandle {
abort(): void
}
export declare class Tokenizer {
count(content: string, allowedSpecial?: Array<string> | undefined | null): number
}
@@ -46,6 +50,16 @@ export declare function getMime(input: Uint8Array): string
export declare function htmlSanitize(input: string): string
export declare function llmDispatch(protocol: string, backendConfigJson: string, requestJson: string): string
export declare function llmDispatchStream(protocol: string, backendConfigJson: string, requestJson: string, callback: ((err: Error | null, arg: string) => void)): LlmStreamHandle
export declare function llmEmbeddingDispatch(protocol: string, backendConfigJson: string, requestJson: string): string
export declare function llmRerankDispatch(protocol: string, backendConfigJson: string, requestJson: string): string
export declare function llmStructuredDispatch(protocol: string, backendConfigJson: string, requestJson: string): string
/**
* Merge updates in form like `Y.applyUpdate(doc, update)` way and return the
* result binary.
@@ -75,6 +89,8 @@ export interface NativeCrawlResult {
export interface NativeMarkdownResult {
title: string
markdown: string
knownUnsupportedBlocks: Array<string>
unknownBlocks: Array<string>
}
export interface NativePageDocContent {
@@ -102,6 +118,8 @@ export declare function parsePageDoc(docBin: Buffer, maxSummaryLength?: number |
export declare function parseWorkspaceDoc(docBin: Buffer): NativeWorkspaceDocContent | null
export declare function processImage(input: Buffer, maxEdge: number, keepExif: boolean): Promise<Buffer>
export declare function readAllDocIdsFromRootDoc(docBin: Buffer, includeTrash?: boolean | undefined | null): Array<string>
/**

View File

@@ -9,6 +9,8 @@ use napi_derive::napi;
pub struct NativeMarkdownResult {
pub title: String,
pub markdown: String,
pub known_unsupported_blocks: Vec<String>,
pub unknown_blocks: Vec<String>,
}
impl From<MarkdownResult> for NativeMarkdownResult {
@@ -16,6 +18,8 @@ impl From<MarkdownResult> for NativeMarkdownResult {
Self {
title: result.title,
markdown: result.markdown,
known_unsupported_blocks: result.known_unsupported_blocks,
unknown_blocks: result.unknown_blocks,
}
}
}

View File

@@ -1,3 +1,4 @@
use matroska::Matroska;
use mp4parse::{TrackType, read_mp4};
use napi_derive::napi;
@@ -8,7 +9,13 @@ pub fn get_mime(input: &[u8]) -> String {
} else {
file_format::FileFormat::from_bytes(input).media_type().to_string()
};
if mimetype == "video/mp4" {
if let Some(container) = matroska_container_kind(input).or(match mimetype.as_str() {
"video/webm" | "application/webm" => Some(ContainerKind::WebM),
"video/x-matroska" | "application/x-matroska" => Some(ContainerKind::Matroska),
_ => None,
}) {
detect_matroska_flavor(input, container, &mimetype)
} else if mimetype == "video/mp4" {
detect_mp4_flavor(input)
} else {
mimetype
@@ -37,3 +44,68 @@ fn detect_mp4_flavor(input: &[u8]) -> String {
Err(_) => "video/mp4".to_string(),
}
}
#[derive(Clone, Copy)]
enum ContainerKind {
WebM,
Matroska,
}
impl ContainerKind {
fn audio_mime(&self) -> &'static str {
match self {
ContainerKind::WebM => "audio/webm",
ContainerKind::Matroska => "audio/x-matroska",
}
}
}
fn detect_matroska_flavor(input: &[u8], container: ContainerKind, fallback: &str) -> String {
match Matroska::open(std::io::Cursor::new(input)) {
Ok(file) => {
let has_video = file.video_tracks().next().is_some();
let has_audio = file.audio_tracks().next().is_some();
if !has_video && has_audio {
container.audio_mime().to_string()
} else {
fallback.to_string()
}
}
Err(_) => fallback.to_string(),
}
}
fn matroska_container_kind(input: &[u8]) -> Option<ContainerKind> {
let header = &input[..1024.min(input.len())];
if header.windows(4).any(|window| window.eq_ignore_ascii_case(b"webm")) {
Some(ContainerKind::WebM)
} else if header.windows(8).any(|window| window.eq_ignore_ascii_case(b"matroska")) {
Some(ContainerKind::Matroska)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
const AUDIO_ONLY_WEBM: &[u8] = include_bytes!("../fixtures/audio-only.webm");
const AUDIO_VIDEO_WEBM: &[u8] = include_bytes!("../fixtures/audio-video.webm");
const AUDIO_ONLY_MATROSKA: &[u8] = include_bytes!("../fixtures/audio-only.mka");
#[test]
fn detects_audio_only_webm_as_audio() {
assert_eq!(get_mime(AUDIO_ONLY_WEBM), "audio/webm");
}
#[test]
fn preserves_video_webm() {
assert_eq!(get_mime(AUDIO_VIDEO_WEBM), "video/webm");
}
#[test]
fn detects_audio_only_matroska_as_audio() {
assert_eq!(get_mime(AUDIO_ONLY_MATROSKA), "audio/x-matroska");
}
}

View File

@@ -0,0 +1,353 @@
use std::io::Cursor;
use anyhow::{Context, Result as AnyResult, bail};
use image::{
AnimationDecoder, DynamicImage, ImageDecoder, ImageFormat, ImageReader,
codecs::{gif::GifDecoder, png::PngDecoder, webp::WebPDecoder},
imageops::FilterType,
metadata::Orientation,
};
use libwebp_sys::{
WEBP_MUX_ABI_VERSION, WebPData, WebPDataClear, WebPDataInit, WebPEncodeRGBA, WebPFree, WebPMuxAssemble,
WebPMuxCreateInternal, WebPMuxDelete, WebPMuxError, WebPMuxSetChunk,
};
use little_exif::{exif_tag::ExifTag, filetype::FileExtension, metadata::Metadata};
use napi::{
Env, Error, Result, Status, Task,
bindgen_prelude::{AsyncTask, Buffer},
};
use napi_derive::napi;
const WEBP_QUALITY: f32 = 80.0;
const MAX_IMAGE_DIMENSION: u32 = 16_384;
const MAX_IMAGE_PIXELS: u64 = 40_000_000;
pub struct AsyncProcessImageTask {
input: Vec<u8>,
max_edge: u32,
keep_exif: bool,
}
#[napi]
impl Task for AsyncProcessImageTask {
type Output = Vec<u8>;
type JsValue = Buffer;
fn compute(&mut self) -> Result<Self::Output> {
process_image_inner(&self.input, self.max_edge, self.keep_exif)
.map_err(|error| Error::new(Status::InvalidArg, error.to_string()))
}
fn resolve(&mut self, _: Env, output: Self::Output) -> Result<Self::JsValue> {
Ok(output.into())
}
}
#[napi]
pub fn process_image(input: Buffer, max_edge: u32, keep_exif: bool) -> AsyncTask<AsyncProcessImageTask> {
AsyncTask::new(AsyncProcessImageTask {
input: input.to_vec(),
max_edge,
keep_exif,
})
}
fn process_image_inner(input: &[u8], max_edge: u32, keep_exif: bool) -> AnyResult<Vec<u8>> {
if max_edge == 0 {
bail!("max_edge must be greater than 0");
}
let format = image::guess_format(input).context("unsupported image format")?;
let (width, height) = read_dimensions(input, format)?;
validate_dimensions(width, height)?;
let mut image = decode_image(input, format)?;
let orientation = read_orientation(input, format)?;
image.apply_orientation(orientation);
if image.width().max(image.height()) > max_edge {
image = image.resize(max_edge, max_edge, FilterType::Lanczos3);
}
let mut output = encode_webp_lossy(&image.into_rgba8())?;
if keep_exif {
preserve_exif(input, format, &mut output)?;
}
Ok(output)
}
fn read_dimensions(input: &[u8], format: ImageFormat) -> AnyResult<(u32, u32)> {
ImageReader::with_format(Cursor::new(input), format)
.into_dimensions()
.context("failed to decode image")
}
fn validate_dimensions(width: u32, height: u32) -> AnyResult<()> {
if width == 0 || height == 0 {
bail!("failed to decode image");
}
if width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION {
bail!("image dimensions exceed limit");
}
if u64::from(width) * u64::from(height) > MAX_IMAGE_PIXELS {
bail!("image pixel count exceeds limit");
}
Ok(())
}
fn decode_image(input: &[u8], format: ImageFormat) -> AnyResult<DynamicImage> {
Ok(match format {
ImageFormat::Gif => {
let decoder = GifDecoder::new(Cursor::new(input)).context("failed to decode image")?;
let frame = decoder
.into_frames()
.next()
.transpose()
.context("failed to decode image")?
.context("image does not contain any frames")?;
DynamicImage::ImageRgba8(frame.into_buffer())
}
ImageFormat::Png => {
let decoder = PngDecoder::new(Cursor::new(input)).context("failed to decode image")?;
if decoder.is_apng().context("failed to decode image")? {
let frame = decoder
.apng()
.context("failed to decode image")?
.into_frames()
.next()
.transpose()
.context("failed to decode image")?
.context("image does not contain any frames")?;
DynamicImage::ImageRgba8(frame.into_buffer())
} else {
DynamicImage::from_decoder(decoder).context("failed to decode image")?
}
}
ImageFormat::WebP => {
let decoder = WebPDecoder::new(Cursor::new(input)).context("failed to decode image")?;
let frame = decoder
.into_frames()
.next()
.transpose()
.context("failed to decode image")?
.context("image does not contain any frames")?;
DynamicImage::ImageRgba8(frame.into_buffer())
}
_ => {
let reader = ImageReader::with_format(Cursor::new(input), format);
let decoder = reader.into_decoder().context("failed to decode image")?;
DynamicImage::from_decoder(decoder).context("failed to decode image")?
}
})
}
fn read_orientation(input: &[u8], format: ImageFormat) -> AnyResult<Orientation> {
Ok(match format {
ImageFormat::Gif => GifDecoder::new(Cursor::new(input))
.context("failed to decode image")?
.orientation()
.context("failed to decode image")?,
ImageFormat::Png => PngDecoder::new(Cursor::new(input))
.context("failed to decode image")?
.orientation()
.context("failed to decode image")?,
ImageFormat::WebP => WebPDecoder::new(Cursor::new(input))
.context("failed to decode image")?
.orientation()
.context("failed to decode image")?,
_ => ImageReader::with_format(Cursor::new(input), format)
.into_decoder()
.context("failed to decode image")?
.orientation()
.context("failed to decode image")?,
})
}
fn encode_webp_lossy(image: &image::RgbaImage) -> AnyResult<Vec<u8>> {
let width = i32::try_from(image.width()).context("image width is too large")?;
let height = i32::try_from(image.height()).context("image height is too large")?;
let stride = width.checked_mul(4).context("image width is too large")?;
let mut output = std::ptr::null_mut();
let encoded_len = unsafe { WebPEncodeRGBA(image.as_ptr(), width, height, stride, WEBP_QUALITY, &mut output) };
if output.is_null() || encoded_len == 0 {
bail!("failed to encode webp");
}
let encoded = unsafe { std::slice::from_raw_parts(output, encoded_len) }.to_vec();
unsafe {
WebPFree(output.cast());
}
Ok(encoded)
}
fn preserve_exif(input: &[u8], format: ImageFormat, output: &mut Vec<u8>) -> AnyResult<()> {
let Some(file_type) = map_exif_file_type(format) else {
return Ok(());
};
let input = input.to_vec();
let Ok(mut metadata) = Metadata::new_from_vec(&input, file_type) else {
return Ok(());
};
metadata.remove_tag(ExifTag::Orientation(vec![1]));
if !metadata.get_ifds().iter().any(|ifd| !ifd.get_tags().is_empty()) {
return Ok(());
}
let encoded_metadata = metadata.encode().context("failed to preserve exif metadata")?;
let source = WebPData {
bytes: output.as_ptr(),
size: output.len(),
};
let exif = WebPData {
bytes: encoded_metadata.as_ptr(),
size: encoded_metadata.len(),
};
let mut assembled = WebPData::default();
let mux = unsafe { WebPMuxCreateInternal(&source, 1, WEBP_MUX_ABI_VERSION as _) };
if mux.is_null() {
bail!("failed to preserve exif metadata");
}
let encoded = (|| -> AnyResult<Vec<u8>> {
if unsafe { WebPMuxSetChunk(mux, c"EXIF".as_ptr(), &exif, 1) } != WebPMuxError::WEBP_MUX_OK {
bail!("failed to preserve exif metadata");
}
WebPDataInit(&mut assembled);
if unsafe { WebPMuxAssemble(mux, &mut assembled) } != WebPMuxError::WEBP_MUX_OK {
bail!("failed to preserve exif metadata");
}
Ok(unsafe { std::slice::from_raw_parts(assembled.bytes, assembled.size) }.to_vec())
})();
unsafe {
WebPDataClear(&mut assembled);
WebPMuxDelete(mux);
}
*output = encoded?;
Ok(())
}
fn map_exif_file_type(format: ImageFormat) -> Option<FileExtension> {
match format {
ImageFormat::Jpeg => Some(FileExtension::JPEG),
ImageFormat::Png => Some(FileExtension::PNG { as_zTXt_chunk: true }),
ImageFormat::Tiff => Some(FileExtension::TIFF),
ImageFormat::WebP => Some(FileExtension::WEBP),
_ => None,
}
}
#[cfg(test)]
mod tests {
use image::{ExtendedColorType, GenericImageView, ImageEncoder, codecs::png::PngEncoder};
use super::*;
fn encode_png(width: u32, height: u32) -> Vec<u8> {
let image = image::RgbaImage::from_pixel(width, height, image::Rgba([255, 0, 0, 255]));
let mut encoded = Vec::new();
PngEncoder::new(&mut encoded)
.write_image(image.as_raw(), width, height, ExtendedColorType::Rgba8)
.unwrap();
encoded
}
fn encode_bmp_header(width: u32, height: u32) -> Vec<u8> {
let mut encoded = Vec::with_capacity(54);
encoded.extend_from_slice(b"BM");
encoded.extend_from_slice(&(54u32).to_le_bytes());
encoded.extend_from_slice(&0u16.to_le_bytes());
encoded.extend_from_slice(&0u16.to_le_bytes());
encoded.extend_from_slice(&(54u32).to_le_bytes());
encoded.extend_from_slice(&(40u32).to_le_bytes());
encoded.extend_from_slice(&(width as i32).to_le_bytes());
encoded.extend_from_slice(&(height as i32).to_le_bytes());
encoded.extend_from_slice(&1u16.to_le_bytes());
encoded.extend_from_slice(&24u16.to_le_bytes());
encoded.extend_from_slice(&0u32.to_le_bytes());
encoded.extend_from_slice(&0u32.to_le_bytes());
encoded.extend_from_slice(&0u32.to_le_bytes());
encoded.extend_from_slice(&0u32.to_le_bytes());
encoded.extend_from_slice(&0u32.to_le_bytes());
encoded.extend_from_slice(&0u32.to_le_bytes());
encoded
}
#[test]
fn process_image_keeps_small_dimensions() {
let png = encode_png(8, 6);
let output = process_image_inner(&png, 512, false).unwrap();
let format = image::guess_format(&output).unwrap();
assert_eq!(format, ImageFormat::WebP);
let decoded = image::load_from_memory(&output).unwrap();
assert_eq!(decoded.dimensions(), (8, 6));
}
#[test]
fn process_image_scales_down_large_dimensions() {
let png = encode_png(1024, 256);
let output = process_image_inner(&png, 512, false).unwrap();
let decoded = image::load_from_memory(&output).unwrap();
assert_eq!(decoded.dimensions(), (512, 128));
}
#[test]
fn process_image_preserves_exif_without_orientation() {
let png = encode_png(8, 8);
let mut png_with_exif = png.clone();
let mut metadata = Metadata::new();
metadata.set_tag(ExifTag::ImageDescription("copilot".to_string()));
metadata.set_tag(ExifTag::Orientation(vec![6]));
metadata
.write_to_vec(&mut png_with_exif, FileExtension::PNG { as_zTXt_chunk: true })
.unwrap();
let output = process_image_inner(&png_with_exif, 512, true).unwrap();
let decoded_metadata = Metadata::new_from_vec(&output, FileExtension::WEBP).unwrap();
assert!(
decoded_metadata
.get_tag(&ExifTag::ImageDescription(String::new()))
.next()
.is_some()
);
assert!(
decoded_metadata
.get_tag(&ExifTag::Orientation(vec![1]))
.next()
.is_none()
);
}
#[test]
fn process_image_rejects_invalid_input() {
let error = process_image_inner(b"not-an-image", 512, false).unwrap_err();
assert_eq!(error.to_string(), "unsupported image format");
}
#[test]
fn process_image_rejects_images_over_dimension_limit_before_decode() {
let bmp = encode_bmp_header(MAX_IMAGE_DIMENSION + 1, 1);
let error = process_image_inner(&bmp, 512, false).unwrap_err();
assert_eq!(error.to_string(), "image dimensions exceed limit");
}
}

View File

@@ -7,6 +7,8 @@ pub mod doc_loader;
pub mod file_type;
pub mod hashcash;
pub mod html_sanitize;
pub mod image;
pub mod llm;
pub mod tiktoken;
use affine_common::napi_utils::map_napi_err;

View File

@@ -0,0 +1,414 @@
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use llm_adapter::{
backend::{
BackendConfig, BackendError, BackendProtocol, DefaultHttpClient, dispatch_embedding_request, dispatch_request,
dispatch_rerank_request, dispatch_stream_events_with, dispatch_structured_request,
},
core::{CoreRequest, EmbeddingRequest, RerankRequest, StreamEvent, StructuredRequest},
middleware::{
MiddlewareConfig, PipelineContext, RequestMiddleware, StreamMiddleware, citation_indexing, clamp_max_tokens,
normalize_messages, run_request_middleware_chain, run_stream_middleware_chain, stream_event_normalize,
tool_schema_rewrite,
},
};
use napi::{
Error, Result, Status,
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
};
use serde::Deserialize;
pub const STREAM_END_MARKER: &str = "__AFFINE_LLM_STREAM_END__";
const STREAM_ABORTED_REASON: &str = "__AFFINE_LLM_STREAM_ABORTED__";
const STREAM_CALLBACK_DISPATCH_FAILED_REASON: &str = "__AFFINE_LLM_STREAM_CALLBACK_DISPATCH_FAILED__";
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
struct LlmMiddlewarePayload {
request: Vec<String>,
stream: Vec<String>,
config: MiddlewareConfig,
}
#[derive(Debug, Clone, Deserialize)]
struct LlmDispatchPayload {
#[serde(flatten)]
request: CoreRequest,
#[serde(default)]
middleware: LlmMiddlewarePayload,
}
#[derive(Debug, Clone, Deserialize)]
struct LlmStructuredDispatchPayload {
#[serde(flatten)]
request: StructuredRequest,
#[serde(default)]
middleware: LlmMiddlewarePayload,
}
#[derive(Debug, Clone, Deserialize)]
struct LlmRerankDispatchPayload {
#[serde(flatten)]
request: RerankRequest,
}
#[napi]
pub struct LlmStreamHandle {
aborted: Arc<AtomicBool>,
}
#[napi]
impl LlmStreamHandle {
#[napi]
pub fn abort(&self) {
self.aborted.store(true, Ordering::SeqCst);
}
}
#[napi(catch_unwind)]
pub fn llm_dispatch(protocol: String, backend_config_json: String, request_json: String) -> Result<String> {
let protocol = parse_protocol(&protocol)?;
let config: BackendConfig = serde_json::from_str(&backend_config_json).map_err(map_json_error)?;
let payload: LlmDispatchPayload = serde_json::from_str(&request_json).map_err(map_json_error)?;
let request = apply_request_middlewares(payload.request, &payload.middleware)?;
let response =
dispatch_request(&DefaultHttpClient::default(), &config, protocol, &request).map_err(map_backend_error)?;
serde_json::to_string(&response).map_err(map_json_error)
}
#[napi(catch_unwind)]
pub fn llm_structured_dispatch(protocol: String, backend_config_json: String, request_json: String) -> Result<String> {
let protocol = parse_protocol(&protocol)?;
let config: BackendConfig = serde_json::from_str(&backend_config_json).map_err(map_json_error)?;
let payload: LlmStructuredDispatchPayload = serde_json::from_str(&request_json).map_err(map_json_error)?;
let request = apply_structured_request_middlewares(payload.request, &payload.middleware)?;
let response = dispatch_structured_request(&DefaultHttpClient::default(), &config, protocol, &request)
.map_err(map_backend_error)?;
serde_json::to_string(&response).map_err(map_json_error)
}
#[napi(catch_unwind)]
pub fn llm_embedding_dispatch(protocol: String, backend_config_json: String, request_json: String) -> Result<String> {
let protocol = parse_protocol(&protocol)?;
let config: BackendConfig = serde_json::from_str(&backend_config_json).map_err(map_json_error)?;
let request: EmbeddingRequest = serde_json::from_str(&request_json).map_err(map_json_error)?;
let response = dispatch_embedding_request(&DefaultHttpClient::default(), &config, protocol, &request)
.map_err(map_backend_error)?;
serde_json::to_string(&response).map_err(map_json_error)
}
#[napi(catch_unwind)]
pub fn llm_rerank_dispatch(protocol: String, backend_config_json: String, request_json: String) -> Result<String> {
let protocol = parse_protocol(&protocol)?;
let config: BackendConfig = serde_json::from_str(&backend_config_json).map_err(map_json_error)?;
let payload: LlmRerankDispatchPayload = serde_json::from_str(&request_json).map_err(map_json_error)?;
let response = dispatch_rerank_request(&DefaultHttpClient::default(), &config, protocol, &payload.request)
.map_err(map_backend_error)?;
serde_json::to_string(&response).map_err(map_json_error)
}
#[napi(catch_unwind)]
pub fn llm_dispatch_stream(
protocol: String,
backend_config_json: String,
request_json: String,
callback: ThreadsafeFunction<String, ()>,
) -> Result<LlmStreamHandle> {
let protocol = parse_protocol(&protocol)?;
let config: BackendConfig = serde_json::from_str(&backend_config_json).map_err(map_json_error)?;
let payload: LlmDispatchPayload = serde_json::from_str(&request_json).map_err(map_json_error)?;
let request = apply_request_middlewares(payload.request, &payload.middleware)?;
let middleware = payload.middleware.clone();
let aborted = Arc::new(AtomicBool::new(false));
let aborted_in_worker = aborted.clone();
std::thread::spawn(move || {
let chain = match resolve_stream_chain(&middleware.stream) {
Ok(chain) => chain,
Err(error) => {
emit_error_event(&callback, error.reason.clone(), "middleware_error");
let _ = callback.call(
Ok(STREAM_END_MARKER.to_string()),
ThreadsafeFunctionCallMode::NonBlocking,
);
return;
}
};
let mut pipeline = StreamPipeline::new(chain, middleware.config.clone());
let mut aborted_by_user = false;
let mut callback_dispatch_failed = false;
let result = dispatch_stream_events_with(&DefaultHttpClient::default(), &config, protocol, &request, |event| {
if aborted_in_worker.load(Ordering::Relaxed) {
aborted_by_user = true;
return Err(BackendError::Http(STREAM_ABORTED_REASON.to_string()));
}
for event in pipeline.process(event) {
let status = emit_stream_event(&callback, &event);
if status != Status::Ok {
callback_dispatch_failed = true;
return Err(BackendError::Http(format!(
"{STREAM_CALLBACK_DISPATCH_FAILED_REASON}:{status}"
)));
}
}
Ok(())
});
if !aborted_by_user {
for event in pipeline.finish() {
if aborted_in_worker.load(Ordering::Relaxed) {
aborted_by_user = true;
break;
}
if emit_stream_event(&callback, &event) != Status::Ok {
callback_dispatch_failed = true;
break;
}
}
}
if let Err(error) = result
&& !aborted_by_user
&& !callback_dispatch_failed
&& !is_abort_error(&error)
&& !is_callback_dispatch_failed_error(&error)
{
emit_error_event(&callback, error.to_string(), "dispatch_error");
}
if !callback_dispatch_failed {
let _ = callback.call(
Ok(STREAM_END_MARKER.to_string()),
ThreadsafeFunctionCallMode::NonBlocking,
);
}
});
Ok(LlmStreamHandle { aborted })
}
fn apply_request_middlewares(request: CoreRequest, middleware: &LlmMiddlewarePayload) -> Result<CoreRequest> {
let chain = resolve_request_chain(&middleware.request)?;
Ok(run_request_middleware_chain(request, &middleware.config, &chain))
}
fn apply_structured_request_middlewares(
request: StructuredRequest,
middleware: &LlmMiddlewarePayload,
) -> Result<StructuredRequest> {
let mut core = request.as_core_request();
core = apply_request_middlewares(core, middleware)?;
Ok(StructuredRequest {
model: core.model,
messages: core.messages,
schema: core
.response_schema
.ok_or_else(|| Error::new(Status::InvalidArg, "Structured request schema is required"))?,
max_tokens: core.max_tokens,
temperature: core.temperature,
reasoning: core.reasoning,
strict: request.strict,
response_mime_type: request.response_mime_type,
})
}
#[derive(Clone)]
struct StreamPipeline {
chain: Vec<StreamMiddleware>,
config: MiddlewareConfig,
context: PipelineContext,
}
impl StreamPipeline {
fn new(chain: Vec<StreamMiddleware>, config: MiddlewareConfig) -> Self {
Self {
chain,
config,
context: PipelineContext::default(),
}
}
fn process(&mut self, event: StreamEvent) -> Vec<StreamEvent> {
run_stream_middleware_chain(event, &mut self.context, &self.config, &self.chain)
}
fn finish(&mut self) -> Vec<StreamEvent> {
self.context.flush_pending_deltas();
self.context.drain_queued_events()
}
}
fn emit_stream_event(callback: &ThreadsafeFunction<String, ()>, event: &StreamEvent) -> Status {
let value = serde_json::to_string(event).unwrap_or_else(|error| {
serde_json::json!({
"type": "error",
"message": format!("failed to serialize stream event: {error}"),
})
.to_string()
});
callback.call(Ok(value), ThreadsafeFunctionCallMode::NonBlocking)
}
fn emit_error_event(callback: &ThreadsafeFunction<String, ()>, message: String, code: &str) {
let error_event = serde_json::to_string(&StreamEvent::Error {
message: message.clone(),
code: Some(code.to_string()),
})
.unwrap_or_else(|_| {
serde_json::json!({
"type": "error",
"message": message,
"code": code,
})
.to_string()
});
let _ = callback.call(Ok(error_event), ThreadsafeFunctionCallMode::NonBlocking);
}
fn is_abort_error(error: &BackendError) -> bool {
matches!(
error,
BackendError::Http(reason) if reason == STREAM_ABORTED_REASON
)
}
fn is_callback_dispatch_failed_error(error: &BackendError) -> bool {
matches!(
error,
BackendError::Http(reason) if reason.starts_with(STREAM_CALLBACK_DISPATCH_FAILED_REASON)
)
}
fn resolve_request_chain(request: &[String]) -> Result<Vec<RequestMiddleware>> {
if request.is_empty() {
return Ok(vec![normalize_messages, tool_schema_rewrite]);
}
request
.iter()
.map(|name| match name.as_str() {
"normalize_messages" => Ok(normalize_messages as RequestMiddleware),
"clamp_max_tokens" => Ok(clamp_max_tokens as RequestMiddleware),
"tool_schema_rewrite" => Ok(tool_schema_rewrite as RequestMiddleware),
_ => Err(Error::new(
Status::InvalidArg,
format!("Unsupported request middleware: {name}"),
)),
})
.collect()
}
fn resolve_stream_chain(stream: &[String]) -> Result<Vec<StreamMiddleware>> {
if stream.is_empty() {
return Ok(vec![stream_event_normalize, citation_indexing]);
}
stream
.iter()
.map(|name| match name.as_str() {
"stream_event_normalize" => Ok(stream_event_normalize as StreamMiddleware),
"citation_indexing" => Ok(citation_indexing as StreamMiddleware),
_ => Err(Error::new(
Status::InvalidArg,
format!("Unsupported stream middleware: {name}"),
)),
})
.collect()
}
fn parse_protocol(protocol: &str) -> Result<BackendProtocol> {
match protocol {
"openai_chat" | "openai-chat" | "openai_chat_completions" | "chat-completions" | "chat_completions" => {
Ok(BackendProtocol::OpenaiChatCompletions)
}
"openai_responses" | "openai-responses" | "responses" => Ok(BackendProtocol::OpenaiResponses),
"anthropic" | "anthropic_messages" | "anthropic-messages" => Ok(BackendProtocol::AnthropicMessages),
"gemini" | "gemini_generate_content" | "gemini-generate-content" => Ok(BackendProtocol::GeminiGenerateContent),
other => Err(Error::new(
Status::InvalidArg,
format!("Unsupported llm backend protocol: {other}"),
)),
}
}
fn map_json_error(error: serde_json::Error) -> Error {
Error::new(Status::InvalidArg, format!("Invalid JSON payload: {error}"))
}
fn map_backend_error(error: BackendError) -> Error {
Error::new(Status::GenericFailure, error.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_parse_supported_protocol_aliases() {
assert!(parse_protocol("openai_chat").is_ok());
assert!(parse_protocol("chat-completions").is_ok());
assert!(parse_protocol("responses").is_ok());
assert!(parse_protocol("anthropic").is_ok());
assert!(parse_protocol("gemini").is_ok());
}
#[test]
fn should_reject_unsupported_protocol() {
let error = parse_protocol("unknown").unwrap_err();
assert_eq!(error.status, Status::InvalidArg);
assert!(error.reason.contains("Unsupported llm backend protocol"));
}
#[test]
fn llm_dispatch_should_reject_invalid_backend_json() {
let error = llm_dispatch("openai_chat".to_string(), "{".to_string(), "{}".to_string()).unwrap_err();
assert_eq!(error.status, Status::InvalidArg);
assert!(error.reason.contains("Invalid JSON payload"));
}
#[test]
fn map_json_error_should_use_invalid_arg_status() {
let parse_error = serde_json::from_str::<serde_json::Value>("{").unwrap_err();
let error = map_json_error(parse_error);
assert_eq!(error.status, Status::InvalidArg);
assert!(error.reason.contains("Invalid JSON payload"));
}
#[test]
fn resolve_request_chain_should_support_clamp_max_tokens() {
let chain = resolve_request_chain(&["normalize_messages".to_string(), "clamp_max_tokens".to_string()]).unwrap();
assert_eq!(chain.len(), 2);
}
#[test]
fn resolve_request_chain_should_reject_unknown_middleware() {
let error = resolve_request_chain(&["unknown".to_string()]).unwrap_err();
assert_eq!(error.status, Status::InvalidArg);
assert!(error.reason.contains("Unsupported request middleware"));
}
#[test]
fn resolve_stream_chain_should_reject_unknown_middleware() {
let error = resolve_stream_chain(&["unknown".to_string()]).unwrap_err();
assert_eq!(error.status, Status::InvalidArg);
assert!(error.reason.contains("Unsupported stream middleware"));
}
}

View File

@@ -6,6 +6,7 @@
# MAILER_HOST=127.0.0.1
# MAILER_PORT=1025
# MAILER_SERVERNAME="mail.example.com"
# MAILER_SENDER="noreply@toeverything.info"
# MAILER_USER="noreply@toeverything.info"
# MAILER_PASSWORD="affine"

View File

@@ -4,17 +4,14 @@
"version": "0.26.3",
"description": "Affine Node.js server",
"type": "module",
"bin": {
"run-test": "./scripts/run-test.ts"
},
"scripts": {
"build": "affine bundle -p @affine/server",
"dev": "nodemon ./src/index.ts",
"dev:mail": "email dev -d src/mails",
"test": "ava --concurrency 1 --serial",
"test:copilot": "ava \"src/__tests__/copilot-*.spec.ts\"",
"test:copilot": "ava \"src/__tests__/copilot/copilot-*.spec.ts\"",
"test:coverage": "c8 ava --concurrency 1 --serial",
"test:copilot:coverage": "c8 ava --timeout=5m \"src/__tests__/copilot-*.spec.ts\"",
"test:copilot:coverage": "c8 ava --timeout=5m \"src/__tests__/copilot/copilot-*.spec.ts\"",
"e2e": "cross-env TEST_MODE=e2e ava --serial",
"e2e:coverage": "cross-env TEST_MODE=e2e c8 ava --serial",
"data-migration": "cross-env NODE_ENV=development SERVER_FLAVOR=script r ./src/index.ts",
@@ -28,19 +25,12 @@
"dependencies": {
"@affine/s3-compat": "workspace:*",
"@affine/server-native": "workspace:*",
"@ai-sdk/anthropic": "^2.0.54",
"@ai-sdk/google": "^2.0.45",
"@ai-sdk/google-vertex": "^3.0.88",
"@ai-sdk/openai": "^2.0.80",
"@ai-sdk/openai-compatible": "^1.0.28",
"@ai-sdk/perplexity": "^2.0.21",
"@apollo/server": "^4.13.0",
"@fal-ai/serverless-client": "^0.15.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
"@google-cloud/opentelemetry-resource-util": "^3.0.0",
"@modelcontextprotocol/sdk": "^1.26.0",
"@nestjs-cls/transactional": "^2.7.0",
"@nestjs-cls/transactional-adapter-prisma": "^1.2.24",
"@nestjs-cls/transactional": "^3.2.0",
"@nestjs-cls/transactional-adapter-prisma": "^1.3.4",
"@nestjs/apollo": "^13.0.4",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/common": "^11.0.21",
@@ -55,18 +45,18 @@
"@node-rs/crc32": "^1.10.6",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^2.2.0",
"@opentelemetry/exporter-prometheus": "^0.211.0",
"@opentelemetry/exporter-prometheus": "^0.212.0",
"@opentelemetry/exporter-zipkin": "^2.2.0",
"@opentelemetry/host-metrics": "^0.38.0",
"@opentelemetry/instrumentation": "^0.211.0",
"@opentelemetry/instrumentation-graphql": "^0.58.0",
"@opentelemetry/instrumentation-http": "^0.211.0",
"@opentelemetry/instrumentation-ioredis": "^0.59.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.57.0",
"@opentelemetry/instrumentation-socket.io": "^0.57.0",
"@opentelemetry/instrumentation": "^0.212.0",
"@opentelemetry/instrumentation-graphql": "^0.60.0",
"@opentelemetry/instrumentation-http": "^0.212.0",
"@opentelemetry/instrumentation-ioredis": "^0.60.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.58.0",
"@opentelemetry/instrumentation-socket.io": "^0.59.0",
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-metrics": "^2.2.0",
"@opentelemetry/sdk-node": "^0.211.0",
"@opentelemetry/sdk-node": "^0.212.0",
"@opentelemetry/sdk-trace-node": "^2.2.0",
"@opentelemetry/semantic-conventions": "^1.38.0",
"@prisma/client": "^6.6.0",
@@ -74,7 +64,6 @@
"@queuedash/api": "^3.16.0",
"@react-email/components": "^0.5.7",
"@socket.io/redis-adapter": "^8.3.0",
"ai": "^5.0.118",
"bullmq": "^5.40.2",
"cookie-parser": "^1.4.7",
"cross-env": "^10.1.0",
@@ -141,8 +130,8 @@
"@types/react-dom": "^19.0.2",
"@types/semver": "^7.5.8",
"@types/sinon": "^21.0.0",
"@types/supertest": "^6.0.2",
"ava": "^6.4.0",
"@types/supertest": "^7.0.0",
"ava": "^7.0.0",
"c8": "^10.1.3",
"nodemon": "^3.1.14",
"react-email": "^4.3.2",

View File

@@ -43,7 +43,9 @@ Generated by [AVA](https://avajs.dev).
> Snapshot 5
Buffer @Uint8Array [
66616b65 20696d61 6765
89504e47 0d0a1a0a 0000000d 49484452 00000001 00000001 08040000 00b51c0c
02000000 0b494441 5478da63 fcff1f00 03030200 efa37c9f 00000000 49454e44
ae426082
]
## should preview link

View File

@@ -12,12 +12,12 @@ Generated by [AVA](https://avajs.dev).
{
messages: [
{
content: 'generate text to text',
content: 'generate text to text stream',
role: 'assistant',
},
],
pinned: false,
tokens: 8,
tokens: 10,
},
]
@@ -27,12 +27,12 @@ Generated by [AVA](https://avajs.dev).
{
messages: [
{
content: 'generate text to text',
content: 'generate text to text stream',
role: 'assistant',
},
],
pinned: false,
tokens: 8,
tokens: 10,
},
]

View File

@@ -4,31 +4,31 @@ import type { ExecutionContext, TestFn } from 'ava';
import ava from 'ava';
import { z } from 'zod';
import { ServerFeature, ServerService } from '../core';
import { AuthService } from '../core/auth';
import { QuotaModule } from '../core/quota';
import { Models } from '../models';
import { CopilotModule } from '../plugins/copilot';
import { prompts, PromptService } from '../plugins/copilot/prompt';
import { ServerFeature, ServerService } from '../../core';
import { AuthService } from '../../core/auth';
import { QuotaModule } from '../../core/quota';
import { Models } from '../../models';
import { CopilotModule } from '../../plugins/copilot';
import { prompts, PromptService } from '../../plugins/copilot/prompt';
import {
CopilotProviderFactory,
CopilotProviderType,
StreamObject,
StreamObjectSchema,
} from '../plugins/copilot/providers';
import { TranscriptionResponseSchema } from '../plugins/copilot/transcript/types';
} from '../../plugins/copilot/providers';
import { TranscriptionResponseSchema } from '../../plugins/copilot/transcript/types';
import {
CopilotChatTextExecutor,
CopilotWorkflowService,
GraphExecutorState,
} from '../plugins/copilot/workflow';
} from '../../plugins/copilot/workflow';
import {
CopilotChatImageExecutor,
CopilotCheckHtmlExecutor,
CopilotCheckJsonExecutor,
} from '../plugins/copilot/workflow/executor';
import { createTestingModule, TestingModule } from './utils';
import { TestAssets } from './utils/copilot';
} from '../../plugins/copilot/workflow/executor';
import { createTestingModule, TestingModule } from '../utils';
import { TestAssets } from '../utils/copilot';
type Tester = {
auth: AuthService;
@@ -118,7 +118,6 @@ test.serial.before(async t => {
enabled: true,
scenarios: {
image: 'flux-1/schnell',
rerank: 'gpt-5-mini',
complex_text_generation: 'gpt-5-mini',
coding: 'gpt-5-mini',
quick_decision_making: 'gpt-5-mini',
@@ -226,6 +225,20 @@ const checkStreamObjects = (result: string) => {
}
};
const parseStreamObjects = (result: string): StreamObject[] => {
const streamObjects = JSON.parse(result);
return z.array(StreamObjectSchema).parse(streamObjects);
};
const getStreamObjectText = (result: string) =>
parseStreamObjects(result)
.filter(
(chunk): chunk is Extract<StreamObject, { type: 'text-delta' }> =>
chunk.type === 'text-delta'
)
.map(chunk => chunk.textDelta)
.join('');
const retry = async (
action: string,
t: ExecutionContext<Tester>,
@@ -445,6 +458,49 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
},
type: 'object' as const,
},
{
name: 'Gemini native text',
promptName: ['Chat With AFFiNE AI'],
messages: [
{
role: 'user' as const,
content:
'In one short sentence, explain what AFFiNE AI is and mention AFFiNE by name.',
},
],
config: { model: 'gemini-2.5-flash' },
verifier: (t: ExecutionContext<Tester>, result: string) => {
assertNotWrappedInCodeBlock(t, result);
t.assert(
result.toLowerCase().includes('affine'),
'should mention AFFiNE'
);
},
prefer: CopilotProviderType.Gemini,
type: 'text' as const,
},
{
name: 'Gemini native stream objects',
promptName: ['Chat With AFFiNE AI'],
messages: [
{
role: 'user' as const,
content:
'Respond with one short sentence about AFFiNE AI and mention AFFiNE by name.',
},
],
config: { model: 'gemini-2.5-flash' },
verifier: (t: ExecutionContext<Tester>, result: string) => {
t.truthy(checkStreamObjects(result), 'should be valid stream objects');
const assembledText = getStreamObjectText(result);
t.assert(
assembledText.toLowerCase().includes('affine'),
'should mention AFFiNE'
);
},
prefer: CopilotProviderType.Gemini,
type: 'object' as const,
},
{
name: 'Should transcribe short audio',
promptName: ['Transcript audio'],
@@ -717,14 +773,13 @@ for (const {
const { factory, prompt: promptService } = t.context;
const prompt = (await promptService.get(promptName))!;
t.truthy(prompt, 'should have prompt');
const provider = (await factory.getProviderByModel(prompt.model, {
const finalConfig = Object.assign({}, prompt.config, config);
const modelId = finalConfig.model || prompt.model;
const provider = (await factory.getProviderByModel(modelId, {
prefer,
}))!;
t.truthy(provider, 'should have provider');
await retry(`action: ${promptName}`, t, async t => {
const finalConfig = Object.assign({}, prompt.config, config);
const modelId = finalConfig.model || prompt.model;
switch (type) {
case 'text': {
const result = await provider.text(
@@ -892,7 +947,7 @@ test(
'should be able to rerank message chunks',
runIfCopilotConfigured,
async t => {
const { factory, prompt } = t.context;
const { factory } = t.context;
await retry('rerank', t, async t => {
const query = 'Is this content relevant to programming?';
@@ -909,14 +964,18 @@ test(
'The stock market is experiencing significant fluctuations.',
];
const p = (await prompt.get('Rerank results'))!;
t.assert(p, 'should have prompt for rerank');
const provider = (await factory.getProviderByModel(p.model))!;
const provider = (await factory.getProviderByModel('gpt-5.2'))!;
t.assert(provider, 'should have provider for rerank');
const scores = await provider.rerank(
{ modelId: p.model },
embeddings.map(e => p.finish({ query, doc: e }))
{ modelId: 'gpt-5.2' },
{
query,
candidates: embeddings.map((text, index) => ({
id: String(index),
text,
})),
}
);
t.is(scores.length, 10, 'should return scores for all chunks');
@@ -931,8 +990,8 @@ test(
t.log('Rerank scores:', scores);
t.is(
scores.filter(s => s > 0.5).length,
4,
'should have 4 related chunks'
5,
'should have 5 related chunks'
);
});
}

View File

@@ -6,25 +6,26 @@ import type { TestFn } from 'ava';
import ava from 'ava';
import Sinon from 'sinon';
import { AppModule } from '../app.module';
import { JobQueue } from '../base';
import { ConfigModule } from '../base/config';
import { AuthService } from '../core/auth';
import { DocReader } from '../core/doc';
import { CopilotContextService } from '../plugins/copilot/context';
import { AppModule } from '../../app.module';
import { JobQueue } from '../../base';
import { ConfigModule } from '../../base/config';
import { AuthService } from '../../core/auth';
import { DocReader } from '../../core/doc';
import { CopilotContextService } from '../../plugins/copilot/context';
import {
CopilotEmbeddingJob,
MockEmbeddingClient,
} from '../plugins/copilot/embedding';
import { prompts, PromptService } from '../plugins/copilot/prompt';
} from '../../plugins/copilot/embedding';
import { ChatMessageCache } from '../../plugins/copilot/message';
import { prompts, PromptService } from '../../plugins/copilot/prompt';
import {
CopilotProviderFactory,
CopilotProviderType,
GeminiGenerativeProvider,
OpenAIProvider,
} from '../plugins/copilot/providers';
import { CopilotStorage } from '../plugins/copilot/storage';
import { MockCopilotProvider } from './mocks';
} from '../../plugins/copilot/providers';
import { CopilotStorage } from '../../plugins/copilot/storage';
import { MockCopilotProvider } from '../mocks';
import {
acceptInviteById,
createTestingApp,
@@ -33,7 +34,7 @@ import {
smallestPng,
TestingApp,
TestUser,
} from './utils';
} from '../utils';
import {
addContextDoc,
addContextFile,
@@ -67,7 +68,7 @@ import {
textToEventStream,
unsplashSearch,
updateCopilotSession,
} from './utils/copilot';
} from '../utils/copilot';
const test = ava as TestFn<{
auth: AuthService;
@@ -416,6 +417,7 @@ test('should be able to use test provider', async t => {
test('should create message correctly', async t => {
const { app } = t.context;
const messageCache = app.get(ChatMessageCache);
{
const { id } = await createWorkspace(app);
@@ -463,6 +465,19 @@ test('should create message correctly', async t => {
new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })
);
t.truthy(messageId, 'should be able to create message with blob');
const message = await messageCache.get(messageId);
const attachment = message?.attachments?.[0] as
| { attachment: string; mimeType: string }
| undefined;
const payload = Buffer.from(
attachment?.attachment.split(',').at(1) || '',
'base64'
);
t.is(attachment?.mimeType, 'image/webp');
t.is(payload.subarray(0, 4).toString('ascii'), 'RIFF');
t.is(payload.subarray(8, 12).toString('ascii'), 'WEBP');
}
// with attachments
@@ -513,7 +528,11 @@ test('should be able to chat with api', async t => {
);
const messageId = await createCopilotMessage(app, sessionId);
const ret = await chatWithText(app, sessionId, messageId);
t.is(ret, 'generate text to text', 'should be able to chat with text');
t.is(
ret,
'generate text to text stream',
'should be able to chat with text'
);
const ret2 = await chatWithTextStream(app, sessionId, messageId);
t.is(
@@ -657,7 +676,7 @@ test('should be able to retry with api', async t => {
const histories = await getHistories(app, { workspaceId: id, docId });
t.deepEqual(
histories.map(h => h.messages.map(m => m.content)),
[['generate text to text', 'generate text to text']],
[['generate text to text stream', 'generate text to text stream']],
'should be able to list history'
);
}
@@ -794,7 +813,7 @@ test('should be able to list history', async t => {
const histories = await getHistories(app, { workspaceId, docId });
t.deepEqual(
histories.map(h => h.messages.map(m => m.content)),
[['hello', 'generate text to text']],
[['hello', 'generate text to text stream']],
'should be able to list history'
);
}
@@ -807,7 +826,7 @@ test('should be able to list history', async t => {
});
t.deepEqual(
histories.map(h => h.messages.map(m => m.content)),
[['generate text to text', 'hello']],
[['generate text to text stream', 'hello']],
'should be able to list history'
);
}
@@ -858,7 +877,7 @@ test('should reject request that user have not permission', async t => {
const histories = await getHistories(app, { workspaceId, docId });
t.deepEqual(
histories.map(h => h.messages.map(m => m.content)),
[['generate text to text']],
[['generate text to text stream']],
'should able to list history'
);

View File

@@ -8,38 +8,35 @@ import ava from 'ava';
import { nanoid } from 'nanoid';
import Sinon from 'sinon';
import { EventBus, JobQueue } from '../base';
import { ConfigModule } from '../base/config';
import { AuthService } from '../core/auth';
import { QuotaModule } from '../core/quota';
import { StorageModule, WorkspaceBlobStorage } from '../core/storage';
import { EventBus, JobQueue } from '../../base';
import { ConfigModule } from '../../base/config';
import { AuthService } from '../../core/auth';
import { QuotaModule } from '../../core/quota';
import { StorageModule, WorkspaceBlobStorage } from '../../core/storage';
import {
ContextCategories,
CopilotSessionModel,
WorkspaceModel,
} from '../models';
import { CopilotModule } from '../plugins/copilot';
import { CopilotContextService } from '../plugins/copilot/context';
import { CopilotCronJobs } from '../plugins/copilot/cron';
} from '../../models';
import { CopilotModule } from '../../plugins/copilot';
import { CopilotContextService } from '../../plugins/copilot/context';
import { CopilotCronJobs } from '../../plugins/copilot/cron';
import {
CopilotEmbeddingJob,
MockEmbeddingClient,
} from '../plugins/copilot/embedding';
import { prompts, PromptService } from '../plugins/copilot/prompt';
} from '../../plugins/copilot/embedding';
import { prompts, PromptService } from '../../plugins/copilot/prompt';
import {
CopilotProviderFactory,
CopilotProviderType,
ModelInputType,
ModelOutputType,
OpenAIProvider,
} from '../plugins/copilot/providers';
import {
CitationParser,
TextStreamParser,
} from '../plugins/copilot/providers/utils';
import { ChatSessionService } from '../plugins/copilot/session';
import { CopilotStorage } from '../plugins/copilot/storage';
import { CopilotTranscriptionService } from '../plugins/copilot/transcript';
} from '../../plugins/copilot/providers';
import { TextStreamParser } from '../../plugins/copilot/providers/utils';
import { ChatSessionService } from '../../plugins/copilot/session';
import { CopilotStorage } from '../../plugins/copilot/storage';
import { CopilotTranscriptionService } from '../../plugins/copilot/transcript';
import {
CopilotChatTextExecutor,
CopilotWorkflowService,
@@ -48,7 +45,7 @@ import {
WorkflowGraphExecutor,
type WorkflowNodeData,
WorkflowNodeType,
} from '../plugins/copilot/workflow';
} from '../../plugins/copilot/workflow';
import {
CopilotChatImageExecutor,
CopilotCheckHtmlExecutor,
@@ -56,16 +53,16 @@ import {
getWorkflowExecutor,
NodeExecuteState,
NodeExecutorType,
} from '../plugins/copilot/workflow/executor';
import { AutoRegisteredWorkflowExecutor } from '../plugins/copilot/workflow/executor/utils';
import { WorkflowGraphList } from '../plugins/copilot/workflow/graph';
import { CopilotWorkspaceService } from '../plugins/copilot/workspace';
import { PaymentModule } from '../plugins/payment';
import { SubscriptionService } from '../plugins/payment/service';
import { SubscriptionStatus } from '../plugins/payment/types';
import { MockCopilotProvider } from './mocks';
import { createTestingModule, TestingModule } from './utils';
import { WorkflowTestCases } from './utils/copilot';
} from '../../plugins/copilot/workflow/executor';
import { AutoRegisteredWorkflowExecutor } from '../../plugins/copilot/workflow/executor/utils';
import { WorkflowGraphList } from '../../plugins/copilot/workflow/graph';
import { CopilotWorkspaceService } from '../../plugins/copilot/workspace';
import { PaymentModule } from '../../plugins/payment';
import { SubscriptionService } from '../../plugins/payment/service';
import { SubscriptionStatus } from '../../plugins/payment/types';
import { MockCopilotProvider } from '../mocks';
import { createTestingModule, TestingModule } from '../utils';
import { WorkflowTestCases } from '../utils/copilot';
type Context = {
auth: AuthService;
@@ -364,6 +361,21 @@ test('should be able to manage chat session', async t => {
});
t.is(newSessionId, sessionId, 'should get same session id');
}
// should create a fresh session when reuseLatestChat is explicitly disabled
{
const newSessionId = await session.create({
userId,
promptName,
...commonParams,
reuseLatestChat: false,
});
t.not(
newSessionId,
sessionId,
'should create new session id when reuseLatestChat is false'
);
}
});
test('should be able to update chat session prompt', async t => {
@@ -645,6 +657,55 @@ test('should be able to generate with message id', async t => {
}
});
test('should preserve file handle attachments when merging user content into prompt', async t => {
const { prompt, session } = t.context;
await prompt.set(promptName, 'model', [
{ role: 'user', content: '{{content}}' },
]);
const sessionId = await session.create({
docId: 'test',
workspaceId: 'test',
userId,
promptName,
pinned: false,
});
const s = (await session.get(sessionId))!;
const message = await session.createMessage({
sessionId,
content: 'Summarize this file',
attachments: [
{
kind: 'file_handle',
fileHandle: 'file_123',
mimeType: 'application/pdf',
},
],
});
await s.pushByMessageId(message);
const finalMessages = s.finish({});
t.deepEqual(finalMessages, [
{
role: 'user',
content: 'Summarize this file',
attachments: [
{
kind: 'file_handle',
fileHandle: 'file_123',
mimeType: 'application/pdf',
},
],
params: {
content: 'Summarize this file',
},
},
]);
});
test('should save message correctly', async t => {
const { prompt, session } = t.context;
@@ -881,6 +942,26 @@ test('should be able to get provider', async t => {
}
});
test('should resolve provider by prefixed model id', async t => {
const { factory } = t.context;
const provider = await factory.getProviderByModel('openai-default/test');
t.truthy(provider, 'should resolve prefixed model id');
t.is(provider?.type, CopilotProviderType.OpenAI);
const result = await provider?.text({ modelId: 'openai-default/test' }, [
{ role: 'user', content: 'hello' },
]);
t.is(result, 'generate text to text');
});
test('should fallback to null when prefixed provider id does not exist', async t => {
const { factory } = t.context;
const provider = await factory.getProviderByModel('unknown/test');
t.is(provider, null);
});
// ==================== workflow ====================
// this test used to preview the final result of the workflow
@@ -1190,149 +1271,6 @@ test('should be able to run image executor', async t => {
Sinon.restore();
});
test('CitationParser should replace citation placeholders with URLs', t => {
const content =
'This is [a] test sentence with [citations [1]] and [[2]] and [3].';
const citations = ['https://example1.com', 'https://example2.com'];
const parser = new CitationParser();
for (const citation of citations) {
parser.push(citation);
}
const result = parser.parse(content) + parser.end();
const expected = [
'This is [a] test sentence with [citations [^1]] and [^2] and [3].',
`[^1]: {"type":"url","url":"${encodeURIComponent(citations[0])}"}`,
`[^2]: {"type":"url","url":"${encodeURIComponent(citations[1])}"}`,
].join('\n');
t.is(result, expected);
});
test('CitationParser should replace chunks of citation placeholders with URLs', t => {
const contents = [
'[[]]',
'This is [',
'a] test sentence ',
'with citations [1',
'] and [',
'[2]] and [[',
'3]] and [[4',
']] and [[5]',
'] and [[6]]',
' and [7',
];
const citations = [
'https://example1.com',
'https://example2.com',
'https://example3.com',
'https://example4.com',
'https://example5.com',
'https://example6.com',
'https://example7.com',
];
const parser = new CitationParser();
for (const citation of citations) {
parser.push(citation);
}
let result = contents.reduce((acc, current) => {
return acc + parser.parse(current);
}, '');
result += parser.end();
const expected = [
'[[]]This is [a] test sentence with citations [^1] and [^2] and [^3] and [^4] and [^5] and [^6] and [7',
`[^1]: {"type":"url","url":"${encodeURIComponent(citations[0])}"}`,
`[^2]: {"type":"url","url":"${encodeURIComponent(citations[1])}"}`,
`[^3]: {"type":"url","url":"${encodeURIComponent(citations[2])}"}`,
`[^4]: {"type":"url","url":"${encodeURIComponent(citations[3])}"}`,
`[^5]: {"type":"url","url":"${encodeURIComponent(citations[4])}"}`,
`[^6]: {"type":"url","url":"${encodeURIComponent(citations[5])}"}`,
`[^7]: {"type":"url","url":"${encodeURIComponent(citations[6])}"}`,
].join('\n');
t.is(result, expected);
});
test('CitationParser should not replace citation already with URLs', t => {
const content =
'This is [a] test sentence with citations [1](https://example1.com) and [[2]](https://example2.com) and [[3](https://example3.com)].';
const citations = [
'https://example4.com',
'https://example5.com',
'https://example6.com',
];
const parser = new CitationParser();
for (const citation of citations) {
parser.push(citation);
}
const result = parser.parse(content) + parser.end();
const expected = [
content,
`[^1]: {"type":"url","url":"${encodeURIComponent(citations[0])}"}`,
`[^2]: {"type":"url","url":"${encodeURIComponent(citations[1])}"}`,
`[^3]: {"type":"url","url":"${encodeURIComponent(citations[2])}"}`,
].join('\n');
t.is(result, expected);
});
test('CitationParser should not replace chunks of citation already with URLs', t => {
const contents = [
'This is [a] test sentence with citations [1',
'](https://example1.com) and [[2]',
'](https://example2.com) and [[3](https://example3.com)].',
];
const citations = [
'https://example4.com',
'https://example5.com',
'https://example6.com',
];
const parser = new CitationParser();
for (const citation of citations) {
parser.push(citation);
}
let result = contents.reduce((acc, current) => {
return acc + parser.parse(current);
}, '');
result += parser.end();
const expected = [
contents.join(''),
`[^1]: {"type":"url","url":"${encodeURIComponent(citations[0])}"}`,
`[^2]: {"type":"url","url":"${encodeURIComponent(citations[1])}"}`,
`[^3]: {"type":"url","url":"${encodeURIComponent(citations[2])}"}`,
].join('\n');
t.is(result, expected);
});
test('CitationParser should replace openai style reference chunks', t => {
const contents = [
'This is [a] test sentence with citations ',
'([example1.com](https://example1.com))',
];
const parser = new CitationParser();
let result = contents.reduce((acc, current) => {
return acc + parser.parse(current);
}, '');
result += parser.end();
const expected = [
contents[0] + '[^1]',
`[^1]: {"type":"url","url":"${encodeURIComponent('https://example1.com')}"}`,
].join('\n');
t.is(result, expected);
});
test('TextStreamParser should format different types of chunks correctly', t => {
// Define interfaces for fixtures
interface BaseFixture {
@@ -2063,25 +2001,23 @@ test('should handle copilot cron jobs correctly', async t => {
});
test('should resolve model correctly based on subscription status and prompt config', async t => {
const { db, session, subscription } = t.context;
const { prompt, session, subscription } = t.context;
// 1) Seed a prompt that has optionalModels and proModels in config
const promptName = 'resolve-model-test';
await db.aiPrompt.create({
data: {
name: promptName,
model: 'gemini-2.5-flash',
messages: {
create: [{ idx: 0, role: 'system', content: 'test' }],
},
config: { proModels: ['gemini-2.5-pro', 'claude-sonnet-4-5@20250929'] },
await prompt.set(
promptName,
'gemini-2.5-flash',
[{ role: 'system', content: 'test' }],
{ proModels: ['gemini-2.5-pro', 'claude-sonnet-4-5@20250929'] },
{
optionalModels: [
'gemini-2.5-flash',
'gemini-2.5-pro',
'claude-sonnet-4-5@20250929',
],
},
});
}
);
// 2) Create a chat session with this prompt
const sessionId = await session.create({
@@ -2106,6 +2042,16 @@ test('should resolve model correctly based on subscription status and prompt con
const model1 = await s.resolveModel(false, 'gemini-2.5-pro');
t.snapshot(model1, 'should honor requested pro model');
const model1WithPrefix = await s.resolveModel(
false,
'openai-default/gemini-2.5-pro'
);
t.is(
model1WithPrefix,
'openai-default/gemini-2.5-pro',
'should honor requested prefixed pro model'
);
const model2 = await s.resolveModel(false, 'not-in-optional');
t.snapshot(model2, 'should fallback to default model');
}
@@ -2119,6 +2065,16 @@ test('should resolve model correctly based on subscription status and prompt con
'should fallback to default model when requesting pro model during trialing'
);
const model3WithPrefix = await s.resolveModel(
true,
'openai-default/gemini-2.5-pro'
);
t.is(
model3WithPrefix,
'gemini-2.5-flash',
'should fallback to default model when requesting prefixed pro model during trialing'
);
const model4 = await s.resolveModel(true, 'gemini-2.5-flash');
t.snapshot(model4, 'should honor requested non-pro model during trialing');
@@ -2141,6 +2097,16 @@ test('should resolve model correctly based on subscription status and prompt con
const model7 = await s.resolveModel(true, 'claude-sonnet-4-5@20250929');
t.snapshot(model7, 'should honor requested pro model during active');
const model7WithPrefix = await s.resolveModel(
true,
'openai-default/claude-sonnet-4-5@20250929'
);
t.is(
model7WithPrefix,
'openai-default/claude-sonnet-4-5@20250929',
'should honor requested prefixed pro model during active'
);
const model8 = await s.resolveModel(true, 'not-in-optional');
t.snapshot(
model8,

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More