├── 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 ├── src ├── line │ ├── index.ts │ ├── jump.ts │ └── splt.tsx ├── index.ts ├── tests │ ├── line │ │ ├── index.tsx │ │ ├── jump.tsx │ │ └── split.tsx │ └── index.tsx ├── trying-to-write-documentation │ ├── index.ts │ ├── line-how.tsx │ └── readme-generator.ts ├── types │ ├── jsx.d.ts │ ├── dom-lite.d.ts │ └── deno-dom.d.ts ├── like.ts └── deferred.ts ├── .npmignore ├── .gitignore ├── .prettierignore ├── .nycrc ├── CONTRIBUTING.md ├── tsconfig.json ├── .github └── workflows │ ├── test-actions.yml │ ├── release-actions.yml │ └── codeql-analysis.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── LICENSE.md ├── README.md ├── import-map-deno.json ├── package.json └── CODE-OF-CONDUCT.md /scripts/imported/config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/line/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./splt"; 2 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./like"; 2 | export * from "./line"; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | esnext 5 | coverage 6 | *.md -------------------------------------------------------------------------------- /src/tests/line/index.tsx: -------------------------------------------------------------------------------- 1 | export default 1; 2 | 3 | await import("./split"); 4 | await import("./jump"); -------------------------------------------------------------------------------- /src/tests/index.tsx: -------------------------------------------------------------------------------- 1 | await import("../trying-to-write-documentation"); 2 | 3 | await import("./line"); 4 | 5 | export default 1; 6 | -------------------------------------------------------------------------------- /src/trying-to-write-documentation/index.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | await import("./line-how"); 4 | 5 | export default 1; 6 | -------------------------------------------------------------------------------- /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 | ], 5 | "reporter": [ 6 | "clover", 7 | "json-summary", 8 | "html", 9 | "text-summary" 10 | ], 11 | "branches": 80, 12 | "lines": 80, 13 | "functions": 80, 14 | "statements": 80 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/types/dom-lite.d.ts: -------------------------------------------------------------------------------- 1 | declare module "dom-lite" { 2 | namespace Default { 3 | export const document: Document; 4 | 5 | export const HTMLElement: { 6 | prototype: HTMLElement; 7 | new (): HTMLElement; 8 | }; 9 | export const Node: { 10 | prototype: Node; 11 | new (): Node; 12 | }; 13 | } 14 | 15 | export type DOMNamespace = typeof Default; 16 | 17 | export default Default; 18 | } 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/like.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | export function isLike(value: unknown, ...and: unknown[]): value is T { 4 | if (!and.length) return !!value; 5 | return !!value && and.every((value) => !!value); 6 | } 7 | 8 | export function ok(value: unknown, message?: string): asserts value; 9 | export function ok(value: unknown, message?: string): asserts value is T; 10 | export function ok(value: unknown, message?: string): asserts value { 11 | if (!value) { 12 | throw new Error(message ?? "Expected value"); 13 | } 14 | } -------------------------------------------------------------------------------- /src/types/deno-dom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "https://deno.land/x/deno_dom/deno-dom-wasm.ts" { 2 | export class DOMParser { 3 | parseFromString(string: string, type: string): Document; 4 | } 5 | 6 | class ElementImpl extends Element {} 7 | class NodeImpl extends Node {} 8 | class HTMLElementImpl extends HTMLElement {} 9 | class DocumentImpl extends Document {} 10 | 11 | export { 12 | ElementImpl as Element, 13 | NodeImpl as Node, 14 | HTMLElementImpl as HTMLElement, 15 | DocumentImpl as Document, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/trying-to-write-documentation/line-how.tsx: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | import { ok, h, descendants } from "@virtualstate/focus"; 4 | 5 | /** 6 | * This is living documentation, change this code, and on build, README.md will be updated 7 | * 8 | * Comments starting with /* (and not /**) will be treated as markdown 9 | * Code is treated as codeblocks 10 | * 11 | * To split code up, add an empty comment 12 | * A comment must have its starting & ending markers on their own lines 13 | */ 14 | 15 | /* 16 | # Hello 17 | 18 | 19 | 20 | */ 21 | 22 | export default 1; 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/tests/line/jump.tsx: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | import { 3 | h, 4 | properties, 5 | } from "@virtualstate/focus"; 6 | import { ok, split } from "@virtualstate/line"; 7 | import {jump} from "../../line/jump"; 8 | 9 | async function* View() { 10 | yield ( 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | const { config } = split(, { 18 | keep: true 19 | }); 20 | const { key } = jump(config); 21 | 22 | ok(properties(await key).value === 1); 23 | 24 | const { key: key2 } = jump( 25 | 26 | 27 | 28 | ); 29 | 30 | ok(properties(await key2).value === 2); 31 | 32 | console.log({ 33 | key: await key, 34 | key2: await key2 35 | }); 36 | -------------------------------------------------------------------------------- /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": false, 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 | "#line": ["./line"], 21 | "@virtualstate/line": ["./"] 22 | } 23 | }, 24 | "include": ["src/**/*"], 25 | "typeRoots": ["./node_modules/@types", "src/types"], 26 | "exclude": ["node_modules/@opennetwork/vdom"] 27 | } 28 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/deferred.ts: -------------------------------------------------------------------------------- 1 | import {ok} from "./like"; 2 | 3 | export interface Deferred { 4 | resolve(value: T): void; 5 | reject(reason: unknown): void; 6 | promise: Promise; 7 | readonly settled: boolean; 8 | } 9 | 10 | export function deferred(): Deferred { 11 | let resolve: Deferred["resolve"] | undefined = undefined, 12 | reject: Deferred["reject"] | undefined = undefined, 13 | settled = false; 14 | const promise = new Promise((resolveFn, rejectFn) => { 15 | resolve = (value) => { 16 | settled = true; 17 | resolveFn(value); 18 | }; 19 | reject = reason => { 20 | settled = true 21 | rejectFn(reason); 22 | }; 23 | }); 24 | ok(resolve); 25 | ok(reject); 26 | return { 27 | get settled() { 28 | return settled; 29 | }, 30 | resolve, 31 | reject, 32 | promise, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/line/jump.ts: -------------------------------------------------------------------------------- 1 | import {children as base, isFragment} from "@virtualstate/focus"; 2 | import {union} from "@virtualstate/union"; 3 | import {Split, split, SplitOptions} from "./splt"; 4 | 5 | export function jump(input: unknown, options: SplitOptions = {}): Split { 6 | return split({ 7 | async * [Symbol.asyncIterator]() { 8 | for await (const snapshot of base(input)) { 9 | if (isFragment(input)) { 10 | for await (const snapshots of union( 11 | snapshot.map( 12 | node => base(node) 13 | ) 14 | )) { 15 | yield snapshots 16 | .filter(Boolean) 17 | .flatMap(value => value); 18 | } 19 | } else { 20 | yield snapshot; 21 | } 22 | } 23 | } 24 | }, options); 25 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@virtualstate/line` 2 | 3 | This project is in semver alpha stage 4 | 5 | [//]: # (badges) 6 | 7 | ### Support 8 | 9 | ![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) 10 | 11 | ### Test Coverage 12 | 13 | ![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) 14 | 15 | [//]: # (badges) 16 | 17 | [//]: # (src/trying-to-write-documentation/line-how.tsx) 18 | 19 | import { ok, h, descendants } from "@virtualstate/focus"; 20 | 21 | /** 22 | * This is living documentation, change this code, and on build, README.md will be updated 23 | * 24 | * Comments starting with /* (and not /**) will be treated as markdown 25 | * Code is treated as codeblocks 26 | * 27 | * To split code up, add an empty comment 28 | * A comment must have its starting & ending markers on their own lines 29 | 30 | # Hello 31 | 32 | [//]: # (src/trying-to-write-documentation/line-how.tsx) 33 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/trying-to-write-documentation/readme-generator.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | import { promises as fs } from "fs"; 4 | import { extname } from "path"; 5 | 6 | const readme = await fs.readFile("README.md", "utf-8"); 7 | let lines = readme.split("\n"); 8 | 9 | const badgeMatch = /^\[\/\/]:\s+#\s+\(([^)]+)\)\s*$/; 10 | 11 | const badges = lines.filter((line) => badgeMatch.test(line)); 12 | 13 | const replacedNames = new Set(); 14 | 15 | for (const badge of badges) { 16 | const [, name] = badge.match(badgeMatch); 17 | if (replacedNames.has(name)) continue; 18 | if (!/\.[tj]sx?$/.test(name)) continue; 19 | replacedNames.add(name); 20 | 21 | const contents = await fs.readFile(name, "utf-8").catch(() => ""); 22 | if (!contents) continue; 23 | 24 | const extension = extname(name).replace(/^\./, ""); 25 | const codeType = 26 | { 27 | tsx: "typescript jsx", 28 | ts: "typescript", 29 | jsx: "javascript jsx", 30 | js: "javascript", 31 | }[extension] || "typescript"; 32 | 33 | const markdown = splitAndJoin(contents, codeType); 34 | 35 | const startIndex = lines.indexOf(badge) + 1; 36 | const remaining = lines.slice(startIndex); 37 | const endIndex = remaining.indexOf(badge); 38 | lines = [ 39 | ...lines.slice(0, startIndex), 40 | `\n${markdown}\n`, 41 | ...remaining.slice(endIndex), 42 | ]; 43 | } 44 | 45 | await fs.writeFile("README.md", lines.join("\n"), "utf-8"); 46 | 47 | function splitAndJoin(code: string, codeType: string) { 48 | const lines = code.split("\n"); 49 | const indexed = Object.entries(lines); 50 | const commentStart = indexed 51 | .filter(([, line]) => /^\s*\/\*/.test(line) && !/^\s*\/\*{2}/.test(line)) 52 | .map(([index]) => +index); 53 | 54 | const blocks = []; 55 | 56 | // Start at the first comment 57 | for (let index = commentStart[0]; index < lines.length; index += 1) { 58 | if (commentStart[0] === index) { 59 | commentStart.shift(); 60 | const endIndex = lines.findIndex( 61 | (line, lineIndex) => lineIndex > index && /\*\/\s*$/.test(line) 62 | ); 63 | if (endIndex === -1) { 64 | throw new Error("Expected to find end of comment"); 65 | } 66 | const comment = lines.slice(index + 1, endIndex).join("\n"); 67 | blocks.push(comment.trim()); 68 | index = endIndex; 69 | } else { 70 | const block = lines 71 | .slice(index, commentStart[0] ?? lines.length + 1) 72 | .join("\n") 73 | .replace(/^\s*export default 1;?\s*$/m, "") 74 | .trim(); 75 | 76 | if (!block) continue; 77 | blocks.push(`\`\`\`${codeType}\n${block}\n\`\`\``); 78 | index = commentStart[0] - 1; 79 | } 80 | } 81 | 82 | return blocks.join("\n\n"); 83 | } 84 | -------------------------------------------------------------------------------- /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/hooks": "https://cdn.skypack.dev/@virtualstate/hooks", 9 | "@virtualstate/hooks-extended": "https://cdn.skypack.dev/@virtualstate/hooks-extended", 10 | "@virtualstate/union": "https://cdn.skypack.dev/@virtualstate/union", 11 | "@virtualstate/x": "https://cdn.skypack.dev/@virtualstate/x", 12 | "@virtualstate/promise": "https://cdn.skypack.dev/@virtualstate/promise", 13 | "@virtualstate/promise/the-thing": "https://cdn.skypack.dev/@virtualstate/promise/the-thing", 14 | "@virtualstate/app-history": "./src/app-history.ts", 15 | "@virtualstate/app-history/polyfill": "./src/polyfill.ts", 16 | "@virtualstate/app-history/event-target/sync": "./src/event-target/sync-event-target.ts", 17 | "@virtualstate/app-history/event-target/async": "./src/event-target/async-event-target.ts", 18 | "@virtualstate/app-history/event-target": "./src/event-target/sync-event-target.ts", 19 | "@virtualstate/app-history-imported": "./src/app-history.ts", 20 | "@virtualstate/app-history-imported/polyfill": "./src/polyfill.ts", 21 | "@virtualstate/app-history-imported/event-target/sync": "./src/event-target/sync-event-target.ts", 22 | "@virtualstate/app-history-imported/event-target/async": "./src/event-target/async-event-target.ts", 23 | "@virtualstate/app-history-imported/event-target": "./src/event-target/sync-event-target.ts", 24 | "@virtualstate/focus": "https://cdn.skypack.dev/@virtualstate/focus", 25 | "dom-lite": "https://cdn.skypack.dev/dom-lite", 26 | "iterable": "https://cdn.skypack.dev/iterable@6.0.1-beta.5", 27 | "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", 28 | "uuid": "./src/util/deno-uuid.ts", 29 | "whatwg-url": "https://cdn.skypack.dev/whatwg-url", 30 | "abort-controller": "https://cdn.skypack.dev/abort-controller", 31 | "deno:std/uuid/mod": "https://deno.land/std@0.113.0/uuid/mod.ts", 32 | "deno:std/uuid/v4": "https://deno.land/std@0.113.0/uuid/v4.ts", 33 | "deno:deno_dom/deno-dom-wasm.ts": "https://deno.land/x/deno_dom/deno-dom-wasm.ts", 34 | "urlpattern-polyfill": "https://cdn.skypack.dev/urlpattern-polyfill", 35 | "./src/tests/config": "./src/tests/config.ts", 36 | "./src/tests/util": "./src/tests/util.ts", 37 | "./src/index": "./src/index.ts", 38 | "./src": "./src/index.ts", 39 | "@virtualstate/combinational": "https://cdn.skypack.dev/@virtualstate/combinational", 40 | "@virtualstate/line": "./esnext/index.js", 41 | "cheerio": "./scripts/nop/index.js" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtualstate/line", 3 | "version": "1.0.1-alpha.5", 4 | "main": "./esnext/index.js", 5 | "module": "./esnext/index.js", 6 | "types": "./esnext/index.d.ts", 7 | "typesVersions": { 8 | "*": { 9 | "*": [ 10 | "./esnext/index.d.ts" 11 | ], 12 | "tests": [ 13 | "./esnext/tests/index.d.ts" 14 | ], 15 | "tests/dom-h": [ 16 | "./esnext/tests/dom-h.d.ts" 17 | ] 18 | } 19 | }, 20 | "type": "module", 21 | "sideEffects": false, 22 | "keywords": [], 23 | "exports": { 24 | ".": "./esnext/index.js", 25 | "./tests": "./esnext/tests/index.js", 26 | "./tests/dom-h": "./esnext/tests/dom-h.js" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/virtualstate/line.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/virtualstate/line/issues" 34 | }, 35 | "homepage": "https://github.com/virtualstate/line#readme", 36 | "author": "Fabian Cook ", 37 | "license": "MIT", 38 | "dependencies": { 39 | "@virtualstate/combinational": "^1.0.1-alpha.2" 40 | }, 41 | "devDependencies": { 42 | "@babel/cli": "^7.15.4", 43 | "@babel/core": "^7.15.4", 44 | "@babel/preset-env": "^7.15.4", 45 | "@opennetwork/http-representation": "^3.0.0", 46 | "@rollup/plugin-node-resolve": "^13.1.1", 47 | "@rollup/plugin-typescript": "^8.3.0", 48 | "@types/chance": "^1.1.3", 49 | "@types/jest": "^27.0.1", 50 | "@types/mkdirp": "^1.0.2", 51 | "@types/node": "^17.0.1", 52 | "@types/rimraf": "^3.0.2", 53 | "@types/uuid": "^8.3.3", 54 | "@types/whatwg-url": "^8.2.1", 55 | "@virtualstate/focus": "^1.0.1-alpha.109", 56 | "@virtualstate/promise": "^1.1.5-alpha.15", 57 | "@virtualstate/union": "^2.48.1", 58 | "c8": "^7.11.3", 59 | "chance": "^1.1.8", 60 | "cheerio": "^1.0.0-rc.10", 61 | "core-js": "^3.17.2", 62 | "dom-lite": "^20.2.0", 63 | "filehound": "^1.17.4", 64 | "jest": "^27.1.0", 65 | "jest-playwright-preset": "^1.7.0", 66 | "mkdirp": "^1.0.4", 67 | "playwright": "^1.17.1", 68 | "rimraf": "^3.0.2", 69 | "rollup": "^2.61.1", 70 | "rollup-plugin-babel": "^4.4.0", 71 | "rollup-plugin-ignore": "^1.0.10", 72 | "ts-jest": "^27.0.5", 73 | "ts-node": "^10.2.1", 74 | "typescript": "^4.4.3", 75 | "urlpattern-polyfill": "^1.0.0-rc2", 76 | "v8-to-istanbul": "^8.1.0" 77 | }, 78 | "scripts": { 79 | "build": "rm -rf esnext && tsc", 80 | "postbuild": "mkdir -p coverage && node scripts/post-build.js", 81 | "generate": "yarn build && node esnext/generate.js", 82 | "prepublishOnly": "npm run build", 83 | "test": "yarn build && node --enable-source-maps esnext/tests/index.js", 84 | "test:deno": "yarn build && deno run --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 85 | "test:deno:r": "yarn build && deno run -r --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 86 | "test:inspect": "yarn build && node --enable-source-maps --inspect-brk esnext/tests/index.js", 87 | "coverage": "yarn build && c8 node esnext/tests/index.js && yarn postbuild" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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 { rollup } from "rollup"; 4 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 5 | import ignore from "rollup-plugin-ignore"; 6 | import babel from "rollup-plugin-babel"; 7 | import { dirname, resolve } from "path"; 8 | 9 | await import("../esnext/trying-to-write-documentation/readme-generator.js"); 10 | 11 | const { pathname } = new URL(import.meta.url); 12 | const cwd = resolve(dirname(pathname), ".."); 13 | 14 | { 15 | // /Volumes/Extreme/Users/fabian/src/virtualstate/esnext/tests/app-history.playwright.wpt.js 16 | // /Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js 17 | 18 | console.log({ 19 | cwd, 20 | path: 21 | `/Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js` === 22 | `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 23 | p: `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 24 | }); 25 | 26 | const bundle = await rollup({ 27 | input: "./esnext/tests/index.js", 28 | plugins: [ 29 | ignore([ 30 | "playwright", 31 | "fs", 32 | "path", 33 | "uuid", 34 | "cheerio", 35 | "@virtualstate/app-history", 36 | "@virtualstate/app-history-imported", 37 | `${cwd}/esnext/tests/app-history.playwright.js`, 38 | `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 39 | `${cwd}/esnext/tests/dependencies-input.js`, 40 | `${cwd}/esnext/tests/dependencies.js`, 41 | "./app-history.playwright.js", 42 | "./app-history.playwright.wpt.js", 43 | ]), 44 | nodeResolve(), 45 | ], 46 | inlineDynamicImports: true, 47 | treeshake: { 48 | preset: "smallest", 49 | moduleSideEffects: "no-external", 50 | }, 51 | }); 52 | await bundle.write({ 53 | sourcemap: true, 54 | output: { 55 | file: "./esnext/tests/rollup.js", 56 | }, 57 | inlineDynamicImports: true, 58 | format: "cjs", 59 | interop: "auto", 60 | globals: { 61 | "esnext/tests/app-history.playwright.js": "globalThis", 62 | }, 63 | }); 64 | } 65 | 66 | if (!process.env.NO_COVERAGE_BADGE_UPDATE) { 67 | const badges = []; 68 | 69 | const { name } = await fs.readFile("package.json", "utf-8").then(JSON.parse); 70 | 71 | badges.push( 72 | "### Support\n\n", 73 | "![Node.js supported](https://img.shields.io/badge/node-%3E%3D16.0.0-blue)", 74 | "![Deno supported](https://img.shields.io/badge/deno-%3E%3D1.17.0-blue)" 75 | // "![Chromium supported](https://img.shields.io/badge/chromium-%3E%3D98.0.4695.0-blue)", 76 | // "![Webkit supported](https://img.shields.io/badge/webkit-%3E%3D15.4-blue)", 77 | // "![Firefox supported](https://img.shields.io/badge/firefox-%3E%3D94.0.1-blue)" 78 | ); 79 | 80 | badges.push( 81 | "\n\n### Test Coverage\n\n" 82 | // `![nycrc config on GitHub](https://img.shields.io/nycrc/${name.replace(/^@/, "")})` 83 | ); 84 | 85 | // const wptResults = await fs 86 | // .readFile("coverage/wpt.results.json", "utf8") 87 | // .then(JSON.parse) 88 | // .catch(() => ({})); 89 | // if (wptResults?.total) { 90 | // const message = `${wptResults.pass}/${wptResults.total}`; 91 | // const name = "Web Platform Tests"; 92 | // badges.push( 93 | // `![${name} ${message}](https://img.shields.io/badge/${encodeURIComponent( 94 | // name 95 | // )}-${encodeURIComponent(message)}-brightgreen)` 96 | // ); 97 | // } 98 | 99 | const coverage = await fs 100 | .readFile("coverage/coverage-summary.json", "utf8") 101 | .then(JSON.parse) 102 | .catch(() => ({})); 103 | const coverageConfig = await fs.readFile(".nycrc", "utf8").then(JSON.parse); 104 | for (const [name, { pct }] of Object.entries(coverage?.total ?? {})) { 105 | const good = coverageConfig[name]; 106 | if (!good) continue; // not configured 107 | const color = pct >= good ? "brightgreen" : "yellow"; 108 | const message = `${pct}%25`; 109 | badges.push( 110 | `![${message} ${name} covered](https://img.shields.io/badge/${name}-${message}-${color})` 111 | ); 112 | } 113 | 114 | const tag = "[//]: # (badges)"; 115 | 116 | const readMe = await fs.readFile("README.md", "utf8"); 117 | const badgeStart = readMe.indexOf(tag); 118 | const badgeStartAfter = badgeStart + tag.length; 119 | if (badgeStart === -1) { 120 | throw new Error(`Expected to find "${tag}" in README.md`); 121 | } 122 | const badgeEnd = badgeStartAfter + readMe.slice(badgeStartAfter).indexOf(tag); 123 | const badgeEndAfter = badgeEnd + tag.length; 124 | const readMeBefore = readMe.slice(0, badgeStart); 125 | const readMeAfter = readMe.slice(badgeEndAfter); 126 | 127 | const readMeNext = `${readMeBefore}${tag}\n\n${badges.join( 128 | " " 129 | )}\n\n${tag}${readMeAfter}`; 130 | await fs.writeFile("README.md", readMeNext); 131 | console.log("Wrote coverage badges!"); 132 | } 133 | -------------------------------------------------------------------------------- /src/line/splt.tsx: -------------------------------------------------------------------------------- 1 | import {children, h, name} from "@virtualstate/focus"; 2 | import { Deferred, deferred } from "../deferred"; 3 | import { ok } from "../like"; 4 | import { TheAsyncThing, anAsyncThing } from "@virtualstate/promise/the-thing"; 5 | 6 | export interface SplitIterable 7 | extends Iterable>, 8 | AsyncIterable { 9 | } 10 | 11 | export type Name = string | symbol; 12 | 13 | export interface Split extends SplitIterable, Record & TheAsyncThing> { 14 | 15 | } 16 | 17 | export interface SplitOptions { 18 | known?: Name[]; 19 | max?: number; 20 | keep?: boolean; 21 | } 22 | 23 | export function split(input: unknown, options: SplitOptions = {}): Split { 24 | let listeners: Deferred[]; 25 | 26 | let namedListeners: Map>; 27 | 28 | let splitPromise: Promise, 29 | spinning = false, 30 | settled = false; 31 | 32 | const { known } = options; 33 | 34 | function init() { 35 | listeners = []; 36 | namedListeners = new Map(); 37 | spinning = false; 38 | splitPromise = undefined; 39 | settled = false; 40 | } 41 | 42 | init(); 43 | 44 | function spin() { 45 | if (spinning) return splitPromise; 46 | spinning = true; 47 | return splitPromise = spinSplit(input); 48 | } 49 | 50 | async function spinSplit(input: unknown) { 51 | let results 52 | try { 53 | results = await children(input); 54 | ok(results, "Expected children to be available"); 55 | if (options?.keep) { 56 | settled = true; 57 | } 58 | if (namedListeners.size) { 59 | const namedResults: Record = 60 | Object.fromEntries(results.map((node) => [name(node), node])); 61 | for (const [name, listener] of namedListeners.entries()) { 62 | listener?.resolve(namedResults[name]); 63 | } 64 | } 65 | for (const [index, listener] of Object.entries(listeners)) { 66 | listener?.resolve(results[index]); 67 | } 68 | } catch (error) { 69 | if (options?.keep) { 70 | settled = true; 71 | } 72 | for (const listener of Object.values(listeners)) { 73 | listener?.reject(error); 74 | } 75 | for (const listener of namedListeners.values()) { 76 | listener?.reject(error); 77 | } 78 | throw error; 79 | } 80 | if (!options?.keep) { 81 | init(); 82 | } 83 | return results; 84 | } 85 | 86 | 87 | function getNamedListener(name: Name) { 88 | const existing = namedListeners.get(name); 89 | if (existing) { 90 | return existing; 91 | } else if (settled) { 92 | return undefined; 93 | } 94 | const listener = deferred(); 95 | void listener.promise.catch(error => void error); 96 | namedListeners.set(name, listener); 97 | return listener; 98 | } 99 | 100 | function getNamedNode(name: Name) { 101 | let listener = getNamedListener(name); 102 | if (!listener) return undefined; 103 | return anAsyncThing({ 104 | async *[Symbol.asyncIterator]() { 105 | listener = listener ?? getNamedListener(name); 106 | if (listener) { 107 | if (!listener.settled) { 108 | await spin(); 109 | } 110 | yield listener.promise; 111 | listener = undefined; 112 | } 113 | }, 114 | }); 115 | } 116 | 117 | function getListener(index: number) { 118 | const existing = listeners[index]; 119 | if (settled) { 120 | ok(existing, "Already settled, cannot add new listeners"); 121 | } 122 | if (existing) { 123 | return existing; 124 | } 125 | const listener = deferred(); 126 | void listener.promise.catch(error => void error); 127 | listeners[index] = listener; 128 | return listener; 129 | } 130 | 131 | function getIndexedNode(index: number) { 132 | let listener = getListener(index); 133 | return anAsyncThing({ 134 | async *[Symbol.asyncIterator]() { 135 | listener = listener ?? getListener(index); 136 | if (listener) { 137 | if (!listener.settled) { 138 | await spin(); 139 | } 140 | yield listener.promise; 141 | listener = undefined; 142 | } 143 | }, 144 | }); 145 | } 146 | 147 | const iterableSplit = { 148 | async *[Symbol.asyncIterator]() { 149 | yield spin(); 150 | }, 151 | [Symbol.iterator]() { 152 | function* withIndex(index: number): Iterable> { 153 | if (options?.max === index) { 154 | return; 155 | } 156 | yield getIndexedNode(index); 157 | yield* withIndex(index + 1); 158 | } 159 | return withIndex(0)[Symbol.iterator](); 160 | }, 161 | }; 162 | 163 | const async = anAsyncThing(iterableSplit); 164 | 165 | const proxy = new Proxy(iterableSplit, { 166 | get(target: unknown, p: Name) { 167 | if (p in iterableSplit && typeof iterableSplit[p] === "function") { 168 | return iterableSplit[p].bind(async); 169 | } 170 | if (p in async && typeof async[p] === "function") { 171 | return async[p].bind(async); 172 | } 173 | if (typeof p === "string" && /^\d+$/.test(p)) { 174 | return getIndexedNode(+p); 175 | } 176 | if (known?.includes(p) !== false) { 177 | return getNamedNode(p); 178 | } 179 | return undefined; 180 | }, 181 | }); 182 | ok(proxy); 183 | return proxy; 184 | } 185 | -------------------------------------------------------------------------------- /src/tests/line/split.tsx: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | import { 4 | h, 5 | createFragment, 6 | children, 7 | toJSON, 8 | descendants, 9 | descendantsSettled, 10 | name, 11 | properties, 12 | } from "@virtualstate/focus"; 13 | import { ok, split } from "@virtualstate/line"; 14 | import {jump} from "../../line/jump"; 15 | import {add} from "cheerio/lib/api/traversing"; 16 | 17 | { 18 | 19 | const fragment = ( 20 | <> 21 | 22 | {true} 23 | {false} 24 | {1} 25 | {2} 26 | 27 | ); 28 | const fragmentSplit = split(fragment); 29 | const [namedNode, a, b, c, d] = fragmentSplit; 30 | const {named} = fragmentSplit; 31 | 32 | console.log(await named); 33 | 34 | console.log(await namedNode, await a, await b, await c, await d); 35 | 36 | console.log(await fragmentSplit); 37 | 38 | } 39 | 40 | { 41 | async function* Component() { 42 | console.group("Running Component, before async"); 43 | 44 | await new Promise(queueMicrotask); 45 | 46 | async function Inner() { 47 | console.log("Running Component inner"); 48 | await new Promise(queueMicrotask); 49 | return "Inner Result"; 50 | } 51 | 52 | console.log("After Component async"); 53 | 54 | yield [ 55 | Promise.resolve(Math.random()), 56 | , 57 | { 58 | async* [Symbol.asyncIterator]() { 59 | console.log("Running asyncIterator"); 60 | yield Math.random(); 61 | }, 62 | }, 63 | , 64 | ]; 65 | 66 | console.groupEnd(); 67 | } 68 | 69 | const node = split(, { 70 | keep: true 71 | }); 72 | 73 | const [randomNumber] = node; 74 | 75 | const [, innerResult, asyncRandomNumber] = node; 76 | 77 | console.log({randomNumber, innerResult, asyncRandomNumber}); 78 | console.log({ 79 | asyncRandomNumber: await children(asyncRandomNumber), 80 | }); 81 | console.log("After loaded"); 82 | console.log({ 83 | randomNumber: await randomNumber, 84 | innerResult: await innerResult, 85 | asyncRandomNumber: await asyncRandomNumber, 86 | all: await children(node), 87 | }); 88 | 89 | const {2: asyncRandom1, named: nodeNamed} = node; 90 | 91 | ok(!nodeNamed, "keep was used, no named node should be available after first resolve"); 92 | 93 | console.log({asyncRandom1, nodeNamed}); 94 | 95 | console.log({ 96 | asyncRandom1: await asyncRandom1, 97 | nodeNamed: await nodeNamed, 98 | all: await children(node), 99 | }); 100 | 101 | const next = split(); 102 | 103 | const {2: asyncRandom2, named: nextNamed} = next; 104 | 105 | // console.log({ asyncRandom2, nextNamed }); 106 | 107 | console.log({ 108 | asyncRandom2: await asyncRandom2, 109 | nextNamed: await nextNamed, 110 | all: await children(next), 111 | }); 112 | 113 | async function* View(options: unknown) { 114 | console.group("Running View, before async"); 115 | await new Promise(queueMicrotask); 116 | 117 | async function Options() { 118 | console.log("Running View inner"); 119 | await new Promise(queueMicrotask); 120 | return ( 121 | 124 | ); 125 | } 126 | 127 | console.log("After View async"); 128 | yield [ 129 | 130 | 131 | 132 | 133 | , 134 | 135 |

Hello!

136 | 137 |
, 138 | ]; 139 | console.groupEnd(); 140 | } 141 | 142 | const {config, web} = split(, { 143 | keep: true 144 | }); 145 | const {key, up, script} = jump(config); 146 | const {script: webScript} = jump(web); 147 | 148 | 149 | console.log({ 150 | key: properties(await key), 151 | up: properties(await up), 152 | script: await jump(script)[0], 153 | webScript: await jump(webScript)[0] 154 | }); 155 | 156 | console.log(await descendantsSettled(web)); 157 | 158 | const max = split(, { 159 | max: 1, 160 | }); 161 | 162 | const [max1, max2] = max; 163 | 164 | console.log({ max1, max2 }); 165 | 166 | ok(max1); 167 | ok(!max2); 168 | } 169 | 170 | { 171 | const { named, unnamed } = split( 172 | <> 173 | 174 | 175 | , 176 | { 177 | known: [ 178 | "named" 179 | ] 180 | } 181 | ); 182 | console.log({ named, unnamed }); 183 | ok(named); 184 | ok(!unnamed); 185 | } 186 | 187 | { 188 | 189 | const almostKey = Math.random(); 190 | 191 | async function Thrower() { 192 | 193 | async function *Throw() { 194 | yield

; 195 | throw "nope"; 196 | } 197 | 198 | return ( 199 | <> 200 | 201 | 202 | 203 | 204 | 205 | ) 206 | } 207 | 208 | const [ { status: s1 }] = await Promise.allSettled([ 209 | (async () => { 210 | 211 | 212 | const { almost, maybe } = split(); 213 | 214 | const inner = jump(maybe) 215 | 216 | ok(properties(await almost).key === almostKey); 217 | 218 | console.log(await inner); 219 | // Should not get here 220 | 221 | })() 222 | ]) 223 | ok(s1 === "rejected", `Got ${s1}`); 224 | 225 | 226 | 227 | const [ { status: s2 }] = await Promise.allSettled([ 228 | (async () => { 229 | 230 | 231 | const { almost, maybe } = split(); 232 | 233 | const [inner] = jump(maybe) 234 | 235 | ok(properties(await almost).key === almostKey); 236 | 237 | console.log(await inner); 238 | // Should not get here 239 | 240 | })() 241 | ]) 242 | ok(s2 === "rejected", `Got ${s2}`); 243 | 244 | 245 | const [ { status: s3 }] = await Promise.allSettled([ 246 | (async () => { 247 | 248 | 249 | const { almost, maybe } = split(); 250 | 251 | const { ha } = jump(maybe) 252 | 253 | ok(properties(await almost).key === almostKey); 254 | 255 | console.log(await ha); 256 | // Should not get here 257 | 258 | })() 259 | ]) 260 | ok(s3 === "rejected", `Got ${s3}`); 261 | 262 | const [ { status: s4 }] = await Promise.allSettled([ 263 | (async () => { 264 | 265 | 266 | const { almost, maybe } = split(, { 267 | keep: true 268 | }); 269 | 270 | const { ha } = jump(maybe, { 271 | keep: true 272 | }); 273 | 274 | ok(properties(await almost).key === almostKey); 275 | 276 | console.log(await ha); 277 | // Should not get here 278 | 279 | })() 280 | ]) 281 | ok(s3 === "rejected", `Got ${s4}`); 282 | } 283 | 284 | { 285 | const node = split(<>Value, { keep: true }); 286 | 287 | function isSame(a: unknown, b: B): a is B { 288 | return a === b; 289 | } 290 | 291 | const [indexed] = node; 292 | ok(indexed); 293 | ok(isSame(await indexed, "Value")) 294 | 295 | const [{status}] = await Promise.allSettled([ 296 | (async () => { 297 | const [, addedIndex] = node; 298 | void addedIndex; // Should not get here 299 | })() 300 | ]) 301 | ok(status === "rejected"); 302 | } 303 | --------------------------------------------------------------------------------