├── scripts ├── imported │ ├── config.js │ ├── package.json │ └── index.js ├── example.js ├── log-version.js ├── nop │ ├── package.json │ └── index.js ├── correct-import-extensions.js └── post-build.js ├── .npmignore ├── .gitignore ├── bun.lockb ├── src ├── index.ts ├── tests │ ├── index.tsx │ └── range.tsx ├── types │ └── jsx.d.ts ├── is.ts ├── async-options.ts └── range.tsx ├── .nycrc ├── CONTRIBUTING.md ├── README.md ├── tsconfig.json ├── .github └── workflows │ ├── test-actions.yml │ ├── release-actions.yml │ └── codeql-analysis.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── LICENSE.md ├── import-map-deno.json ├── package.json └── CODE-OF-CONDUCT.md /scripts/imported/config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | coverage -------------------------------------------------------------------------------- /scripts/example.js: -------------------------------------------------------------------------------- 1 | import "../esnext/example/index.js"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | esnext 5 | coverage -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualstate/range/main/bun.lockb -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./range"; 2 | export * from "./async-options"; 3 | -------------------------------------------------------------------------------- /src/tests/index.tsx: -------------------------------------------------------------------------------- 1 | export default 1; 2 | 3 | await import("./range"); 4 | -------------------------------------------------------------------------------- /src/types/jsx.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace JSX { 2 | interface IntrinsicElements 3 | extends Record> {} 4 | } 5 | -------------------------------------------------------------------------------- /scripts/log-version.js: -------------------------------------------------------------------------------- 1 | import("fs") 2 | .then(({ promises }) => promises.readFile("package.json")) 3 | .then(JSON.parse) 4 | .then(({ version }) => console.log(version)); 5 | -------------------------------------------------------------------------------- /scripts/nop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/imported/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | ], 4 | "reporter": [ 5 | "clover", 6 | "json-summary", 7 | "html", 8 | "text-summary" 9 | ], 10 | "branches": 80, 11 | "lines": 80, 12 | "functions": 80, 13 | "statements": 80 14 | } 15 | -------------------------------------------------------------------------------- /scripts/nop/index.js: -------------------------------------------------------------------------------- 1 | export const listenAndServe = undefined; 2 | export const createServer = undefined; 3 | 4 | /** 5 | * @type {any} 6 | */ 7 | const on = () => {}; 8 | /** 9 | * @type {any} 10 | */ 11 | const env = {}; 12 | 13 | /*** 14 | * @type {any} 15 | */ 16 | export default { 17 | env, 18 | on, 19 | exit(arg) {}, 20 | }; 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make by way of an issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | We are open to all ideas big or small, and are greatly appreciative of any and all contributions. 7 | 8 | Please note we have a code of conduct, please follow it in all your interactions with the project. 9 | -------------------------------------------------------------------------------- /scripts/imported/index.js: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | const initialImportPath = 4 | getConfig()["@virtualstate/app-history/test/imported/path"] ?? 5 | "@virtualstate/app-history"; 6 | 7 | if (typeof initialImportPath !== "string") 8 | throw new Error("Expected string import path"); 9 | 10 | export const { AppHistory } = await import(initialImportPath); 11 | 12 | export function getConfig() { 13 | return { 14 | ...getNodeConfig(), 15 | }; 16 | } 17 | 18 | function getNodeConfig() { 19 | if (typeof process === "undefined") return {}; 20 | return JSON.parse(process.env.TEST_CONFIG ?? "{}"); 21 | } 22 | /* c8 ignore end */ 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@virtualstate/range` 2 | 3 | [//]: # (badges) 4 | 5 | ### Support 6 | 7 | ![Node.js supported](https://img.shields.io/badge/node-%3E%3D16.0.0-blue) ![Deno supported](https://img.shields.io/badge/deno-%3E%3D1.17.0-blue) 8 | 9 | ### Test Coverage 10 | 11 | ![100%25 lines covered](https://img.shields.io/badge/lines-100%25-brightgreen) ![100%25 statements covered](https://img.shields.io/badge/statements-100%25-brightgreen) ![100%25 functions covered](https://img.shields.io/badge/functions-100%25-brightgreen) ![100%25 branches covered](https://img.shields.io/badge/branches-100%25-brightgreen) 12 | 13 | [//]: # (badges) 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": ["es2018", "esnext", "dom"], 5 | "types": ["jest", "node"], 6 | "esModuleInterop": true, 7 | "target": "esnext", 8 | "noImplicitAny": true, 9 | "downlevelIteration": true, 10 | "moduleResolution": "node", 11 | "declaration": true, 12 | "sourceMap": true, 13 | "outDir": "esnext", 14 | "baseUrl": "./src", 15 | "jsx": "react", 16 | "jsxFactory": "h", 17 | "jsxFragmentFactory": "createFragment", 18 | "allowJs": true, 19 | "paths": {} 20 | }, 21 | "include": ["src/**/*"], 22 | "typeRoots": ["./node_modules/@types", "src/types"], 23 | "exclude": ["node_modules/@opennetwork/vdom"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/test-actions.yml: -------------------------------------------------------------------------------- 1 | name: test-actions 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | env: 8 | NO_COVERAGE_BADGE_UPDATE: 1 9 | FLAGS: FETCH_SERVICE_DISABLE,POST_CONFIGURE_TEST,PLAYWRIGHT,CONTINUE_ON_ERROR 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: "16.x" 17 | registry-url: "https://registry.npmjs.org" 18 | - uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: "v1.x" 21 | - run: | 22 | yarn install 23 | npx playwright install-deps 24 | - run: yarn build 25 | # yarn coverage === c8 + yarn test 26 | - run: yarn coverage 27 | - run: yarn test:deno 28 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 16, 14, 12 4 | ARG VARIANT="16-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | # RUN su node -c "npm install -g " 17 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 12, 14, 16 8 | "args": { 9 | "VARIANT": "16" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": ["dbaeumer.vscode-eslint"], 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [], 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | // "postCreateCommand": "yarn install", 24 | 25 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 26 | "remoteUser": "node" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Axiom Applied Technologies and Development Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/is.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | import { isLike } from "@virtualstate/focus"; 4 | 5 | export function isAsyncIterable(value: unknown): value is AsyncIterable { 6 | return !!( 7 | isLike>(value) && 8 | typeof value[Symbol.asyncIterator] === "function" 9 | ); 10 | } 11 | 12 | export function isIterable(value: unknown): value is Iterable { 13 | return !!( 14 | isLike>(value) && 15 | typeof value[Symbol.iterator] === "function" 16 | ); 17 | } 18 | 19 | export function isIteratorYieldResult( 20 | value: unknown 21 | ): value is IteratorYieldResult { 22 | return !!( 23 | isLike>>(value) && 24 | typeof value.done === "boolean" && 25 | !value.done 26 | ); 27 | } 28 | 29 | export function isIteratorResult( 30 | value: unknown 31 | ): value is IteratorYieldResult { 32 | return !!( 33 | isLike>>(value) && typeof value.done === "boolean" 34 | ); 35 | } 36 | 37 | export function isRejected( 38 | value: PromiseSettledResult 39 | ): value is PromiseRejectedResult; 40 | export function isRejected( 41 | value: PromiseSettledResult 42 | ): value is R; 43 | export function isRejected( 44 | value: PromiseSettledResult 45 | ): value is R { 46 | return value?.status === "rejected"; 47 | } 48 | 49 | export function isFulfilled( 50 | value: PromiseSettledResult 51 | ): value is PromiseFulfilledResult { 52 | return value?.status === "fulfilled"; 53 | } 54 | 55 | export function isPromise(value: unknown): value is Promise { 56 | return isLike>(value) && typeof value.then === "function"; 57 | } 58 | 59 | export function isArray(value: unknown): value is T[] { 60 | return Array.isArray(value); 61 | } 62 | 63 | export function isDefined(value?: T): value is T { 64 | return typeof value !== "undefined"; 65 | } 66 | -------------------------------------------------------------------------------- /src/async-options.ts: -------------------------------------------------------------------------------- 1 | import { union } from "@virtualstate/union"; 2 | import { isAsyncIterable, isPromise } from "./is"; 3 | 4 | type Options = Record; 5 | type AsyncOption = Promise | AsyncIterable; 6 | /** 7 | * @internal 8 | */ 9 | export type AsyncResult = AsyncIterable<[string, unknown]>; 10 | /** 11 | * @internal 12 | */ 13 | export type AsyncResultRecord = Record; 14 | 15 | /** 16 | * @internal 17 | */ 18 | export async function* generateOptions( 19 | options: Options, 20 | asyncKeys: AsyncResultRecord 21 | ): AsyncIterable> { 22 | const keys = Object.keys(asyncKeys); 23 | for await (const snapshot of union(Object.values(asyncKeys))) { 24 | const currentKeys = snapshot.filter(Boolean).map(([key]) => key); 25 | const every = keys.every((key) => currentKeys.includes(key)); 26 | if (!every) { 27 | continue; 28 | } 29 | const snapshotOptions = { 30 | ...options, 31 | }; 32 | for (const [key, value] of snapshot) { 33 | snapshotOptions[key] = value; 34 | } 35 | yield snapshotOptions; 36 | } 37 | } 38 | 39 | /** 40 | * @internal 41 | */ 42 | export function findAsyncOptions( 43 | options: Record 44 | ): [AsyncResultRecord, boolean] { 45 | const entries = Object.entries(options) 46 | .filter((entry): entry is [string, AsyncOption] => { 47 | const [, value] = entry; 48 | return isAsyncIterable(value) || isPromise(value); 49 | }) 50 | .map(([key, value]) => [key, resolve(key, value)] as const); 51 | return [Object.fromEntries(entries), entries.length > 0]; 52 | 53 | async function* resolve( 54 | key: string, 55 | value: AsyncOption 56 | ): AsyncIterable<[string, unknown]> { 57 | if (isAsyncIterable(value)) { 58 | for await (const snapshot of value) { 59 | yield [key, snapshot]; 60 | } 61 | } else { 62 | const snapshot = await value; 63 | yield [key, snapshot]; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/release-actions.yml: -------------------------------------------------------------------------------- 1 | name: release-actions 2 | on: 3 | push: 4 | branches: 5 | - main 6 | release: 7 | types: 8 | - created 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | env: 13 | NO_COVERAGE_BADGE_UPDATE: 1 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: "16.x" 21 | registry-url: "https://registry.npmjs.org" 22 | - uses: denoland/setup-deno@v1 23 | with: 24 | deno-version: "v1.x" 25 | - run: | 26 | yarn install 27 | - run: yarn build 28 | # yarn coverage === c8 + yarn test 29 | - run: yarn coverage 30 | - run: yarn test:deno 31 | - name: Package Registry Publish - npm 32 | run: | 33 | git config user.name "${{ github.actor }}" 34 | git config user.email "${{ github.actor}}@users.noreply.github.com" 35 | npm set "registry=https://registry.npmjs.org/" 36 | npm set "@virtualstate:registry=https://registry.npmjs.org/" 37 | npm set "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" 38 | npm publish --access=public 39 | continue-on-error: true 40 | env: 41 | YARN_TOKEN: ${{ secrets.YARN_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.YARN_TOKEN }} 43 | NODE_AUTH_TOKEN: ${{ secrets.YARN_TOKEN }} 44 | - uses: actions/setup-node@v2 45 | with: 46 | node-version: "16.x" 47 | registry-url: "https://npm.pkg.github.com" 48 | - name: Package Registry Publish - GitHub 49 | run: | 50 | git config user.name "${{ github.actor }}" 51 | git config user.email "${{ github.actor}}@users.noreply.github.com" 52 | npm set "registry=https://npm.pkg.github.com/" 53 | npm set "@virtualstate:registry=https://npm.pkg.github.com/virtualstate" 54 | npm set "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" 55 | npm publish --access=public 56 | env: 57 | YARN_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | continue-on-error: true 61 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '15 10 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /import-map-deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@opennetwork/http-representation": "https://cdn.skypack.dev/@opennetwork/http-representation", 4 | "@virtualstate/astro-renderer": "https://cdn.skypack.dev/@virtualstate/astro-renderer", 5 | "@virtualstate/dom": "https://cdn.skypack.dev/@virtualstate/dom", 6 | "@virtualstate/examples": "https://cdn.skypack.dev/@virtualstate/examples", 7 | "@virtualstate/fringe": "https://cdn.skypack.dev/@virtualstate/fringe", 8 | "@virtualstate/focus": "https://cdn.skypack.dev/@virtualstate/focus", 9 | "@virtualstate/focus/static-h": "https://cdn.skypack.dev/@virtualstate/focus/static-h", 10 | "@virtualstate/hooks": "https://cdn.skypack.dev/@virtualstate/hooks", 11 | "@virtualstate/hooks-extended": "https://cdn.skypack.dev/@virtualstate/hooks-extended", 12 | "@virtualstate/union": "https://cdn.skypack.dev/@virtualstate/union", 13 | "@virtualstate/x": "https://cdn.skypack.dev/@virtualstate/x", 14 | "@virtualstate/promise": "https://cdn.skypack.dev/@virtualstate/promise", 15 | "chevrotain": "https://cdn.skypack.dev/chevrotain", 16 | "@virtualstate/promise/the-thing": "https://cdn.skypack.dev/@virtualstate/promise/the-thing", 17 | "@virtualstate/app-history": "./src/app-history.ts", 18 | "@virtualstate/app-history/polyfill": "./src/polyfill.ts", 19 | "@virtualstate/app-history/event-target/sync": "./src/event-target/sync-event-target.ts", 20 | "@virtualstate/app-history/event-target/async": "./src/event-target/async-event-target.ts", 21 | "@virtualstate/app-history/event-target": "./src/event-target/sync-event-target.ts", 22 | "@virtualstate/app-history-imported": "./src/app-history.ts", 23 | "@virtualstate/app-history-imported/polyfill": "./src/polyfill.ts", 24 | "@virtualstate/app-history-imported/event-target/sync": "./src/event-target/sync-event-target.ts", 25 | "@virtualstate/app-history-imported/event-target/async": "./src/event-target/async-event-target.ts", 26 | "@virtualstate/app-history-imported/event-target": "./src/event-target/sync-event-target.ts", 27 | "dom-lite": "https://cdn.skypack.dev/dom-lite", 28 | "iterable": "https://cdn.skypack.dev/iterable@6.0.1-beta.5", 29 | "https://cdn.skypack.dev/-/iterable@v5.7.0-CNtyuMJo9f2zFu6CuB1D/dist=es2019,mode=imports/optimized/iterable.js": "https://cdn.skypack.dev/iterable@6.0.1-beta.5", 30 | "uuid": "./src/util/deno-uuid.ts", 31 | "whatwg-url": "https://cdn.skypack.dev/whatwg-url", 32 | "abort-controller": "https://cdn.skypack.dev/abort-controller", 33 | "deno:std/uuid/mod": "https://deno.land/std@0.113.0/uuid/mod.ts", 34 | "deno:std/uuid/v4": "https://deno.land/std@0.113.0/uuid/v4.ts", 35 | "deno:deno_dom/deno-dom-wasm.ts": "https://deno.land/x/deno_dom/deno-dom-wasm.ts", 36 | "urlpattern-polyfill": "https://cdn.skypack.dev/urlpattern-polyfill", 37 | "cheerio": "./scripts/nop/index.js" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtualstate/range", 3 | "version": "1.2.1", 4 | "main": "./esnext/index.js", 5 | "module": "./esnext/index.js", 6 | "types": "./esnext/index.d.ts", 7 | "type": "module", 8 | "sideEffects": false, 9 | "keywords": [], 10 | "exports": { 11 | ".": "./esnext/index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/virtualstate/range.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/virtualstate/range/issues" 19 | }, 20 | "homepage": "https://github.com/virtualstate/range#readme", 21 | "author": "Fabian Cook ", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@virtualstate/kdl": "^1.0.1", 25 | "@virtualstate/focus": "^1.0.1", 26 | "@virtualstate/promise": "^1.1.5", 27 | "abort-controller": "^3.0.0", 28 | "chevrotain": "^10.1.2", 29 | "uuid": "^8.3.2", 30 | "whatwg-url": "^9.1.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/cli": "^7.15.4", 34 | "@babel/core": "^7.15.4", 35 | "@babel/preset-env": "^7.15.4", 36 | "@opennetwork/http-representation": "^3.0.0", 37 | "@types/chance": "^1.1.3", 38 | "@types/jest": "^27.0.1", 39 | "@types/mkdirp": "^1.0.2", 40 | "@types/node": "^17.0.1", 41 | "@types/rimraf": "^3.0.2", 42 | "@types/uuid": "^8.3.3", 43 | "@types/whatwg-url": "^8.2.1", 44 | "@virtualstate/dom": "^2.46.0", 45 | "@virtualstate/examples": "^2.46.0", 46 | "@virtualstate/fringe": "^2.46.1", 47 | "@virtualstate/hooks": "^2.46.0", 48 | "@virtualstate/hooks-extended": "^2.46.0", 49 | "@virtualstate/union": "^2.46.0", 50 | "@virtualstate/x": "^2.46.0", 51 | "c8": "^7.12.0", 52 | "chance": "^1.1.8", 53 | "cheerio": "^1.0.0-rc.10", 54 | "core-js": "^3.17.2", 55 | "dom-lite": "^20.2.0", 56 | "filehound": "^1.17.4", 57 | "jest": "^27.1.0", 58 | "jest-playwright-preset": "^1.7.0", 59 | "mkdirp": "^1.0.4", 60 | "playwright": "^1.17.1", 61 | "rimraf": "^3.0.2", 62 | "ts-jest": "^27.0.5", 63 | "ts-node": "^10.2.1", 64 | "typescript": "^4.7.4", 65 | "urlpattern-polyfill": "^1.0.0-rc2", 66 | "v8-to-istanbul": "^8.1.0" 67 | }, 68 | "scripts": { 69 | "build": "rm -rf esnext && tsc", 70 | "postbuild": "mkdir -p coverage && node scripts/post-build.js", 71 | "prepublishOnly": "npm run build", 72 | "test": "yarn build && node --enable-source-maps esnext/tests/index.js", 73 | "test:deno": "yarn build && deno run --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 74 | "test:deno:r": "yarn build && deno run -r --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 75 | "test:inspect": "yarn build && node --enable-source-maps --inspect-brk esnext/tests/index.js", 76 | "coverage": "yarn build && c8 node esnext/tests/index.js && yarn postbuild" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /scripts/correct-import-extensions.js: -------------------------------------------------------------------------------- 1 | import FileHound from "filehound"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { promisify } from "util"; 5 | // 6 | // const packages = await FileHound.create() 7 | // .paths(`packages`) 8 | // .directory() 9 | // .depth(1) 10 | // .find(); 11 | 12 | const buildPaths = ["esnext"]; 13 | 14 | for (const buildPath of buildPaths) { 15 | const filePaths = await FileHound.create() 16 | .paths(buildPath) 17 | .discard("node_modules") 18 | .ext("js") 19 | .find(); 20 | 21 | await Promise.all( 22 | filePaths.map(async (filePath) => { 23 | const initialContents = await fs.readFile(filePath, "utf-8"); 24 | 25 | const statements = initialContents.match( 26 | /(?:(?:import|export)(?: .+ from)? ".+";|(?:import\(".+"\)))/g 27 | ); 28 | 29 | if (!statements) { 30 | return; 31 | } 32 | 33 | const importMap = process.env.IMPORT_MAP 34 | ? JSON.parse(await fs.readFile(process.env.IMPORT_MAP, "utf-8")) 35 | : undefined; 36 | const contents = await statements.reduce( 37 | async (contentsPromise, statement) => { 38 | const contents = await contentsPromise; 39 | const url = statement.match(/"(.+)"/)[1]; 40 | if (importMap?.imports?.[url]) { 41 | const replacement = importMap.imports[url]; 42 | if (!replacement.includes("./src")) { 43 | return contents.replace( 44 | statement, 45 | statement.replace(url, replacement) 46 | ); 47 | } 48 | const shift = filePath 49 | .split("/") 50 | // Skip top folder + file 51 | .slice(2) 52 | // Replace with shift up directory 53 | .map(() => "..") 54 | .join("/"); 55 | return contents.replace( 56 | statement, 57 | statement.replace( 58 | url, 59 | replacement.replace("./src", shift).replace(/\.tsx?$/, ".js") 60 | ) 61 | ); 62 | } else { 63 | return contents.replace(statement, await getReplacement(url)); 64 | } 65 | 66 | async function getReplacement(url) { 67 | const [stat, indexStat] = await Promise.all([ 68 | fs 69 | .stat(path.resolve(path.dirname(filePath), url + ".js")) 70 | .catch(() => {}), 71 | fs 72 | .stat(path.resolve(path.dirname(filePath), url + "/index.js")) 73 | .catch(() => {}), 74 | ]); 75 | 76 | if (stat && stat.isFile()) { 77 | return statement.replace(url, url + ".js"); 78 | } else if (indexStat && indexStat.isFile()) { 79 | return statement.replace(url, url + "/index.js"); 80 | } 81 | return statement; 82 | } 83 | }, 84 | Promise.resolve(initialContents) 85 | ); 86 | 87 | await fs.writeFile(filePath, contents, "utf-8"); 88 | }) 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [conduct+axiom@fabiancook.dev](mailto:conduct+axiom@fabiancook.dev). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /scripts/post-build.js: -------------------------------------------------------------------------------- 1 | import "./correct-import-extensions.js"; 2 | import { promises as fs } from "fs"; 3 | import { dirname, resolve } from "path"; 4 | 5 | const { pathname } = new URL(import.meta.url); 6 | const cwd = resolve(dirname(pathname), ".."); 7 | 8 | { 9 | // /Volumes/Extreme/Users/fabian/src/virtualstate/esnext/tests/app-history.playwright.wpt.js 10 | // /Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js 11 | 12 | // console.log({ 13 | // cwd, 14 | // path: 15 | // `/Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js` === 16 | // `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 17 | // p: `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 18 | // }); 19 | 20 | // const bundle = await rollup({ 21 | // input: "./esnext/tests/index.js", 22 | // plugins: [ 23 | // ignore([ 24 | // "playwright", 25 | // "fs", 26 | // "path", 27 | // "uuid", 28 | // "cheerio", 29 | // "@virtualstate/app-history", 30 | // "@virtualstate/app-history-imported", 31 | // `${cwd}/esnext/tests/app-history.playwright.js`, 32 | // `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 33 | // `${cwd}/esnext/tests/dependencies-input.js`, 34 | // `${cwd}/esnext/tests/dependencies.js`, 35 | // "./app-history.playwright.js", 36 | // "./app-history.playwright.wpt.js", 37 | // ]), 38 | // nodeResolve(), 39 | // ], 40 | // inlineDynamicImports: true, 41 | // treeshake: { 42 | // preset: "smallest", 43 | // moduleSideEffects: "no-external", 44 | // }, 45 | // }); 46 | // await bundle.write({ 47 | // sourcemap: true, 48 | // output: { 49 | // file: "./esnext/tests/rollup.js", 50 | // }, 51 | // inlineDynamicImports: true, 52 | // format: "cjs", 53 | // interop: "auto", 54 | // globals: { 55 | // "esnext/tests/app-history.playwright.js": "globalThis", 56 | // }, 57 | // }); 58 | } 59 | 60 | if (!process.env.NO_COVERAGE_BADGE_UPDATE) { 61 | const badges = []; 62 | 63 | const { name } = await fs.readFile("package.json", "utf-8").then(JSON.parse); 64 | 65 | badges.push( 66 | "### Support\n\n", 67 | "![Node.js supported](https://img.shields.io/badge/node-%3E%3D16.0.0-blue)", 68 | "![Deno supported](https://img.shields.io/badge/deno-%3E%3D1.17.0-blue)", 69 | // "![Chromium supported](https://img.shields.io/badge/chromium-%3E%3D98.0.4695.0-blue)", 70 | // "![Webkit supported](https://img.shields.io/badge/webkit-%3E%3D15.4-blue)", 71 | // "![Firefox supported](https://img.shields.io/badge/firefox-%3E%3D94.0.1-blue)" 72 | ); 73 | 74 | badges.push( 75 | "\n\n### Test Coverage\n\n" 76 | // `![nycrc config on GitHub](https://img.shields.io/nycrc/${name.replace(/^@/, "")})` 77 | ); 78 | 79 | // const wptResults = await fs 80 | // .readFile("coverage/wpt.results.json", "utf8") 81 | // .then(JSON.parse) 82 | // .catch(() => ({})); 83 | // if (wptResults?.total) { 84 | // const message = `${wptResults.pass}/${wptResults.total}`; 85 | // const name = "Web Platform Tests"; 86 | // badges.push( 87 | // `![${name} ${message}](https://img.shields.io/badge/${encodeURIComponent( 88 | // name 89 | // )}-${encodeURIComponent(message)}-brightgreen)` 90 | // ); 91 | // } 92 | 93 | const coverage = await fs 94 | .readFile("coverage/coverage-summary.json", "utf8") 95 | .then(JSON.parse) 96 | .catch(() => ({})); 97 | const coverageConfig = await fs.readFile(".nycrc", "utf8").then(JSON.parse); 98 | for (const [name, { pct }] of Object.entries(coverage?.total ?? {})) { 99 | const good = coverageConfig[name]; 100 | if (!good) continue; // not configured 101 | const color = pct >= good ? "brightgreen" : "yellow"; 102 | const message = `${pct}%25`; 103 | badges.push( 104 | `![${message} ${name} covered](https://img.shields.io/badge/${name}-${message}-${color})` 105 | ); 106 | } 107 | 108 | const tag = "[//]: # (badges)"; 109 | 110 | const readMe = await fs.readFile("README.md", "utf8"); 111 | const badgeStart = readMe.indexOf(tag); 112 | const badgeStartAfter = badgeStart + tag.length; 113 | if (badgeStart === -1) { 114 | throw new Error(`Expected to find "${tag}" in README.md`); 115 | } 116 | const badgeEnd = badgeStartAfter + readMe.slice(badgeStartAfter).indexOf(tag); 117 | const badgeEndAfter = badgeEnd + tag.length; 118 | const readMeBefore = readMe.slice(0, badgeStart); 119 | const readMeAfter = readMe.slice(badgeEndAfter); 120 | 121 | const readMeNext = `${readMeBefore}${tag}\n\n${badges.join( 122 | " " 123 | )}\n\n${tag}${readMeAfter}`; 124 | await fs.writeFile("README.md", readMeNext); 125 | console.log("Wrote coverage badges!"); 126 | } 127 | -------------------------------------------------------------------------------- /src/range.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getChildrenFromRawNode, 3 | ok, 4 | name, 5 | h, 6 | properties, 7 | isUnknownJSXNode, 8 | possiblePropertiesKeys, 9 | raw, 10 | possibleChildrenKeys, 11 | } from "@virtualstate/focus"; 12 | import { isArray } from "./is"; 13 | import { findAsyncOptions, generateOptions } from "./async-options"; 14 | 15 | const unknownPropertyKeys: readonly unknown[] = possiblePropertiesKeys; 16 | const unknownChildrenKeys: readonly unknown[] = possibleChildrenKeys; 17 | 18 | export async function* Range( 19 | options?: Record, 20 | input?: unknown 21 | ): AsyncIterable { 22 | const array = getChildrenFromRawNode(input); 23 | ok(isArray(array), "Expected an array"); 24 | ok(array.length > 0); 25 | 26 | const [mapped, others] = splitRange(array, options); 27 | 28 | if (!others.length && mapped.length) { 29 | return yield mapped; 30 | } 31 | 32 | ok(!mapped.length, "Use range or values, not both"); 33 | 34 | const [asyncKeys, hasAsyncKeys] = findAsyncOptions(options); 35 | 36 | if (!hasAsyncKeys) { 37 | return yield* getMatch(options); 38 | } 39 | 40 | for await (const snapshotOptions of generateOptions(options, asyncKeys)) { 41 | yield* getMatch(snapshotOptions); 42 | } 43 | 44 | function isRange(node: unknown): boolean { 45 | return name(node) === Range.name; 46 | } 47 | 48 | function replaceMatch(match: unknown, options: Record) { 49 | ok(isUnknownJSXNode(match)); 50 | const snapshotOptions = { 51 | ...properties(match), 52 | ...options, 53 | }; 54 | const node = raw(match); 55 | const withoutProperties = Object.entries(node).filter( 56 | ([key]) => !unknownPropertyKeys.includes(key) 57 | ); 58 | const withoutChildren = withoutProperties.filter( 59 | ([key]) => !unknownChildrenKeys.includes(key) 60 | ); 61 | const template = { 62 | ...Object.fromEntries(withoutProperties), 63 | options: snapshotOptions, 64 | }; 65 | const matchedChildren = matchChildren(template, options); 66 | if (!matchedChildren) return template; 67 | return { 68 | ...Object.fromEntries(withoutChildren), 69 | options: snapshotOptions, 70 | children: matchedChildren, 71 | }; 72 | } 73 | 74 | function matchChildren(template: unknown, options: Record) { 75 | const rawChildren = getChildrenFromRawNode(template); 76 | if (!Array.isArray(rawChildren) || !rawChildren.length) return undefined; 77 | const [mapped, others] = splitRange(rawChildren, options); 78 | if (!mapped.length) return undefined; 79 | ok(!others.length, "Use range or values, not both"); 80 | return mapped; 81 | } 82 | 83 | function* getMatch(options: Record) { 84 | const match = others.find((node) => isMatch(node, options)); 85 | if (match) { 86 | yield replaceMatch(match, options); 87 | } else { 88 | yield* getDefaultMatch(options); 89 | } 90 | } 91 | 92 | function* getDefaultMatch( 93 | options: Record 94 | ): Iterable { 95 | const node = others.at(-1); 96 | ok(typeof node !== "undefined"); 97 | const validators = Object.values(properties(node)).filter(isMatchValidator); 98 | if (validators.length) return undefined; 99 | yield replaceMatch(node, options); 100 | } 101 | 102 | interface MatchValidatorFn { 103 | (arg: unknown): unknown; 104 | } 105 | 106 | function isMatchValidator(value: unknown): value is MatchValidatorFn { 107 | return typeof value === "function"; 108 | } 109 | 110 | function isMatchValidatorEntry( 111 | value: [string, unknown] 112 | ): value is [string, MatchValidatorFn] { 113 | return isMatchValidator(value[1]); 114 | } 115 | 116 | function isMatch(node: unknown, options: Record) { 117 | const expected = Object.entries(properties(node)); 118 | if (expected.length === 0) { 119 | const keys = Object.keys(options); 120 | return keys.length === 0; 121 | } 122 | const validators = expected.filter(isMatchValidatorEntry); 123 | if (validators.length) { 124 | for (const [key, validator] of validators) { 125 | const found = options[key]; 126 | if (!validator(found)) { 127 | return false; 128 | } 129 | } 130 | } else { 131 | for (const [key, value] of expected) { 132 | const found = options[key]; 133 | if (found !== value) { 134 | return false; 135 | } 136 | } 137 | } 138 | return true; 139 | } 140 | 141 | function splitRange(array: unknown[], options: Record) { 142 | const ranges = array.filter(isRange); 143 | const others = ranges.length 144 | ? array.filter((value) => !ranges.includes(value)) 145 | : array; 146 | const mapped = ranges.map((range) => { 147 | const children = getChildrenFromRawNode(range); 148 | const defaults = properties(range); 149 | ok(isArray(children)); 150 | return ( 151 | 152 | {...children} 153 | 154 | ); 155 | }); 156 | return [mapped, others]; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/tests/range.tsx: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | import { 3 | children, 4 | getChildrenFromRawNode, 5 | h, 6 | isComponentFn, 7 | isLike, 8 | name, 9 | ok, 10 | properties, 11 | } from "@virtualstate/focus"; 12 | import { Range } from "../range"; 13 | import { Push } from "@virtualstate/promise"; 14 | 15 | async function* Component(options: Record, input?: unknown) { 16 | const children = getChildrenFromRawNode(input); 17 | console.log(Array.isArray(children) ? children.map(properties) : undefined); 18 | yield {input}; 19 | } 20 | 21 | { 22 | const range = ( 23 | 24 | 25 | 26 | ); 27 | 28 | let snapshot = await children(range); 29 | 30 | console.log(snapshot); 31 | 32 | ok(snapshot.length === 1); 33 | ok(name(snapshot[0]) === "default"); 34 | } 35 | { 36 | const range = ( 37 | 38 | 39 | 40 | 41 | ); 42 | 43 | let snapshot = await children(range); 44 | 45 | console.log(snapshot); 46 | 47 | ok(snapshot.length === 1); 48 | ok(name(snapshot[0]) === "match"); 49 | } 50 | 51 | { 52 | const range = ( 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | 60 | let snapshot = await children(range); 61 | 62 | console.log(snapshot); 63 | 64 | ok(snapshot.length === 1); 65 | ok(name(snapshot[0]) === "default"); 66 | 67 | const rangeOne = {range}; 68 | 69 | snapshot = await children(rangeOne); 70 | 71 | console.log(snapshot); 72 | 73 | ok(snapshot.length === 1); 74 | ok(name(snapshot[0]) === "component"); 75 | 76 | const rangeTwo = {rangeOne}; 77 | snapshot = await children(rangeTwo); 78 | 79 | console.log(snapshot); 80 | 81 | ok(snapshot.length === 1); 82 | ok(name(snapshot[0]) === "match"); 83 | } 84 | 85 | { 86 | const range = ( 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ); 99 | 100 | let snapshot = await children(range); 101 | 102 | console.log(snapshot); 103 | 104 | ok(snapshot.length === 2); 105 | ok(name(snapshot[0]) === "match"); 106 | ok(name(snapshot[1]) === "default"); 107 | 108 | const rangeOne = {range}; 109 | snapshot = await children(rangeOne); 110 | 111 | console.log(snapshot); 112 | 113 | ok(snapshot.length === 2); 114 | ok(name(snapshot[0]) === "match"); 115 | ok(name(snapshot[1]) === "component"); 116 | 117 | const rangeTwo = {rangeOne}; 118 | snapshot = await children(rangeTwo); 119 | 120 | console.log(snapshot); 121 | 122 | ok(snapshot.length === 2); 123 | ok(name(snapshot[0]) === "component"); 124 | ok(name(snapshot[1]) === "match"); 125 | } 126 | 127 | { 128 | const range = ( 129 | 136 | 137 | 138 | 139 | ); 140 | const snapshot = await children(range); 141 | 142 | console.log(snapshot); 143 | 144 | ok(snapshot.length === 1); 145 | ok(name(snapshot[0]) === "match"); 146 | } 147 | 148 | { 149 | const target = new Push(); 150 | const range = ( 151 | 152 | 153 | 154 | 155 | ); 156 | target.push(1); 157 | target.close(); 158 | 159 | const snapshot = await children(range); 160 | 161 | console.log(snapshot); 162 | 163 | ok(snapshot.length === 1); 164 | ok(name(snapshot[0]) === "match"); 165 | } 166 | 167 | { 168 | const target = new Push(); 169 | const range = ( 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | ); 179 | target.push(1); 180 | target.push(2); 181 | target.push(1); 182 | target.close(); 183 | 184 | const seen: string[] = []; 185 | 186 | for await (const snapshot of children(range)) { 187 | const value = name(snapshot[0]); 188 | console.log(snapshot, value); 189 | ok(snapshot.length === 1); 190 | ok(value === "match" || value === "component"); 191 | seen.push(value); 192 | } 193 | 194 | ok(seen.length === 3); 195 | ok(seen[0] === "match"); 196 | ok(seen[1] === "component"); 197 | ok(seen[2] === "match"); 198 | } 199 | 200 | { 201 | const target = new Push(); 202 | const range = ( 203 | 204 | 205 | 206 | 207 | ); 208 | target.close(); 209 | 210 | const snapshot = await children(range); 211 | 212 | console.log(snapshot); 213 | 214 | ok(snapshot.length === 0); 215 | } 216 | 217 | { 218 | const valueTarget = new Push(); 219 | const checkedTarget = new Push(); 220 | const range = ( 221 | 222 | 223 | 224 | 225 | ); 226 | 227 | valueTarget.push(true); 228 | valueTarget.close(); 229 | 230 | async function run() { 231 | await new Promise((resolve) => setTimeout(resolve, 10)); 232 | checkedTarget.push(true); 233 | checkedTarget.close(); 234 | } 235 | 236 | const [snapshot] = await Promise.all([children(range), run()]); 237 | 238 | console.log(snapshot); 239 | 240 | ok(snapshot.length === 1); 241 | ok(name(snapshot[0]) === "match"); 242 | } 243 | 244 | { 245 | const range = ( 246 | 247 | 248 | 249 | 250 | ); 251 | const snapshot = await children(range); 252 | 253 | console.log(snapshot); 254 | 255 | ok(snapshot.length === 1); 256 | ok(name(snapshot[0]) === "match"); 257 | } 258 | 259 | { 260 | const range = ( 261 | 262 | 263 | 264 | ); 265 | const snapshot = await children(range); 266 | 267 | console.log(snapshot, properties(snapshot[0])); 268 | 269 | ok(snapshot.length === 1); 270 | ok(name(snapshot[0]) === "input"); 271 | ok(properties(snapshot[0]).type === "text"); 272 | ok(properties(snapshot[0]).value === "something"); 273 | } 274 | 275 | { 276 | const target = new Push(); 277 | const range = ( 278 | 279 | 280 | 281 | ); 282 | target.push("something"); 283 | target.close(); 284 | 285 | const snapshot = await children(range); 286 | 287 | console.log(snapshot, properties(snapshot[0])); 288 | 289 | ok(snapshot.length === 1); 290 | ok(name(snapshot[0]) === "input"); 291 | ok(properties(snapshot[0]).type === "text"); 292 | ok(properties(snapshot[0]).value === "something"); 293 | } 294 | 295 | { 296 | const range = ( 297 | 298 | 299 | 300 | ); 301 | 302 | const snapshot = await children(range); 303 | 304 | console.log(snapshot, properties(snapshot[0])); 305 | 306 | ok(snapshot.length === 1); 307 | ok(name(snapshot[0]) === "input"); 308 | ok(properties(snapshot[0]).type === "text"); 309 | ok(properties(snapshot[0]).value === "something"); 310 | } 311 | 312 | function isNumber(value: unknown): value is number { 313 | return typeof value === "number"; 314 | } 315 | 316 | function isString(value: unknown): value is string { 317 | return typeof value === "string"; 318 | } 319 | 320 | { 321 | const range = ( 322 | 323 | 324 | 325 | 326 | ); 327 | const snapshot = await children(range); 328 | 329 | console.log(snapshot, properties(snapshot[0])); 330 | 331 | ok(snapshot.length === 1); 332 | ok(name(snapshot[0]) === "input"); 333 | ok(properties(snapshot[0]).type === "number"); 334 | ok(properties(snapshot[0]).value === 1); 335 | } 336 | 337 | { 338 | const range = ( 339 | 340 | 341 | 342 | ); 343 | const snapshot = await children(range); 344 | console.log(snapshot); 345 | ok(snapshot.length === 0); 346 | ok(!snapshot[0]); 347 | } 348 | 349 | { 350 | const range = ( 351 | 352 | 353 | 354 | ); 355 | const snapshot = await children(range); 356 | console.log(snapshot, properties(snapshot[0])); 357 | ok(snapshot.length === 1); 358 | ok(name(snapshot[0]) === "component"); 359 | ok(properties(snapshot[0]).value === 1); 360 | } 361 | 362 | { 363 | const range = ( 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | ); 373 | const snapshot = await children(range); 374 | 375 | console.log(snapshot); 376 | 377 | ok(snapshot.length === 1); 378 | ok(name(snapshot[0]) === "match"); 379 | 380 | const matchSnapshot = await children(snapshot[0]); 381 | console.log(matchSnapshot); 382 | ok(matchSnapshot.length === 1); 383 | ok(name(matchSnapshot[0]) === "input"); 384 | ok(properties(matchSnapshot[0]).type === "number"); 385 | } 386 | 387 | { 388 | const range = ( 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | ); 398 | const snapshot = await children(range); 399 | 400 | console.log(snapshot); 401 | 402 | ok(snapshot.length === 1); 403 | ok(name(snapshot[0]) === "component"); 404 | 405 | const matchSnapshot = await children(snapshot[0]); 406 | console.log(matchSnapshot); 407 | ok(matchSnapshot.length === 1); 408 | ok(name(matchSnapshot[0]) === "input"); 409 | ok(properties(matchSnapshot[0]).type === "number"); 410 | } 411 | 412 | { 413 | const range = ( 414 | 415 | 416 | 417 | 418 | 419 | ); 420 | const snapshot = await children(range); 421 | 422 | console.log(snapshot); 423 | 424 | ok(snapshot.length === 1); 425 | ok(name(snapshot[0]) === "match"); 426 | 427 | const matchSnapshot = await children(snapshot[0]); 428 | console.log(matchSnapshot); 429 | ok(matchSnapshot.length === 1); 430 | ok(name(matchSnapshot[0]) === "input"); 431 | ok(properties(matchSnapshot[0]).type === "text"); 432 | } 433 | --------------------------------------------------------------------------------