From c86cc5bb0109f764862005ee9a48e6d93ea7de42 Mon Sep 17 00:00:00 2001 From: Eyal Alon <3756641+soyeyalon@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:27:57 +0200 Subject: [PATCH 1/6] docs(domql): keep latest on the placeholder npm's page for a package shows the version latest names, so until 1.0 it shows DOMQL's placeholder deprecated. D110 now records why latest stays there: moving it to a preview would make a pre-release the default install, which FormaUI Versioning forbids, and the deprecation is what tells npm install to ask for @preview. --- docs/decisions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/decisions.md b/docs/decisions.md index aca5136..94b21da 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -1696,3 +1696,4 @@ Each entry is one of three kinds: - Shipping the bundle on npm: it carries Nexo inside it, so a host that installs Nexo too would load two, each with its own `NexoError`. - Publishing an npm package of `1.0.0-preview.1` from another commit, which would give a fixed release a package its full gate never built. - Waiting for the NuGet package to depend on Nexo's as well, which changes how a .NET application serves the language, and which [consuming the language](features-status.md#consuming-nexo) plans on its own. + - Moving npm's `latest` to a preview, so the package's page shows it rather than the deprecated placeholder that holds `latest` until 1.0: a pre-release would become the default install, which [FormaUI Versioning's publication](https://github.com/FormaUI/Versioning/blob/v1.0.0-preview.1/docs/packages.md#publication) forbids. npm's page shows the version `latest` names, so it shows the placeholder's deprecation, as `npm install @formaui-net/domql` warns, naming `@preview`. From dec39079937f21a5ef7433cf16a8a8b75f2165da Mon Sep 17 00:00:00 2001 From: Eyal Alon <3756641+soyeyalon@users.noreply.github.com> Date: Thu, 12 Nov 2026 23:27:54 +0200 Subject: [PATCH 2/6] test(domql): read Nexo's linked files at once The link test spawned git once for each Nexo file a document links to, which took about a second alone and passed vitest's five-second limit while the other test files ran beside it. It now reads them all through one git cat-file --batch, and still refuses a link to a file or an anchor the release doesn't hold. --- tests/DocumentLinks.test.js | 39 +++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/tests/DocumentLinks.test.js b/tests/DocumentLinks.test.js index d43f9c9..5b61e55 100644 --- a/tests/DocumentLinks.test.js +++ b/tests/DocumentLinks.test.js @@ -79,20 +79,39 @@ const anchorsOf = text => { /** The Nexo checkout whose history holds the release's tag and the commits a record of the past names: the one `NEXO_SOURCE` names, or the one beside this repository. */ const nexoCheckout = process.env.NEXO_SOURCE ? resolve(process.env.NEXO_SOURCE) : resolve(root, '..', 'formaui-net.Nexo'); -/** A file of Nexo's as a ref holds it, from the checkout's history, or undefined where the ref holds no such file; each is read once for the run, since a tag and a commit never change. */ -const nexoFiles = new Map(); +/** Files of Nexo's as refs hold them, each `ref:path`, read from the checkout's history by one `git cat-file --batch`; a file a ref doesn't hold reads as undefined. */ +const readNexoFiles = keys => { + const output = execFileSync('git', ['-C', nexoCheckout, 'cat-file', '--batch'], { input: keys.map(key => `${key}\n`).join(''), stdio: ['pipe', 'pipe', 'ignore'], maxBuffer: 256 * 1024 * 1024 }); + const files = new Map(); + let offset = 0; + + keys.forEach(key => { + const end = output.indexOf(10, offset); + // A found object's header is its name, its type and its size, and its content follows; a missing one's is its name and `missing`, and nothing follows. + const [, type, size] = output.toString('utf8', offset, end).split(' '); + + offset = end + 1; + if (size === undefined) { + files.set(key, undefined); + + return; + } + + files.set(key, type === 'blob' ? output.toString('utf8', offset, offset + Number(size)) : undefined); + offset += Number(size) + 1; + }); + + return files; +}; + +/** Every file of Nexo's a document links to, as its ref holds it, read together the first time one is asked for, since one process reading them all costs far less than one for each; a tag and a commit never change, so each is read once for the run. */ +let nexoFiles; const nexoFileAt = (ref, path) => { const key = `${ref}:${path}`; - if (!nexoFiles.has(key)) { - try { - nexoFiles.set(key, execFileSync('git', ['-C', nexoCheckout, 'show', key], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })); - } catch { - nexoFiles.set(key, undefined); - } - } + nexoFiles ??= readNexoFiles([...new Set(filesOf('.').flatMap(file => [...readFileSync(resolve(root, file), 'utf8').matchAll(NEXO_LINK)].map(([, linkRef, linkPath]) => `${linkRef}:${linkPath}`)))]); - return nexoFiles.get(key); + return nexoFiles.has(key) ? nexoFiles.get(key) : readNexoFiles([key]).get(key); }; /** The commits of Nexo's that a record of DOMQL's past links to, each with what it holds; every other link names the pinned release, so it moves when the pin does. */ From 5efed13cc915d6672d469e6903031c371a2bd11d Mon Sep 17 00:00:00 2001 From: Eyal Alon <3756641+soyeyalon@users.noreply.github.com> Date: Thu, 12 Nov 2026 23:27:54 +0200 Subject: [PATCH 3/6] docs(domql): say why DOMQL and Nexo queries The README answers why DOMQL exists and why its queries are Nexo scripts, where it compared DOMQL with jQuery alone. It names the work DOMQL takes off each component, what it can't follow, when plain JavaScript is simpler, and what Nexo lets DOMQL learn before, during and after a query's evaluation, keeping the C# half as planned. --- README.md | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 77e28eb..1098362 100644 --- a/README.md +++ b/README.md @@ -95,16 +95,38 @@ console.log(snapshot.items[0].selected); // false The source import above works when serving the repository; the [library guide](nuget/README.md#install-and-load) loads the packaged bundle, and goes on to watching, events, the language and extensions. -## How DOMQL differs from jQuery - -| | jQuery | DOMQL | -| --- | --- | --- | -| Main abstraction | Element collections with methods | Typed requests | -| Reading data | JavaScript assembles the result | A query describes the result | -| Keeping data current | Application code repeats its reads | A watch tracks declared observations | -| Events | Handlers work with events | Subscriptions project events into data | -| Changing the page | DOM manipulation methods | Built-in and module-defined executions, once or continuous, each answering what it did | -| Extension | Plugins | Typed vocabulary modules | +## Why DOMQL + +A component that needs the page's state, such as focus, selection, sizes or scrolling, has to: + +1. read it; +2. observe what changes it; +3. schedule the next read; +4. compare the results; +5. notify whatever uses them; +6. and dispose of its listeners when it's done. + +Each component that does this by hand repeats the work, and does it a little differently. + +DOMQL makes that work one shared contract. The caller describes the data it needs, and DOMQL supplies the vocabulary, the observation, the scheduling and the delivery of results. Events and executions extend the same contract to what happens on the page, and to changes the caller asks for. + +DOMQL is explicit about what it can't follow. Each member declares whether its changes are observable, partly observable or unobserved. A watch of a partly observable member needs its caller's consent, and a watch of an unobserved one is refused, as [how a member changes](spec/watches.md#how-a-member-changes) sets out. + +Plain JavaScript stays the simpler choice for a small, local operation. A DOM utility library helps with each read or event, but the application still owns observing, scheduling and delivering. DOMQL earns its place when those responsibilities recur across an application. + +## Why queries are Nexo scripts + +Nexo gives DOMQL control over what a query means, and sight of how it evaluates: + +| Stage | What DOMQL learns | +| --- | --- | +| Before evaluation | Which declarations a query may use, and whether their observations allow watching it. A member of a value of unknown type is checked against every declaration it could select. | +| During evaluation | Each member the query actually reads, with its receiver and its arguments. A watch observes what its last evaluation read, starting and stopping observations as those reads change, as [dependencies](spec/watches.md#dependencies) sets out. | +| After evaluation | The result as immutable data, with metadata recording where each item came from, where a projection keeps it, which DOMQL uses to compare one result with the next. | + +A query is text or a definition, which DOMQL interprets without running it as JavaScript. So a query can be stored or generated, and the C# half, which waits for its design, is planned to send queries from .NET. + +A JavaScript callback gives none of this by itself. A restricted JavaScript API, or another embedded language, could provide it with integration and enforcement of its own. Nexo provides it as its hosting contract, and DOMQL declares the DOM as a Nexo module like any other. ## Documents From 0b00f05f7b27fee5b286d3d1b9167b2b001e587d Mon Sep 17 00:00:00 2001 From: Eyal Alon <3756641+soyeyalon@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:56:39 +0200 Subject: [PATCH 4/6] docs(domql): tell where DOMQL comes from "Why DOMQL" now opens with what a component gets, then where DOMQL comes from. Forma's scripts each tracked their own part of the page, grew hard to reason about and to cache, and cost a round trip into JavaScript for each answer the C# side needed. "Why queries are Nexo scripts" opens with how the language grew inside DOMQL and became Nexo. --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1098362..61eda68 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,15 @@ The source import above works when serving the repository; the [library guide](n ## Why DOMQL -A component that needs the page's state, such as focus, selection, sizes or scrolling, has to: +With DOMQL, a component describes the page state it needs and receives it as data. DOMQL reads that state, follows its changes and keeps the result current. -1. read it; +DOMQL comes from [Forma](https://github.com/FormaUI/Forma), FormaUI's Blazor component library. Forma's components rely on many scripts: reusable behaviors, operations, and objects that support existing components. Knowledge of the page was spread across them, each script tracking its own part at its own level. As they came to interact, they grew hard to reason about and to cache. Each new case took another script, or another change to one. Each answer the C# side needed was another round trip into JavaScript. + +The idea that led to DOMQL was one place to ask about the page's state, as one queries a database, in place of scripts that each track it. A component would ask which element has the focus, how large the window is, whether a container can scroll and where its scroll stands. + +To answer such a question by hand and keep the answer current, a component has to: + +1. read the state; 2. observe what changes it; 3. schedule the next read; 4. compare the results; @@ -108,7 +114,7 @@ A component that needs the page's state, such as focus, selection, sizes or scro Each component that does this by hand repeats the work, and does it a little differently. -DOMQL makes that work one shared contract. The caller describes the data it needs, and DOMQL supplies the vocabulary, the observation, the scheduling and the delivery of results. Events and executions extend the same contract to what happens on the page, and to changes the caller asks for. +DOMQL makes that work one shared contract: it supplies the vocabulary, the observation, the scheduling and the delivery of results. Events and executions extend the same contract to what happens on the page, and to changes the caller asks for. DOMQL is explicit about what it can't follow. Each member declares whether its changes are observable, partly observable or unobserved. A watch of a partly observable member needs its caller's consent, and a watch of an unobserved one is refused, as [how a member changes](spec/watches.md#how-a-member-changes) sets out. @@ -116,7 +122,9 @@ Plain JavaScript stays the simpler choice for a small, local operation. A DOM ut ## Why queries are Nexo scripts -Nexo gives DOMQL control over what a query means, and sight of how it evaluates: +The questions Forma's components asked came to combine, filter and shape what they read in more and more ways. A language expressed them more naturally than more JavaScript coordinating each case, and gave them consistent semantics. That language grew inside DOMQL and became Nexo. + +Nexo also gives DOMQL control over what a query means, and sight of how it evaluates: | Stage | What DOMQL learns | | --- | --- | From 7f9694a39f69a2c26bb51efa05939aec342cbd8d Mon Sep 17 00:00:00 2001 From: Eyal Alon <3756641+soyeyalon@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:43:18 +0200 Subject: [PATCH 5/6] docs(domql): say why npm latest is a placeholder The README's Install section now says that npm's latest tag holds a deprecated placeholder until 1.0, so a reader who sees the warning on npm knows to install the preview dist-tag. The npm package carries the README, so this change and the two README sections since 1.0.0-preview.2 open 1.0.0-preview.3's changelog entry, and the sources declare that version. The specification stays at 1.0.0-preview.1. --- CHANGELOG.md | 8 ++++++++ README.md | 4 +++- nuget/README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/Versions.mjs | 2 +- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d3502..1017e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Each release of DOMQL's packages, newest first. The npm package `@formaui-net/do - **An entry is dated when its release is cut,** and reads *Unreleased* until then. [Cutting a release](CONTRIBUTING.md#cut-a-release) tags its commit, and its packages are published from that tag. - **A change a caller can notice** is listed in the newest entry while it is unreleased. Once it is released, the change opens the next version's entry, and the sources declare that version in the same commit. + + +## `1.0.0-preview.3` + +*Unreleased · specification `1.0.0-preview.1` · Nexo language `1.0.0-preview.1` · definition format 38* + +- The README, which the npm package carries, says why DOMQL exists, where it and its queries come from, and why npm's `latest` tag holds a deprecated placeholder. Nothing else changes but the version the packages and `domql.info.versions.runtime` report. + ## `1.0.0-preview.2` diff --git a/README.md b/README.md index 61eda68..69ed0ae 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ DOMQL is a JavaScript library for querying the DOM, allowing you to read information from a document once or observe it as it changes. Its queries are written in [Nexo](https://github.com/FormaUI/Nexo/blob/v1.0.0-preview.1/spec/README.md). -> **Preview.** No breaking redesign is planned, and a version that breaks what the one before it allowed says how to migrate. The runtime, `1.0.0-preview.2`, implements DOMQL's specification `1.0.0-preview.1` on Nexo's language `1.0.0-preview.1`, definition version 38. DOMQL declares the DOM as types of its own. It is published to npm and NuGet, as [installing it](#install) sets out, and you can also [build and try it locally](#build-and-try-it). See [the status overview](docs/features-status.md), and [what 1.0 requires](docs/features-status.md#what-1-0-requires). +> **Preview.** No breaking redesign is planned, and a version that breaks what the one before it allowed says how to migrate. The runtime, `1.0.0-preview.3`, implements DOMQL's specification `1.0.0-preview.1` on Nexo's language `1.0.0-preview.1`, definition version 38. DOMQL declares the DOM as types of its own. It is published to npm and NuGet, as [installing it](#install) sets out, and you can also [build and try it locally](#build-and-try-it). See [the status overview](docs/features-status.md), and [what 1.0 requires](docs/features-status.md#what-1-0-requires). ## Features @@ -36,6 +36,8 @@ From NuGet, as one bundle with Nexo inside it, which a .NET web application serv dotnet add package formaui-net.DOMQL --prerelease ``` +Until 1.0, npm's `latest` tag holds a deprecated placeholder, so install `@preview`, as above. + ## Your first query Suppose a panel contains three items: diff --git a/nuget/README.md b/nuget/README.md index a20c56c..4315e78 100644 --- a/nuget/README.md +++ b/nuget/README.md @@ -4,7 +4,7 @@ DOMQL is a JavaScript library for querying the DOM, allowing you to read information from a document once or observe it as it changes. This guide takes you from your first query to watching results, handling events and extending the library. Its queries are written in [Nexo](https://github.com/FormaUI/Nexo/blob/v1.0.0-preview.1/spec/README.md). -> **Preview:** no breaking redesign is planned, and a version that breaks what the one before it allowed says how to migrate. `formaui-net.DOMQL` is published to NuGet as a pre-release, which you [install by name](#install-and-load). The package distributes a JavaScript ES module; the API in this guide runs in JavaScript. Every example here runs on this preview. The runtime, `1.0.0-preview.2`, implements DOMQL's specification `1.0.0-preview.1` on Nexo's language `1.0.0-preview.1`, definition version 38. [The status overview](https://github.com/FormaUI/DOMQL/blob/main/docs/features-status.md) says what is implemented and what is being built. +> **Preview:** no breaking redesign is planned, and a version that breaks what the one before it allowed says how to migrate. `formaui-net.DOMQL` is published to NuGet as a pre-release, which you [install by name](#install-and-load). The package distributes a JavaScript ES module; the API in this guide runs in JavaScript. Every example here runs on this preview. The runtime, `1.0.0-preview.3`, implements DOMQL's specification `1.0.0-preview.1` on Nexo's language `1.0.0-preview.1`, definition version 38. [The status overview](https://github.com/FormaUI/DOMQL/blob/main/docs/features-status.md) says what is implemented and what is being built. ## In this guide diff --git a/package-lock.json b/package-lock.json index 1455068..141ee15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@formaui-net/domql", - "version": "1.0.0-preview.2", + "version": "1.0.0-preview.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@formaui-net/domql", - "version": "1.0.0-preview.2", + "version": "1.0.0-preview.3", "license": "Apache-2.0", "dependencies": { "@formaui-net/nexo": "^1.0.0-preview.1" diff --git a/package.json b/package.json index 66c68c2..badc078 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@formaui-net/domql", - "version": "1.0.0-preview.2", + "version": "1.0.0-preview.3", "description": "DOMQL is a JavaScript library for querying the DOM, allowing you to read information from a document once or observe it as it changes.", "license": "Apache-2.0", "author": "Eyal Alon", diff --git a/src/Versions.mjs b/src/Versions.mjs index 532953c..f2dbdea 100644 --- a/src/Versions.mjs +++ b/src/Versions.mjs @@ -6,7 +6,7 @@ import { nexo } from '@formaui-net/nexo'; export class Versions { /** The runtime release version, which the package and the bundle carry. */ - static runtime = '1.0.0-preview.2'; + static runtime = '1.0.0-preview.3'; /** The DOMQL specification version the implementation follows. */ static specification = '1.0.0-preview.1'; From de3d5742f9a73e32fda8f177b3f1b53d3e506b9e Mon Sep 17 00:00:00 2001 From: Eyal Alon <3756641+soyeyalon@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:44:51 +0200 Subject: [PATCH 6/6] chore(domql): release 1.0.0-preview.3 The third preview, implementing DOMQL's specification 1.0.0-preview.1 on Nexo's language 1.0.0-preview.1, as the second does. Its npm package changes only the README it carries. It dates the changelog's entry, and is tagged v1.0.0-preview.3, whose release publishes both packages. --- CHANGELOG.md | 2 +- docs/features-status.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1017e03..a1af7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Each release of DOMQL's packages, newest first. The npm package `@formaui-net/do ## `1.0.0-preview.3` -*Unreleased · specification `1.0.0-preview.1` · Nexo language `1.0.0-preview.1` · definition format 38* +*November 13, 2026 · specification `1.0.0-preview.1` · Nexo language `1.0.0-preview.1` · definition format 38* - The README, which the npm package carries, says why DOMQL exists, where it and its queries come from, and why npm's `latest` tag holds a deprecated placeholder. Nothing else changes but the version the packages and `domql.info.versions.runtime` report. diff --git a/docs/features-status.md b/docs/features-status.md index ba28834..043ef8b 100644 --- a/docs/features-status.md +++ b/docs/features-status.md @@ -24,7 +24,7 @@ The [specification](../spec/README.md) defines the contract, and the [design](do | --- | --- | | [The C# half](#the-c-half) | Waits for its design | | [Planned capabilities](#planned-capabilities) | The C# half's alone remain, waiting for its design | -| [Release](#release) | Done: `1.0.0-preview.2`, which adds the npm package, is released | +| [Release](#release) | Done: `1.0.0-preview.3`, whose npm package changes only the README it carries, is released | | [What 1.0 requires](#what-1-0-requires) | Six requirements, two met | - **A specification version collects the changes between two releases.** A change to DOMQL's contract joins the [version history](../VERSION-HISTORY.md)'s newest entry while it reads *Unreleased*, and opens the next version's entry once it is released, as [D79](decisions.md#d79) records. @@ -273,7 +273,7 @@ None remains: T1 to T3 are implemented, as [the features](#the-host) show: an im DOMQL releases as [cutting a release](../CONTRIBUTING.md#cut-a-release) sets out, by FormaUI Versioning's path, as [D107](decisions.md#d107) records. Its first release, `1.0.0-preview.1`, is released, tagged `v1.0.0-preview.1`, and `release.yml` publishes its NuGet package, with Nexo inside its bundle, to nuget.org through trusted publishing. nuget.org's policy names `FormaUI/DOMQL`, `release.yml` and the `release` environment. -Its second, `1.0.0-preview.2`, is released, tagged `v1.0.0-preview.2`, and `release.yml` publishes its npm package beside the NuGet package, as [D110](decisions.md#d110) records. npm's trusted publisher names the same repository, workflow and environment, and allows `npm publish`. A deprecated placeholder, `@formaui-net/domql@0.0.0`, holds npm's `latest` dist-tag until 1.0, as Nexo's does, since npm takes a trusted publisher only for a package it holds. +Its second, `1.0.0-preview.2`, is released, tagged `v1.0.0-preview.2`, and `release.yml` publishes its npm package beside the NuGet package, as [D110](decisions.md#d110) records. npm's trusted publisher names the same repository, workflow and environment, and allows `npm publish`. A deprecated placeholder, `@formaui-net/domql@0.0.0`, holds npm's `latest` dist-tag until 1.0, as Nexo's does, since npm takes a trusted publisher only for a package it holds. Its third, `1.0.0-preview.3`, whose npm package changes only the README it carries, is released, tagged `v1.0.0-preview.3`, and published the same way.