diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 7e1c51f..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,43 +0,0 @@ -const OFF = 0; -const WARN = 1; -const ERROR = 2; - -// TODO: Types -// /** @type {import('eslint/lib/shared/types').ConfigData & { parserOptions: import('@typescript-eslint/types').ParserOptions }} */ -module.exports = { - root: true, - reportUnusedDisableDirectives: true, - parser: '@typescript-eslint/parser', - parserOptions: { - project: ['./test/tsconfig.json'], - tsconfigRootDir: __dirname, - }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'airbnb-base', - 'airbnb-typescript/base', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - 'prettier', - ], - plugins: ['prettier'], - rules: { - '@typescript-eslint/explicit-module-boundary-types': ERROR, - // void return can be used for efficient code (if used safely!) - 'consistent-return': WARN, - 'default-param-last': WARN, - 'import/prefer-default-export': OFF, - // useful for compact and memory efficient code... but be careful! - 'no-cond-assign': OFF, - // can be used for efficient code (if used safely!) - 'no-param-reassign': WARN, - 'no-plusplus': OFF, - 'no-restricted-syntax': OFF, - // useful for compact and memory efficient code... but be careful! - 'no-return-assign': OFF, - // used in synthetic event handler names - 'no-underscore-dangle': OFF, - 'prettier/prettier': WARN, - }, -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c8ad27..43cb9d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,37 +1,62 @@ name: ci on: push: - branches: [master] - pull_request: - branches: [master] + branches: [master, next] + paths-ignore: ['**.md'] + pull_request: {} + workflow_dispatch: {} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.head.label || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: test: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: oven-sh/setup-bun@v2 with: - node-version: 18 - - run: npm install --global pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm run build - - run: pnpm run test + bun-version: latest + - run: bun install --frozen-lockfile + - run: bun run build + - run: bun run test --coverage-reporter=lcov --coverage-reporter=text - name: Report coverage if: ${{ github.repository_owner == 'maxmilton' }} run: | - curl -Lo ./cc-test-reporter https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 - chmod +x ./cc-test-reporter - ./cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.json coverage/lcov.info - ./cc-test-reporter upload-coverage + curl -LO https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 \ + -LO https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64.sha256 + grep test-reporter-latest-linux-amd64 test-reporter-latest-linux-amd64.sha256 | shasum -a 256 -c - + mv test-reporter-latest-linux-amd64 test-reporter + chmod +x ./test-reporter + ./test-reporter format-coverage -t lcov -o coverage/codeclimate.json coverage/lcov.info + ./test-reporter upload-coverage env: CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + e2e: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install --frozen-lockfile + - run: bun playwright install chromium + - run: bun run build + - run: bun run test:e2e --reporter=github,html + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 lint: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: oven-sh/setup-bun@v2 with: - node-version: 18 - - run: npm install --global pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm run lint + bun-version: latest + - run: bun install --frozen-lockfile + - run: bun run lint diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a6070df..4b04985 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,14 +1,18 @@ name: codeql on: push: - branches: [master] - pull_request: - branches: [master] + branches: [master, next] + pull_request: {} + workflow_dispatch: {} schedule: - cron: '28 6 * * 4' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.head.label || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: analyze: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: actions: read contents: read diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f9e79de --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,38 @@ +name: publish +on: + release: + types: [published] +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + id-token: write + steps: + - name: Check valid version tag + run: | + [[ "${{ github.ref }}" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-next.[0-9]+)? ]] || exit 1 + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install --frozen-lockfile + - run: bun run build + - run: bun run lint + - run: bun run test + - uses: actions/setup-node@v4 + with: + node-version: latest + registry-url: 'https://registry.npmjs.org' + - name: Determine npm tag + run: | + if [[ "${{ github.ref }}" == *"-next."* ]]; then + echo "NPM_TAG=next" >> $GITHUB_ENV + else + echo "NPM_TAG=latest" >> $GITHUB_ENV + fi + - run: npm publish --provenance --access public --tag ${{ env.NPM_TAG }} + shell: bash + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..57b9b90 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: release +on: + push: + tags: [v*.*.*] +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install --frozen-lockfile + - run: bun run build + - run: bun run lint + - run: bun run test + - run: gh release create "${{ github.ref_name }}" --draft --generate-notes + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/semgrep-analysis.yml b/.github/workflows/semgrep-analysis.yml index d98df42..fe659b6 100644 --- a/.github/workflows/semgrep-analysis.yml +++ b/.github/workflows/semgrep-analysis.yml @@ -2,24 +2,28 @@ name: semgrep on: push: branches: [master, next] - pull_request: - branches: [master, next] + pull_request: {} + workflow_dispatch: {} schedule: - cron: '28 6 * * 4' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.head.label || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: analyze: runs-on: ubuntu-latest + timeout-minutes: 5 container: - image: returntocorp/semgrep + image: semgrep/semgrep permissions: actions: read contents: read security-events: write steps: - uses: actions/checkout@v4 - - run: semgrep ci --sarif --output=semgrep.sarif || true + - run: semgrep ci --sarif > semgrep.sarif env: - SEMGREP_RULES: p/default + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} - uses: github/codeql-action/upload-sarif@v3 if: always() with: diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml new file mode 100644 index 0000000..eb56f07 --- /dev/null +++ b/.github/workflows/typos.yml @@ -0,0 +1,16 @@ +name: typos +on: + push: + branches: [master, next] + pull_request: {} + workflow_dispatch: {} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.head.label || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +jobs: + typos: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@master diff --git a/.gitignore b/.gitignore index 4aa800e..594336e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ *-lock.json* +*-lock.yaml +*.bak *.lock *.log /coverage/ -dist -node_modules +/dist/ +/node_modules/ +/playwright-report/ +/test-results/ diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 3c4fdd8..0000000 --- a/.prettierrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "arrowParens": "always", - "endOfLine": "lf", - "singleQuote": true, - "trailingComma": "all", - "overrides": [ - { - "files": ["*.test.ts"], - "options": { - "printWidth": 100 - } - } - ] -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 7efca3f..e13eef8 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,8 @@ { "recommendations": [ + "biomejs.biome", "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode" + "oven.bun-vscode", + "tekumara.typos-vscode" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 2b4338f..5e297c3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,41 +1,21 @@ -// https://code.visualstudio.com/docs/editor/debugging#_launch-configurations { "version": "0.2.0", "configurations": [ { - "name": "Attach by Process ID", - "type": "pwa-node", - "processId": "${command:PickProcess}", + "name": "Attach to Bun", + "type": "bun", "request": "attach", - "sourceMaps": true, - "skipFiles": ["/**"], - "smartStep": true + // use `bun --inspect` to open a debug port + "url": "ws://localhost:6499/" }, - // https://github.com/Microsoft/vscode-recipes/tree/master/debugging-jest-tests { - "name": "uvu All", - "type": "pwa-node", + "name": "bun test ", + "type": "bun", "request": "launch", - "sourceMaps": true, - "skipFiles": [ - "/**", - "${workspaceFolder}/node_modules/uvu/**" - ], - "program": "${workspaceFolder}/node_modules/uvu/bin.js", - "args": ["-r", "tsm", "./test", "\\.test\\.ts$"], - "console": "integratedTerminal" - }, - { - "name": "uvu Current File", - "type": "pwa-node", - "request": "launch", - "sourceMaps": true, - "skipFiles": [ - "/**", - "${workspaceFolder}/node_modules/uvu/**" - ], - "args": ["-r", "tsm", "${relativeFile}"], - "console": "integratedTerminal" + "runtimeArgs": ["test"], + "program": "${file}", + "internalConsoleOptions": "openOnSessionStart" } + // TODO: Add a way to run all tests ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index dbeb24b..c7d110d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,26 +1,32 @@ { - "editor.codeActionsOnSave": { - "source.fixAll": true - }, - "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": false, - "eslint.lintTask.options": "--ignore-path .gitignore", - "git.branchProtection": ["master"], + "eslint.format.enable": false, + "eslint.useFlatConfig": true, + "git.branchProtection": ["master", "next"], "js/ts.implicitProjectConfig.checkJs": true, - "prettier.ignorePath": ".gitignore", + "prettier.enable": false, // use biome instead + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.format.enable": false, "typescript.tsdk": "node_modules/typescript/lib", - "[javascript]": { + + "[css][html][json][jsonc][markdown][scss][xml]": { "editor.codeActionsOnSave": { - "source.sortImports": true, - "source.fixAll": true - } + "source.fixAll": "explicit", + "quickfix.biome": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome" }, - "[typescript]": { + "[javascript][typescript]": { "editor.codeActionsOnSave": { - "source.sortImports": true, - "source.fixAll": true - } + "source.sortImports": "never", + "source.organizeImports": "never", + "source.organizeImports.biome": "explicit", + "source.fixAll": "never", + "source.fixAll.eslint": "never", + "quickfix.biome": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome" } } diff --git a/LICENSE b/LICENSE index c66ae18..c81c473 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Max Milton +Copyright (c) 2024 Max Milton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3b5b955..ab80deb 100644 --- a/README.md +++ b/README.md @@ -15,36 +15,42 @@ Originally a fork of the excellent project. ## TODO - Add documentation about: - - Internal use of `innerHTML` — potencial risk of XSS etc.; never use `h` and `html` functions with untrusted input + - Internal use of `innerHTML` — potential risk of XSS etc.; never use `h` and `html` functions with untrusted input - In future we may have `Sanitizer.sanitizeFor(...)` which could be used to sanitize untrusted input - - It's unlikely we'll use it internally due to performance overhead but developers should definately sanitize input when untrusted before passing it in... although we could create light wrapper functions + - It's unlikely we'll use it internally due to performance overhead but developers should definitely sanitize input when untrusted before passing it in... although we could create light wrapper functions - - - New DOM utility functions - - `createFragment` + - `fragment` + - `text` - `create` + - `clone` - `append` - `prepend` - - `onNodeRemove` + - `insert` + - `onRemove` - New reactive store feature - Differences from the original `stage0` project: - - `h` is now `function h(template: string): S1Node` e.g., `h('

#text

')` - - `html` is available to use as a string template literal tag function e.g., `` html`

#text

` `` - - Import paths - - Other than reconcilers, everything is a named export from `stage1` - - Reconcilers all export a `reconcile` function - - `/keyed` --> `/reconcile/keyed` - - `/reconcile` --> `/reconcile/non-keyed` - - `/reuse-nodes` --> `/reconcile/reuse-nodes` + - There are now 2 runtime modes: + - New precompiled runtime mode for ultimate performance. Compiles templates at build-time via a bun macro that minifies templates, generates metadata, and then includes minimal runtime code in your JS bundle. Currently only works with [Bun.build](https://bun.sh/docs/bundler). + - The regular mode is still available which generates metadata when your JS is run in the browser. Regular mode can be used with or without a build process. + - Ref nodes are now marked with `@` rather than `#` + - `h` is now `function h(template: string): S1Node` e.g., `h('

@key

')` + - `html` is available to use as a string template literal tag function e.g., `` html`

@key

` `` (regular mode only) + - `view.collect` is now a `collect` function that needs to be imported separately - Extra DOM utils - New reactive `store` factory can be imported from `stage1/store` - Improved TypeScript support - Reduced size and improved load and runtime performance - - `process.env.NODE_ENV` must be defined - - If `process.env.NODE_ENV === 'production` you must minify `h`/`html` strings with a compatible minifier - - Add full example with `esbuild` + `esbuild-minify-templates` + - Import paths: + - Other than reconcilers and the store, everything is a named export from `stage1` + - Reconcilers all export a `reconcile` function + - `/keyed` --> `/reconcile/keyed` + - `/reconcile` --> `/reconcile/non-keyed` + - `/reuse-nodes` --> `/reconcile/reuse-nodes` - Ref names must be lowercase because some browsers normalise element attribute names when rendering HTML - Add API and usage documentation + - The regular mode `h()` function does not support skipping minification in whitespace sensitive HTML blocks like `

` and `` because it would be too slow. The precompiled mode `compile()` macro does however. Same for the other compile options.
 - Add more tests
 - Add examples
 - Set up benchmarking + compare to `stage0` and other JS frameworks
@@ -56,6 +62,8 @@ Originally a fork of the excellent  project.
 
 Minimum browser version required:
 
+
+
 - Chrome 26
 - Edge 13
 - Firefox 22
@@ -64,13 +72,18 @@ Minimum browser version required:
 
 Some optional features require a higher browser version:
 
-- Compiler `html` tagged template literal function uses `String.raw`; [requirements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw#browser_compatibility)
-- `createFragment` utility function uses `DocumentFragment`; [requirements](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment/DocumentFragment#browser_compatibility)
-- `onNodeRemove` utility function uses `for...of`; [requirements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of#browser_compatibility)
+- `html` tagged template literal function uses `String.raw`; [requirements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw#browser_compatibility)
+- `onRemove` utility function uses `for...of` and `MutationObserver`; [requirements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of#browser_compatibility), [requirements](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/)
 - `store` uses `Proxy`; [requirements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy#browser_compatibility)
   - Also uses [logical nullish assignment](<(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_nullish_assignment#browser_compatibility)>) and [optional chaining operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#browser_compatibility), however, build tools can transform these for old browser targets
 
-SSR via Node.js or Deno is not supported and is not the intended use of this library.
+SSR (server-side rendering) is not supported and is not the intended use of this library.
+
+## Build environment JS runtime support
+
+The default runtime requires [bun](https://bun.sh/) version 1.0.20 or above.
+
+The browser bundle does not have any specific build requirements.
 
 ## Bugs
 
@@ -86,4 +99,4 @@ MIT license. See [LICENSE](https://github.com/maxmilton/stage1/blob/master/LICEN
 
 ---
 
-© 2023 [Max Milton](https://maxmilton.com)
+© 2024 [Max Milton](https://maxmilton.com)
diff --git a/bench/build.ts b/bench/build.ts
new file mode 100644
index 0000000..b3d4ae2
--- /dev/null
+++ b/bench/build.ts
@@ -0,0 +1,14 @@
+/* eslint-disable no-console */
+export {};
+
+// TODO: Easy way to build a single benchmark test case for running in the browser.
+
+console.time('build');
+const out = await Bun.build({
+  entrypoints: ['src/temp.ts'],
+  outdir: 'dist',
+  target: 'browser',
+  minify: true,
+});
+console.timeEnd('build');
+console.log(out);
diff --git a/bench/cases/index.ts b/bench/cases/index.ts
new file mode 100644
index 0000000..4d9832a
--- /dev/null
+++ b/bench/cases/index.ts
@@ -0,0 +1 @@
+import './replace-vs-replaceAll';
diff --git a/bench/cases/replace-vs-replaceAll.ts b/bench/cases/replace-vs-replaceAll.ts
new file mode 100644
index 0000000..af441a8
--- /dev/null
+++ b/bench/cases/replace-vs-replaceAll.ts
@@ -0,0 +1,258 @@
+import { baseline, bench, group, run } from 'mitata';
+
+const html = `
+  
+

hello

+

world

+ + +
+

hello

+

world

+ + + + + + + + +
+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

hello

+

world

+
+
+ +
+

hello

+

world

+ +
+

hello

+

world

+ +
+ + + + + + + + +

hello

+

world

+
+
+
+
+

hello

+

world

+ + + + + + + +
+

hello

+

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

world

+

world

+

world

+

world

😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼 +

world

+

world

+

world

+

world

+

world

+

world

+

world

+ 😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼

world

+

world

+

world

+

world

+

world

+

world

+YYYYYYYYYYYYYYYYY

world

+

world

+

world

+

world

+

world

+

world

+

world

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +

world

+

world

+

world

+ 😼

world

+ +
+😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼😼

hello

+

world

+
+
+ +
+

hello

+

world

+ +
+

hello

+

world

+ +
+

hello

+

world

+
+
+
+
+
+`; + +const base = () => ''; + +function one(template: string) { + return ( + template + // reduce any whitespace to a single space + .replace(/\s+/g, ' ') + // remove space adjacent to tags + .replace(/> /g, '>') + .replace(/ /g, '>') + .replaceAll(/ ', '>') + .replaceAll(' <', '<') + ); +} +function twoS2(template: string) { + return ( + template + // reduce any whitespace to a single space + .replace(/\s+/g, ' ') + // remove space adjacent to tags + .replaceAll('> ', '>') + .replaceAll(' <', '<') + ); +} + +function oneB() { + return ( + html + // reduce any whitespace to a single space + .replace(/\s+/g, ' ') + // remove space adjacent to tags + .replace(/> /g, '>') + .replace(/ /g, '>') + .replaceAll(/ + template + // reduce any whitespace to a single space + .replace(/\s+/g, ' ') + // remove space adjacent to tags + .replace(/> /g, '>') + .replace(/ + template + // reduce any whitespace to a single space + .replaceAll(/\s+/g, ' ') + // remove space adjacent to tags + .replaceAll(/> /g, '>') + .replaceAll(/ + html + // reduce any whitespace to a single space + .replace(/\s+/g, ' ') + // remove space adjacent to tags + .replace(/> /g, '>') + .replace(/ + html + // reduce any whitespace to a single space + .replaceAll(/\s+/g, ' ') + // remove space adjacent to tags + .replaceAll(/> /g, '>') + .replaceAll(/ { + baseline('baseline', base); + bench('one:replace', () => one(html)); + bench('two:replaceAll', () => two(html)); + bench('twoS1:replaceAll,string', () => twoS1(html)); + bench('twoS2:combo,string', () => twoS2(html)); + bench('oneB:replace', () => oneB()); + bench('twoB:replaceAll', () => twoB()); + bench('oneC:replace,arrow', () => oneC(html)); + bench('twoC:replaceAll,arrow', () => twoC(html)); +}); + +group('input from global var', () => { + baseline('baseline', base); + bench('oneB2:replace', oneB); + bench('twoB2:replaceAll', twoB); + bench('oneD:replace,arrow', oneD); + bench('twoD:replaceAll,arrow', twoD); +}); + +// console.log('#### SAME', one(html) === two(html)); +// console.log('#########', one(html)); + +const out = await run(); +// await run({ collect: true }); +console.log(out); diff --git a/bench/package.json b/bench/package.json new file mode 100644 index 0000000..10453b3 --- /dev/null +++ b/bench/package.json @@ -0,0 +1,15 @@ +{ + "name": "bench", + "version": "0.0.0", + "private": "true", + "scripts": { + "build": "bun build.ts", + "lint": "bun run lint:js && bun run lint:ts", + "lint:js": "eslint --ext .ts .", + "lint:ts": "tsc --noEmit", + "start": "bun cases/index.ts" + }, + "dependencies": { + "mitata": "0.1.6" + } +} diff --git a/bench/tsconfig.json b/bench/tsconfig.json new file mode 100644 index 0000000..1f5bd2c --- /dev/null +++ b/bench/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["bun-types"] + }, + "include": ["cases", "build.ts"] +} diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..32fb660 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,176 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, + "organizeImports": { "enabled": true }, + "formatter": { + "formatWithErrors": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80 + }, + "linter": { + "rules": { + "recommended": true, + "complexity": { + "noForEach": "off", + "noUselessStringConcat": "warn", + "noUselessUndefinedInitialization": "warn", + "useSimplifiedLogicExpression": "info", + "useDateNow": "error" + }, + "correctness": { + "noUndeclaredDependencies": "warn", + "noUndeclaredVariables": "warn", + "noUnusedFunctionParameters": "warn", + "noUnusedImports": { "level": "error", "fix": "none" } + }, + "performance": { + "noBarrelFile": "error", + "noReExportAll": "error" + }, + "style": { + "noDoneCallback": "warn", + "noNamespace": "error", + "noNamespaceImport": "warn", + "noNegationElse": "error", + "noNonNullAssertion": "off", + "noParameterProperties": "error", + "noRestrictedGlobals": "error", + "noShoutyConstants": "error", + "noYodaExpression": "warn", + "useCollapsedElseIf": "error", + "useConsistentBuiltinInstantiation": "error", + "useDefaultSwitchClause": "warn", + "useEnumInitializers": "off", + "useExplicitLengthCheck": "warn", + "useNamingConvention": { + "level": "error", + "options": { "enumMemberCase": "CONSTANT_CASE", "strictCase": false } + }, + "useShorthandArrayType": "error", + "useShorthandAssign": "error", + "useSingleCaseStatement": "info", + "useTemplate": "off", + "useThrowNewError": "error", + "useThrowOnlyError": "warn" + }, + "suspicious": { + "noApproximativeNumericConstant": "error", + "noAssignInExpressions": "off", + "noConfusingVoidType": "off", + "noConsoleLog": "warn", + "noConstEnum": "off", + "noDuplicateAtImportRules": "error", + "noDuplicateFontNames": "error", + "noDuplicateSelectorsKeyframeBlock": "error", + "noEmptyBlock": "warn", + "noEvolvingTypes": "warn", + "noExplicitAny": "off", + "noFocusedTests": "error", + "noMisplacedAssertion": "error", + "noMisrefactoredShorthandAssign": "error", + "useErrorMessage": "warn" + }, + "nursery": { + "noCommonJs": "error", + "noDuplicateElseIf": "error", + "noDynamicNamespaceImportAccess": "error", + "noEnum": "error", + "noExportedImports": "error", + // "noSecrets": "info", // TODO: Change to "error" when more stable + "noSubstr": "warn", + "noUnknownPseudoClass": "warn", + "noUnknownPseudoElement": "warn", + "useAdjacentOverloadSignatures": "error", + "useValidAutocomplete": "warn" + } + } + }, + "javascript": { + "globals": ["HTMLRewriter"], + "formatter": { + "semicolons": "always", + "trailingCommas": "all", + "quoteStyle": "single" + } + }, + "overrides": [ + { + "include": [".vscode/*.json", "tsconfig*.json"], + "json": { + "parser": { + "allowComments": true, + "allowTrailingCommas": true + } + } + }, + { + "include": ["**/*.spec.ts", "**/*.test.ts", "test/**"], + "formatter": { + "lineWidth": 100 + }, + "linter": { + "rules": { + "correctness": { + "noUndeclaredDependencies": "off" + }, + "style": { + "noNamespaceImport": "off" + }, + "nursery": { + "noDynamicNamespaceImportAccess": "off" + } + } + }, + "javascript": { + "globals": ["$console", "Bun", "happyDOM"] + } + }, + { + "include": ["*.config.mjs", "*.config.ts", "*.d.ts", "build.ts"], + "linter": { + "rules": { + "correctness": { + "noUndeclaredDependencies": "off" + }, + "style": { + "noNamespaceImport": "off", + "useNamingConvention": "off" + }, + "suspicious": { + "noConsoleLog": "off" + } + } + }, + "javascript": { + "globals": ["Bun"] + } + }, + { + "include": ["src/reconcile/*.ts"], + "linter": { + "rules": { + "style": { + "noParameterAssign": "off" + } + } + } + }, + { + "include": ["src/browser/index.ts", "src/index.ts"], + "linter": { + "rules": { + "performance": { + "noBarrelFile": "off", + "noReExportAll": "off" + } + } + } + }, + { + "include": ["bench/**"], + "linter": { "enabled": false } + } + ] +} diff --git a/build.mjs b/build.mjs deleted file mode 100644 index 5d13ae6..0000000 --- a/build.mjs +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies, no-console */ - -import esbuild from 'esbuild'; - -// We don't use NODE_ENV here to avoid confusion with process.env.NODE_ENV -// which we want kept in the build output -const dev = !!process.env.DEV_BUILD; - -/** @type {esbuild.BuildOptions} */ -const esbuildConfig = { - entryPoints: [ - 'src/index.ts', - 'src/store.ts', - 'src/reconcile/keyed.ts', - 'src/reconcile/non-keyed.ts', - 'src/reconcile/reuse-nodes.ts', - ], - outdir: 'dist', - platform: 'neutral', - bundle: true, - sourcemap: true, - metafile: !dev && process.stdout.isTTY, - logLevel: 'debug', -}; - -if (dev) { - const context = await esbuild.context(esbuildConfig); - await context.watch(); -} else { - const out = await esbuild.build(esbuildConfig); - - if (out.metafile) { - console.log(await esbuild.analyzeMetafile(out.metafile)); - } -} diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..3223d16 --- /dev/null +++ b/build.ts @@ -0,0 +1,102 @@ +import { createBundle } from 'dts-buddy'; +import { rollup } from 'rollup'; +import { minify } from 'terser'; + +console.time('prebuild'); +await Bun.$`rm -rf dist`; +console.timeEnd('prebuild'); + +console.time('build1'); + +const out = await Bun.build({ + entrypoints: ['src/browser/index.ts'], + outdir: 'dist', + naming: '[dir]/browser.js', + target: 'browser', + minify: true, + sourcemap: 'inline', +}); +// TODO: Once bun supports compact iife output, remove this rollup build step. +const bundle = await rollup({ + input: out.outputs[0].path, +}); +await bundle.write({ + file: out.outputs[0].path, + format: 'iife', + name: 'stage1', + sourcemap: true, + plugins: [ + // @ts-expect-error - TODO: Fix return types + { + name: 'terser', + renderChunk(src) { + return minify(src, { + ecma: 2015, + sourceMap: true, + compress: { + reduce_funcs: false, // prevent functions being inlined + passes: 2, + }, + }); + }, + }, + ], +}); + +console.timeEnd('build1'); +console.time('build2'); + +await Bun.build({ + entrypoints: ['src/browser/index.ts'], + outdir: 'dist', + target: 'browser', + naming: '[dir]/browser.mjs', + minify: true, + sourcemap: 'linked', +}); + +await Bun.build({ + entrypoints: ['src/index.ts'], + outdir: 'dist', + target: 'browser', + minify: true, + sourcemap: 'linked', +}); + +await Bun.build({ + entrypoints: ['src/macro.ts'], + outdir: 'dist', + target: 'bun', + minify: true, + sourcemap: 'linked', +}); + +await Bun.build({ + entrypoints: [ + 'src/reconcile/keyed.ts', + 'src/reconcile/non-keyed.ts', + 'src/reconcile/reuse-nodes.ts', + ], + outdir: 'dist/reconcile', + target: 'browser', + minify: true, + sourcemap: 'linked', +}); + +console.timeEnd('build2'); +console.time('dts'); + +await createBundle({ + output: 'dist/index.d.ts', + modules: { + stage1: 'src/index.ts', + 'stage1/browser': 'src/browser/index.ts', + 'stage1/macro': 'src/macro.ts', + 'stage1/reconcile/keyed': 'src/reconcile/keyed.ts', + 'stage1/reconcile/non-keyed': 'src/reconcile/non-keyed.ts', + 'stage1/reconcile/reuse-nodes': 'src/reconcile/reuse-nodes.ts', + }, + include: ['src'], +}); + +console.timeEnd('dts'); diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..58ec229 Binary files /dev/null and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..1f72da7 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,7 @@ +[run] +bun = true + +[test] +preload = ["./test/setup.ts"] +root = "./test/unit" +coverage = true diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..de0dc4f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,95 @@ +import eslint from '@eslint/js'; +import mm from '@maxmilton/eslint-config'; +import unicorn from 'eslint-plugin-unicorn'; +import ts from 'typescript-eslint'; + +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +export default ts.config( + eslint.configs.recommended, + ...ts.configs.strictTypeChecked, + ...ts.configs.stylisticTypeChecked, + unicorn.configs['flat/recommended'], + mm.configs.recommended, + { + linterOptions: { + reportUnusedDisableDirectives: ERROR, + }, + languageOptions: { + parserOptions: { + project: ['tsconfig.json', 'tsconfig.node.json'], + projectService: { + allowDefaultProject: ['*.mjs'], + }, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + // TODO: Remove once String.raw doesn't crash bun macros + 'unicorn/prefer-string-raw': OFF, + + // somewhat unstable and not always useful + '@typescript-eslint/no-unnecessary-type-parameters': WARN, + + // prefer to clearly separate Bun and DOM + 'unicorn/prefer-global-this': OFF, + + /* Performance and byte savings */ + // byte savings + '@typescript-eslint/no-confusing-void-expression': WARN, + // byte savings (but reduces debugging ability) + '@typescript-eslint/restrict-plus-operands': WARN, + // byte savings (but reduces debugging ability) + '@typescript-eslint/restrict-template-expressions': WARN, + // byte savings with same performance + 'prefer-template': WARN, + // indexOf is faster + '@typescript-eslint/prefer-includes': OFF, + // worse for performance-critical code + '@typescript-eslint/prefer-for-of': OFF, + // alternatives offer byte savings and better performance + '@typescript-eslint/prefer-string-starts-ends-with': OFF, + // void return can be used for efficient code... but be careful! + 'consistent-return': WARN, + // useful for compact and memory efficient code... but be careful! + 'no-cond-assign': OFF, + // more compact at the cost of being harder to read... but be careful! + 'no-multi-assign': WARN, + // can be used for efficient code... but be careful! + 'no-param-reassign': WARN, + // byte savings + 'no-plusplus': OFF, + // useful for compact and memory efficient code... but be careful! + 'no-return-assign': WARN, + // byte savings + faster + 'unicorn/explicit-length-check': OFF, + 'unicorn/no-array-callback-reference': WARN, + // forEach is often faster (in Chrome and Bun but not Firefox) + 'unicorn/no-array-for-each': OFF, + // indexOf is faster (in Chrome) + 'unicorn/prefer-includes': OFF, + // slower and worse browser support + 'unicorn/prefer-string-replace-all': OFF, + // byte savings (minification doesn't currently automatically remove) + 'unicorn/switch-case-braces': [ERROR, 'avoid'], + + /* stage1 */ + // underscores in synthetic event handler names + 'no-underscore-dangle': OFF, + 'unicorn/prefer-add-event-listener': OFF, + 'unicorn/prefer-dom-node-append': OFF, + 'unicorn/prefer-query-selector': OFF, + }, + }, + { + files: ['build.ts'], + rules: { + 'no-console': OFF, + }, + }, + { + ignores: ['**/*.bak', 'bench/**', 'coverage/**', 'dist/**'], + }, +); diff --git a/package.json b/package.json index 3eb3bbb..f938cea 100644 --- a/package.json +++ b/package.json @@ -1,70 +1,73 @@ { "name": "stage1", - "version": "0.7.2", - "type": "module", + "version": "0.8.0-next.15", "description": "High-performance JavaScript micro framework", "repository": "maxmilton/stage1", - "homepage": "https://github.com/maxmilton/stage1", "author": "Max Milton ", "license": "MIT", + "type": "module", + "sideEffects": false, "main": "dist/index.js", + "browser": "dist/browser.js", + "types": "dist/index.d.ts", "exports": { - ".": "./dist/index.js", - "./reconcile/keyed": "./dist/reconcile/keyed.js", - "./reconcile/non-keyed": "./dist/reconcile/non-keyed.js", - "./reconcile/reuse-nodes": "./dist/reconcile/reuse-nodes.js", - "./store": "./dist/store.js", + ".": { + "types": "./dist/index.d.ts", + "bun": "./src/index.ts", + "default": "./dist/index.js" + }, + "./macro": { + "types": "./dist/index.d.ts", + "default": "./dist/macro.js" + }, + "./browser": { + "types": "./dist/index.d.ts", + "bun": "./src/browser/index.ts", + "import": "./dist/browser.mjs", + "require": "./dist/browser.js" + }, + "./reconcile/keyed": { + "types": "./dist/index.d.ts", + "bun": "./src/reconcile/keyed.ts", + "default": "./dist/reconcile/keyed.js" + }, + "./reconcile/non-keyed": { + "types": "./dist/index.d.ts", + "bun": "./src/reconcile/non-keyed.ts", + "default": "./dist/reconcile/non-keyed.js" + }, + "./reconcile/reuse-nodes": { + "types": "./dist/index.d.ts", + "bun": "./src/reconcile/reuse-nodes.ts", + "default": "./dist/reconcile/reuse-nodes.js" + }, + "./dist/*": "./dist/*", "./package.json": "./package.json" }, - "types": "dist/index.d.ts", - "typesVersions": { - "*": { - "reconcile/keyed": [ - "dist/reconcile/keyed.d.ts" - ], - "reconcile/non-keyed": [ - "dist/reconcile/non-keyed.d.ts" - ], - "reconcile/reuse-nodes": [ - "dist/reconcile/reuse-nodes.d.ts" - ], - "store": [ - "dist/store.d.ts" - ] - } - }, - "files": [ - "dist" - ], + "files": ["dist", "src"], "scripts": { - "build": "pnpm run prebuild && node build.mjs && pnpm run postbuild", - "dev": "DEV_BUILD=1 node build.mjs", - "lint": "pnpm run lint:js && pnpm run lint:ts", - "lint:js": "eslint --ignore-path .gitignore --ext .ts,.js,.mjs,.cjs .", - "lint:ts": "tsc --project test --noEmit", - "postbuild": "tsc --emitDeclarationOnly", - "prebuild": "rm -rf dist; mkdir dist", - "test": "c8 --all --include=src --reporter=text --reporter=lcovonly uvu -r tsm -r \"$PWD\"/test/setup.ts test '\\.test\\.ts$'" + "build": "bun build.ts", + "lint": "bun lint:fmt && bun lint:js && bun lint:ts", + "lint:fmt": "biome check", + "lint:js": "eslint", + "lint:ts": "tsc --noEmit", + "test": "bun test", + "test:e2e": "playwright test" }, "devDependencies": { - "@types/jsdom": "21.1.1", - "@types/node": "20.4.1", - "@typescript-eslint/eslint-plugin": "5.61.0", - "@typescript-eslint/parser": "5.61.0", - "c8": "8.0.0", - "esbuild": "0.18.11", - "eslint": "8.44.0", - "eslint-config-airbnb-base": "15.0.0", - "eslint-config-airbnb-typescript": "17.0.0", - "eslint-config-prettier": "8.8.0", - "eslint-plugin-import": "2.27.5", - "eslint-plugin-prettier": "5.0.0-alpha.2", - "jsdom": "22.1.0", - "nanospy": "1.0.0", - "prettier": "3.0.0", - "tsm": "2.3.0", - "typescript": "5.1.6", - "uvu": "0.5.6" - }, - "sideEffects": false + "@biomejs/biome": "1.9.4", + "@eslint/js": "9.16.0", + "@maxmilton/eslint-config": "0.0.7", + "@maxmilton/test-utils": "0.0.6", + "@playwright/test": "1.49.0", + "@types/bun": "1.1.14", + "dts-buddy": "0.5.4", + "eslint": "9.16.0", + "eslint-plugin-unicorn": "56.0.1", + "happy-dom": "15.11.7", + "rollup": "4.28.0", + "terser": "5.36.0", + "typescript": "5.7.2", + "typescript-eslint": "8.16.0" + } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..c880d64 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: 'test/e2e', + testMatch: 'test/e2e/**/*.spec.ts', + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + use: { + acceptDownloads: false, + contextOptions: { strictSelectors: true }, + locale: 'en-US', + offline: true, // no networking necessary + timezoneId: 'UTC', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index d89a2f3..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,2891 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -devDependencies: - '@types/jsdom': - specifier: 21.1.1 - version: 21.1.1 - '@types/node': - specifier: 20.4.1 - version: 20.4.1 - '@typescript-eslint/eslint-plugin': - specifier: 5.61.0 - version: 5.61.0(@typescript-eslint/parser@5.61.0)(eslint@8.44.0)(typescript@5.1.6) - '@typescript-eslint/parser': - specifier: 5.61.0 - version: 5.61.0(eslint@8.44.0)(typescript@5.1.6) - c8: - specifier: 8.0.0 - version: 8.0.0 - esbuild: - specifier: 0.18.11 - version: 0.18.11 - eslint: - specifier: 8.44.0 - version: 8.44.0 - eslint-config-airbnb-base: - specifier: 15.0.0 - version: 15.0.0(eslint-plugin-import@2.27.5)(eslint@8.44.0) - eslint-config-airbnb-typescript: - specifier: 17.0.0 - version: 17.0.0(@typescript-eslint/eslint-plugin@5.61.0)(@typescript-eslint/parser@5.61.0)(eslint-plugin-import@2.27.5)(eslint@8.44.0) - eslint-config-prettier: - specifier: 8.8.0 - version: 8.8.0(eslint@8.44.0) - eslint-plugin-import: - specifier: 2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.61.0)(eslint@8.44.0) - eslint-plugin-prettier: - specifier: 5.0.0-alpha.2 - version: 5.0.0-alpha.2(eslint-config-prettier@8.8.0)(eslint@8.44.0)(prettier@3.0.0) - jsdom: - specifier: 22.1.0 - version: 22.1.0 - nanospy: - specifier: 1.0.0 - version: 1.0.0 - prettier: - specifier: 3.0.0 - version: 3.0.0 - tsm: - specifier: 2.3.0 - version: 2.3.0 - typescript: - specifier: 5.1.6 - version: 5.1.6 - uvu: - specifier: 0.5.6 - version: 0.5.6 - -packages: - - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: true - - /@bcoe/v8-coverage@0.2.3: - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - dev: true - - /@esbuild/android-arm64@0.18.11: - resolution: {integrity: sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.15.18: - resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.18.11: - resolution: {integrity: sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.18.11: - resolution: {integrity: sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.18.11: - resolution: {integrity: sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.18.11: - resolution: {integrity: sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.18.11: - resolution: {integrity: sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.18.11: - resolution: {integrity: sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.18.11: - resolution: {integrity: sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.18.11: - resolution: {integrity: sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.18.11: - resolution: {integrity: sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.15.18: - resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.18.11: - resolution: {integrity: sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.18.11: - resolution: {integrity: sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.18.11: - resolution: {integrity: sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.18.11: - resolution: {integrity: sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.18.11: - resolution: {integrity: sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.18.11: - resolution: {integrity: sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.18.11: - resolution: {integrity: sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.18.11: - resolution: {integrity: sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.18.11: - resolution: {integrity: sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.18.11: - resolution: {integrity: sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.18.11: - resolution: {integrity: sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.18.11: - resolution: {integrity: sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@eslint-community/eslint-utils@4.4.0(eslint@8.44.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 8.44.0 - eslint-visitor-keys: 3.4.1 - dev: true - - /@eslint-community/regexpp@4.5.1: - resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true - - /@eslint/eslintrc@2.1.0: - resolution: {integrity: sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.6.0 - globals: 13.20.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/js@8.44.0: - resolution: {integrity: sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /@humanwhocodes/config-array@0.11.10: - resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true - - /@humanwhocodes/object-schema@1.2.1: - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - dev: true - - /@istanbuljs/schema@0.1.3: - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - dev: true - - /@jridgewell/resolve-uri@3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/sourcemap-codec@1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - dev: true - - /@jridgewell/trace-mapping@0.3.18: - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: true - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.15.0 - dev: true - - /@pkgr/utils@2.4.2: - resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - dependencies: - cross-spawn: 7.0.3 - fast-glob: 3.3.0 - is-glob: 4.0.3 - open: 9.1.0 - picocolors: 1.0.0 - tslib: 2.6.0 - dev: true - - /@tootallnate/once@2.0.0: - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - dev: true - - /@types/istanbul-lib-coverage@2.0.4: - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} - dev: true - - /@types/jsdom@21.1.1: - resolution: {integrity: sha512-cZFuoVLtzKP3gmq9eNosUL1R50U+USkbLtUQ1bYVgl/lKp0FZM7Cq4aIHAL8oIvQ17uSHi7jXPtfDOdjPwBE7A==} - dependencies: - '@types/node': 20.4.1 - '@types/tough-cookie': 4.0.2 - parse5: 7.1.2 - dev: true - - /@types/json-schema@7.0.12: - resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} - dev: true - - /@types/json5@0.0.29: - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - dev: true - - /@types/node@20.4.1: - resolution: {integrity: sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==} - dev: true - - /@types/semver@7.5.0: - resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} - dev: true - - /@types/tough-cookie@4.0.2: - resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} - dev: true - - /@typescript-eslint/eslint-plugin@5.61.0(@typescript-eslint/parser@5.61.0)(eslint@8.44.0)(typescript@5.1.6): - resolution: {integrity: sha512-A5l/eUAug103qtkwccSCxn8ZRwT+7RXWkFECdA4Cvl1dOlDUgTpAOfSEElZn2uSUxhdDpnCdetrf0jvU4qrL+g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.61.0(eslint@8.44.0)(typescript@5.1.6) - '@typescript-eslint/scope-manager': 5.61.0 - '@typescript-eslint/type-utils': 5.61.0(eslint@8.44.0)(typescript@5.1.6) - '@typescript-eslint/utils': 5.61.0(eslint@8.44.0)(typescript@5.1.6) - debug: 4.3.4 - eslint: 8.44.0 - graphemer: 1.4.0 - ignore: 5.2.4 - natural-compare-lite: 1.4.0 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.1.6) - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser@5.61.0(eslint@8.44.0)(typescript@5.1.6): - resolution: {integrity: sha512-yGr4Sgyh8uO6fSi9hw3jAFXNBHbCtKKFMdX2IkT3ZqpKmtAq3lHS4ixB/COFuAIJpwl9/AqF7j72ZDWYKmIfvg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.61.0 - '@typescript-eslint/types': 5.61.0 - '@typescript-eslint/typescript-estree': 5.61.0(typescript@5.1.6) - debug: 4.3.4 - eslint: 8.44.0 - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/scope-manager@5.61.0: - resolution: {integrity: sha512-W8VoMjoSg7f7nqAROEmTt6LoBpn81AegP7uKhhW5KzYlehs8VV0ZW0fIDVbcZRcaP3aPSW+JZFua+ysQN+m/Nw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.61.0 - '@typescript-eslint/visitor-keys': 5.61.0 - dev: true - - /@typescript-eslint/type-utils@5.61.0(eslint@8.44.0)(typescript@5.1.6): - resolution: {integrity: sha512-kk8u//r+oVK2Aj3ph/26XdH0pbAkC2RiSjUYhKD+PExemG4XSjpGFeyZ/QM8lBOa7O8aGOU+/yEbMJgQv/DnCg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.61.0(typescript@5.1.6) - '@typescript-eslint/utils': 5.61.0(eslint@8.44.0)(typescript@5.1.6) - debug: 4.3.4 - eslint: 8.44.0 - tsutils: 3.21.0(typescript@5.1.6) - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/types@5.61.0: - resolution: {integrity: sha512-ldyueo58KjngXpzloHUog/h9REmHl59G1b3a5Sng1GfBo14BkS3ZbMEb3693gnP1k//97lh7bKsp6/V/0v1veQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /@typescript-eslint/typescript-estree@5.61.0(typescript@5.1.6): - resolution: {integrity: sha512-Fud90PxONnnLZ36oR5ClJBLTLfU4pIWBmnvGwTbEa2cXIqj70AEDEmOmpkFComjBZ/037ueKrOdHuYmSFVD7Rw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.61.0 - '@typescript-eslint/visitor-keys': 5.61.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.1.6) - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/utils@5.61.0(eslint@8.44.0)(typescript@5.1.6): - resolution: {integrity: sha512-mV6O+6VgQmVE6+xzlA91xifndPW9ElFW8vbSF0xCT/czPXVhwDewKila1jOyRwa9AE19zKnrr7Cg5S3pJVrTWQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.44.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.61.0 - '@typescript-eslint/types': 5.61.0 - '@typescript-eslint/typescript-estree': 5.61.0(typescript@5.1.6) - eslint: 8.44.0 - eslint-scope: 5.1.1 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/visitor-keys@5.61.0: - resolution: {integrity: sha512-50XQ5VdbWrX06mQXhy93WywSFZZGsv3EOjq+lqp6WC2t+j3mb6A9xYVdrRxafvK88vg9k9u+CT4l6D8PEatjKg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.61.0 - eslint-visitor-keys: 3.4.1 - dev: true - - /abab@2.0.6: - resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} - dev: true - - /acorn-jsx@5.3.2(acorn@8.9.0): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.9.0 - dev: true - - /acorn@8.9.0: - resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - dependencies: - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true - - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: true - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /array-buffer-byte-length@1.0.0: - resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} - dependencies: - call-bind: 1.0.2 - is-array-buffer: 3.0.2 - dev: true - - /array-includes@3.1.6: - resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - get-intrinsic: 1.2.1 - is-string: 1.0.7 - dev: true - - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true - - /array.prototype.flat@1.3.1: - resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - es-shim-unscopables: 1.0.0 - dev: true - - /array.prototype.flatmap@1.3.1: - resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - es-shim-unscopables: 1.0.0 - dev: true - - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true - - /available-typed-arrays@1.0.5: - resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} - engines: {node: '>= 0.4'} - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /big-integer@1.6.51: - resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} - engines: {node: '>=0.6'} - dev: true - - /bplist-parser@0.2.0: - resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} - engines: {node: '>= 5.10.0'} - dependencies: - big-integer: 1.6.51 - dev: true - - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - - /bundle-name@3.0.0: - resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} - engines: {node: '>=12'} - dependencies: - run-applescript: 5.0.0 - dev: true - - /c8@8.0.0: - resolution: {integrity: sha512-XHA5vSfCLglAc0Xt8eLBZMv19lgiBSjnb1FLAQgnwkuhJYEonpilhEB4Ea3jPAbm0FhD6VVJrc0z73jPe7JyGQ==} - engines: {node: '>=12'} - hasBin: true - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@istanbuljs/schema': 0.1.3 - find-up: 5.0.0 - foreground-child: 2.0.0 - istanbul-lib-coverage: 3.2.0 - istanbul-lib-report: 3.0.0 - istanbul-reports: 3.1.5 - rimraf: 3.0.2 - test-exclude: 6.0.0 - v8-to-istanbul: 9.1.0 - yargs: 16.2.0 - yargs-parser: 20.2.9 - dev: true - - /call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} - dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.2.1 - dev: true - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: true - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: true - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - dev: true - - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - - /confusing-browser-globals@1.0.11: - resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} - dev: true - - /convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - dev: true - - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: true - - /cssstyle@3.0.0: - resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} - engines: {node: '>=14'} - dependencies: - rrweb-cssom: 0.6.0 - dev: true - - /data-urls@4.0.0: - resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==} - engines: {node: '>=14'} - dependencies: - abab: 2.0.6 - whatwg-mimetype: 3.0.0 - whatwg-url: 12.0.1 - dev: true - - /debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: true - - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - - /decimal.js@10.4.3: - resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - dev: true - - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true - - /default-browser-id@3.0.0: - resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} - engines: {node: '>=12'} - dependencies: - bplist-parser: 0.2.0 - untildify: 4.0.0 - dev: true - - /default-browser@4.0.0: - resolution: {integrity: sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==} - engines: {node: '>=14.16'} - dependencies: - bundle-name: 3.0.0 - default-browser-id: 3.0.0 - execa: 7.1.1 - titleize: 3.0.0 - dev: true - - /define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} - dev: true - - /define-properties@1.2.0: - resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} - engines: {node: '>= 0.4'} - dependencies: - has-property-descriptors: 1.0.0 - object-keys: 1.1.1 - dev: true - - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: true - - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: true - - /diff@5.1.0: - resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} - engines: {node: '>=0.3.1'} - dev: true - - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - dependencies: - path-type: 4.0.0 - dev: true - - /doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - dependencies: - esutils: 2.0.3 - dev: true - - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - - /domexception@4.0.0: - resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} - engines: {node: '>=12'} - dependencies: - webidl-conversions: 7.0.0 - dev: true - - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true - - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: true - - /es-abstract@1.21.2: - resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.0 - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - es-set-tostringtag: 2.0.1 - es-to-primitive: 1.2.1 - function.prototype.name: 1.1.5 - get-intrinsic: 1.2.1 - get-symbol-description: 1.0.0 - globalthis: 1.0.3 - gopd: 1.0.1 - has: 1.0.3 - has-property-descriptors: 1.0.0 - has-proto: 1.0.1 - has-symbols: 1.0.3 - internal-slot: 1.0.5 - is-array-buffer: 3.0.2 - is-callable: 1.2.7 - is-negative-zero: 2.0.2 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - is-string: 1.0.7 - is-typed-array: 1.1.10 - is-weakref: 1.0.2 - object-inspect: 1.12.3 - object-keys: 1.1.1 - object.assign: 4.1.4 - regexp.prototype.flags: 1.5.0 - safe-regex-test: 1.0.0 - string.prototype.trim: 1.2.7 - string.prototype.trimend: 1.0.6 - string.prototype.trimstart: 1.0.6 - typed-array-length: 1.0.4 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.9 - dev: true - - /es-set-tostringtag@2.0.1: - resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.1 - has: 1.0.3 - has-tostringtag: 1.0.0 - dev: true - - /es-shim-unscopables@1.0.0: - resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} - dependencies: - has: 1.0.3 - dev: true - - /es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} - dependencies: - is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - dev: true - - /esbuild-android-64@0.15.18: - resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /esbuild-android-arm64@0.15.18: - resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /esbuild-darwin-64@0.15.18: - resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /esbuild-darwin-arm64@0.15.18: - resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /esbuild-freebsd-64@0.15.18: - resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-freebsd-arm64@0.15.18: - resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-32@0.15.18: - resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-64@0.15.18: - resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-arm64@0.15.18: - resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-arm@0.15.18: - resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-mips64le@0.15.18: - resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-ppc64le@0.15.18: - resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-riscv64@0.15.18: - resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-linux-s390x@0.15.18: - resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /esbuild-netbsd-64@0.15.18: - resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-openbsd-64@0.15.18: - resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /esbuild-sunos-64@0.15.18: - resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /esbuild-windows-32@0.15.18: - resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /esbuild-windows-64@0.15.18: - resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /esbuild-windows-arm64@0.15.18: - resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /esbuild@0.15.18: - resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.15.18 - '@esbuild/linux-loong64': 0.15.18 - esbuild-android-64: 0.15.18 - esbuild-android-arm64: 0.15.18 - esbuild-darwin-64: 0.15.18 - esbuild-darwin-arm64: 0.15.18 - esbuild-freebsd-64: 0.15.18 - esbuild-freebsd-arm64: 0.15.18 - esbuild-linux-32: 0.15.18 - esbuild-linux-64: 0.15.18 - esbuild-linux-arm: 0.15.18 - esbuild-linux-arm64: 0.15.18 - esbuild-linux-mips64le: 0.15.18 - esbuild-linux-ppc64le: 0.15.18 - esbuild-linux-riscv64: 0.15.18 - esbuild-linux-s390x: 0.15.18 - esbuild-netbsd-64: 0.15.18 - esbuild-openbsd-64: 0.15.18 - esbuild-sunos-64: 0.15.18 - esbuild-windows-32: 0.15.18 - esbuild-windows-64: 0.15.18 - esbuild-windows-arm64: 0.15.18 - dev: true - - /esbuild@0.18.11: - resolution: {integrity: sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.18.11 - '@esbuild/android-arm64': 0.18.11 - '@esbuild/android-x64': 0.18.11 - '@esbuild/darwin-arm64': 0.18.11 - '@esbuild/darwin-x64': 0.18.11 - '@esbuild/freebsd-arm64': 0.18.11 - '@esbuild/freebsd-x64': 0.18.11 - '@esbuild/linux-arm': 0.18.11 - '@esbuild/linux-arm64': 0.18.11 - '@esbuild/linux-ia32': 0.18.11 - '@esbuild/linux-loong64': 0.18.11 - '@esbuild/linux-mips64el': 0.18.11 - '@esbuild/linux-ppc64': 0.18.11 - '@esbuild/linux-riscv64': 0.18.11 - '@esbuild/linux-s390x': 0.18.11 - '@esbuild/linux-x64': 0.18.11 - '@esbuild/netbsd-x64': 0.18.11 - '@esbuild/openbsd-x64': 0.18.11 - '@esbuild/sunos-x64': 0.18.11 - '@esbuild/win32-arm64': 0.18.11 - '@esbuild/win32-ia32': 0.18.11 - '@esbuild/win32-x64': 0.18.11 - dev: true - - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: true - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true - - /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.27.5)(eslint@8.44.0): - resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} - engines: {node: ^10.12.0 || >=12.0.0} - peerDependencies: - eslint: ^7.32.0 || ^8.2.0 - eslint-plugin-import: ^2.25.2 - dependencies: - confusing-browser-globals: 1.0.11 - eslint: 8.44.0 - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.61.0)(eslint@8.44.0) - object.assign: 4.1.4 - object.entries: 1.1.6 - semver: 6.3.0 - dev: true - - /eslint-config-airbnb-typescript@17.0.0(@typescript-eslint/eslint-plugin@5.61.0)(@typescript-eslint/parser@5.61.0)(eslint-plugin-import@2.27.5)(eslint@8.44.0): - resolution: {integrity: sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g==} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.13.0 - '@typescript-eslint/parser': ^5.0.0 - eslint: ^7.32.0 || ^8.2.0 - eslint-plugin-import: ^2.25.3 - dependencies: - '@typescript-eslint/eslint-plugin': 5.61.0(@typescript-eslint/parser@5.61.0)(eslint@8.44.0)(typescript@5.1.6) - '@typescript-eslint/parser': 5.61.0(eslint@8.44.0)(typescript@5.1.6) - eslint: 8.44.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.27.5)(eslint@8.44.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.61.0)(eslint@8.44.0) - dev: true - - /eslint-config-prettier@8.8.0(eslint@8.44.0): - resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - dependencies: - eslint: 8.44.0 - dev: true - - /eslint-import-resolver-node@0.3.7: - resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} - dependencies: - debug: 3.2.7 - is-core-module: 2.12.1 - resolve: 1.22.2 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.61.0)(eslint-import-resolver-node@0.3.7)(eslint@8.44.0): - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - dependencies: - '@typescript-eslint/parser': 5.61.0(eslint@8.44.0)(typescript@5.1.6) - debug: 3.2.7 - eslint: 8.44.0 - eslint-import-resolver-node: 0.3.7 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.61.0)(eslint@8.44.0): - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - '@typescript-eslint/parser': 5.61.0(eslint@8.44.0)(typescript@5.1.6) - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.44.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.61.0)(eslint-import-resolver-node@0.3.7)(eslint@8.44.0) - has: 1.0.3 - is-core-module: 2.12.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.values: 1.1.6 - resolve: 1.22.2 - semver: 6.3.0 - tsconfig-paths: 3.14.2 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true - - /eslint-plugin-prettier@5.0.0-alpha.2(eslint-config-prettier@8.8.0)(eslint@8.44.0)(prettier@3.0.0): - resolution: {integrity: sha512-F6YBCbrRzvZwcINw3crm1+/uX/i+rJYaFErPtwCfUoPLywRfY7pwBtI3yMe5OpIotuaiws8cd29oM80ca6NQSQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '*' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true - dependencies: - eslint: 8.44.0 - eslint-config-prettier: 8.8.0(eslint@8.44.0) - prettier: 3.0.0 - prettier-linter-helpers: 1.0.0 - synckit: 0.8.5 - dev: true - - /eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - dev: true - - /eslint-scope@7.2.0: - resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - - /eslint-visitor-keys@3.4.1: - resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /eslint@8.44.0: - resolution: {integrity: sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.44.0) - '@eslint-community/regexpp': 4.5.1 - '@eslint/eslintrc': 2.1.0 - '@eslint/js': 8.44.0 - '@humanwhocodes/config-array': 0.11.10 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.0 - eslint-visitor-keys: 3.4.1 - espree: 9.6.0 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.20.0 - graphemer: 1.4.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.3 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - - /espree@9.6.0: - resolution: {integrity: sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.9.0 - acorn-jsx: 5.3.2(acorn@8.9.0) - eslint-visitor-keys: 3.4.1 - dev: true - - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} - dependencies: - estraverse: 5.3.0 - dev: true - - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - dependencies: - estraverse: 5.3.0 - dev: true - - /estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - dev: true - - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true - - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - - /execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: true - - /execa@7.1.1: - resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} - engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 4.3.1 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.1.0 - onetime: 6.0.0 - signal-exit: 3.0.7 - strip-final-newline: 3.0.0 - dev: true - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true - - /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true - - /fast-glob@3.3.0: - resolution: {integrity: sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: true - - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true - - /fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} - dependencies: - reusify: 1.0.4 - dev: true - - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flat-cache: 3.0.4 - dev: true - - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - - /flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 - dev: true - - /flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - dev: true - - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - dependencies: - is-callable: 1.2.7 - dev: true - - /foreground-child@2.0.0: - resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} - engines: {node: '>=8.0.0'} - dependencies: - cross-spawn: 7.0.3 - signal-exit: 3.0.7 - dev: true - - /form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - dev: true - - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true - - /function.prototype.name@1.1.5: - resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - functions-have-names: 1.2.3 - dev: true - - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: true - - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: true - - /get-intrinsic@1.2.1: - resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} - dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-proto: 1.0.1 - has-symbols: 1.0.3 - dev: true - - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: true - - /get-symbol-description@1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - - /globals@13.20.0: - resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - - /globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} - engines: {node: '>= 0.4'} - dependencies: - define-properties: 1.2.0 - dev: true - - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.0 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 - dev: true - - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - dependencies: - get-intrinsic: 1.2.1 - dev: true - - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true - - /has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /has-property-descriptors@1.0.0: - resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} - dependencies: - get-intrinsic: 1.2.1 - dev: true - - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - dev: true - - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - dev: true - - /has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: true - - /has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} - dependencies: - function-bind: 1.1.1 - dev: true - - /html-encoding-sniffer@3.0.0: - resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} - engines: {node: '>=12'} - dependencies: - whatwg-encoding: 2.0.0 - dev: true - - /html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - dev: true - - /http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} - dependencies: - '@tootallnate/once': 2.0.0 - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true - - /https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true - - /human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - dev: true - - /human-signals@4.3.1: - resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} - engines: {node: '>=14.18.0'} - dev: true - - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: true - - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - dev: true - - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: true - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: true - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true - - /internal-slot@1.0.5: - resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.1 - has: 1.0.3 - side-channel: 1.0.4 - dev: true - - /is-array-buffer@3.0.2: - resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-typed-array: 1.1.10 - dev: true - - /is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} - dependencies: - has-bigints: 1.0.2 - dev: true - - /is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: true - - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: true - - /is-core-module@2.12.1: - resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} - dependencies: - has: 1.0.3 - dev: true - - /is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - dev: true - - /is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - dev: true - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: true - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - - /is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - dependencies: - is-docker: 3.0.0 - dev: true - - /is-negative-zero@2.0.2: - resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} - engines: {node: '>= 0.4'} - dev: true - - /is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true - - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true - - /is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - dev: true - - /is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: true - - /is-shared-array-buffer@1.0.2: - resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} - dependencies: - call-bind: 1.0.2 - dev: true - - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - dev: true - - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: true - - /is-typed-array@1.1.10: - resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - dev: true - - /is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - dependencies: - call-bind: 1.0.2 - dev: true - - /is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - dependencies: - is-docker: 2.2.1 - dev: true - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true - - /istanbul-lib-coverage@3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} - engines: {node: '>=8'} - dev: true - - /istanbul-lib-report@3.0.0: - resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} - engines: {node: '>=8'} - dependencies: - istanbul-lib-coverage: 3.2.0 - make-dir: 3.1.0 - supports-color: 7.2.0 - dev: true - - /istanbul-reports@3.1.5: - resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} - engines: {node: '>=8'} - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.0 - dev: true - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /jsdom@22.1.0: - resolution: {integrity: sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==} - engines: {node: '>=16'} - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - dependencies: - abab: 2.0.6 - cssstyle: 3.0.0 - data-urls: 4.0.0 - decimal.js: 10.4.3 - domexception: 4.0.0 - form-data: 4.0.0 - html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.5 - parse5: 7.1.2 - rrweb-cssom: 0.6.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 4.1.3 - w3c-xmlserializer: 4.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 2.0.0 - whatwg-mimetype: 3.0.0 - whatwg-url: 12.0.1 - ws: 8.13.0 - xml-name-validator: 4.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true - - /json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - dependencies: - minimist: 1.2.8 - dev: true - - /kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - dev: true - - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true - - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true - - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - - /make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - dependencies: - semver: 6.3.0 - dev: true - - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true - - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true - - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: true - - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: true - - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: true - - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: true - - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true - - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true - - /mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - dev: true - - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true - - /nanospy@1.0.0: - resolution: {integrity: sha512-wvmmALNstRRhLhy7RV11NCRY2k1zxstImiju4VyyKNNRIKDVjyBtmEd/Q4G82/3dN4VSTe+0PRR3DUAASSbEEQ==} - engines: {node: ^8.0.0 || ^10.0.0 || ^12.0.0 || ^14.0.0 || ^16.0.0 || ^18.0.0 || >=20.0.0} - dev: true - - /natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - dev: true - - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true - - /npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - dependencies: - path-key: 3.1.1 - dev: true - - /npm-run-path@5.1.0: - resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - path-key: 4.0.0 - dev: true - - /nwsapi@2.2.5: - resolution: {integrity: sha512-6xpotnECFy/og7tKSBVmUNft7J3jyXAka4XvG6AUhFWRz+Q/Ljus7znJAA3bxColfQLdS+XsjoodtJfCgeTEFQ==} - dev: true - - /object-inspect@1.12.3: - resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} - dev: true - - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - dev: true - - /object.assign@4.1.4: - resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - has-symbols: 1.0.3 - object-keys: 1.1.1 - dev: true - - /object.entries@1.1.6: - resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - dev: true - - /object.values@1.1.6: - resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - dev: true - - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: true - - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - dependencies: - mimic-fn: 2.1.0 - dev: true - - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - dependencies: - mimic-fn: 4.0.0 - dev: true - - /open@9.1.0: - resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==} - engines: {node: '>=14.16'} - dependencies: - default-browser: 4.0.0 - define-lazy-prop: 3.0.0 - is-inside-container: 1.0.0 - is-wsl: 2.2.0 - dev: true - - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} - engines: {node: '>= 0.8.0'} - dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - dev: true - - /parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} - dependencies: - entities: 4.5.0 - dev: true - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true - - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true - - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true - - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true - - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true - - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true - - /prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} - dependencies: - fast-diff: 1.3.0 - dev: true - - /prettier@3.0.0: - resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} - engines: {node: '>=14'} - hasBin: true - dev: true - - /psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - dev: true - - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} - engines: {node: '>=6'} - dev: true - - /querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - dev: true - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - - /regexp.prototype.flags@1.5.0: - resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - functions-have-names: 1.2.3 - dev: true - - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: true - - /requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: true - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true - - /resolve@1.22.2: - resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} - hasBin: true - dependencies: - is-core-module: 2.12.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: true - - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - - /rrweb-cssom@0.6.0: - resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} - dev: true - - /run-applescript@5.0.0: - resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} - engines: {node: '>=12'} - dependencies: - execa: 5.1.1 - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - - /sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - dependencies: - mri: 1.2.0 - dev: true - - /safe-regex-test@1.0.0: - resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-regex: 1.1.4 - dev: true - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true - - /saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - dependencies: - xmlchars: 2.2.0 - dev: true - - /semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true - dev: true - - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: true - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true - - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - object-inspect: 1.12.3 - dev: true - - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true - - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: true - - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: true - - /string.prototype.trim@1.2.7: - resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - dev: true - - /string.prototype.trimend@1.0.6: - resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - dev: true - - /string.prototype.trimstart@1.0.6: - resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - dev: true - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: true - - /strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - dev: true - - /strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - dev: true - - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true - - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: true - - /symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - dev: true - - /synckit@0.8.5: - resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} - engines: {node: ^14.18.0 || >=16.0.0} - dependencies: - '@pkgr/utils': 2.4.2 - tslib: 2.6.0 - dev: true - - /test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - dev: true - - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - - /titleize@3.0.0: - resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} - engines: {node: '>=12'} - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - - /tough-cookie@4.1.3: - resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} - engines: {node: '>=6'} - dependencies: - psl: 1.9.0 - punycode: 2.3.0 - universalify: 0.2.0 - url-parse: 1.5.10 - dev: true - - /tr46@4.1.1: - resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} - engines: {node: '>=14'} - dependencies: - punycode: 2.3.0 - dev: true - - /tsconfig-paths@3.14.2: - resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - dev: true - - /tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true - - /tslib@2.6.0: - resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} - dev: true - - /tsm@2.3.0: - resolution: {integrity: sha512-++0HFnmmR+gMpDtKTnW3XJ4yv9kVGi20n+NfyQWB9qwJvTaIWY9kBmzek2YUQK5APTQ/1DTrXmm4QtFPmW9Rzw==} - engines: {node: '>=12'} - hasBin: true - dependencies: - esbuild: 0.15.18 - dev: true - - /tsutils@3.21.0(typescript@5.1.6): - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - dependencies: - tslib: 1.14.1 - typescript: 5.1.6 - dev: true - - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - - /typed-array-length@1.0.4: - resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} - dependencies: - call-bind: 1.0.2 - for-each: 0.3.3 - is-typed-array: 1.1.10 - dev: true - - /typescript@5.1.6: - resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - - /unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - dependencies: - call-bind: 1.0.2 - has-bigints: 1.0.2 - has-symbols: 1.0.3 - which-boxed-primitive: 1.0.2 - dev: true - - /universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} - dev: true - - /untildify@4.0.0: - resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} - engines: {node: '>=8'} - dev: true - - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.3.0 - dev: true - - /url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - dev: true - - /uvu@0.5.6: - resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} - engines: {node: '>=8'} - hasBin: true - dependencies: - dequal: 2.0.3 - diff: 5.1.0 - kleur: 4.1.5 - sade: 1.8.1 - dev: true - - /v8-to-istanbul@9.1.0: - resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} - engines: {node: '>=10.12.0'} - dependencies: - '@jridgewell/trace-mapping': 0.3.18 - '@types/istanbul-lib-coverage': 2.0.4 - convert-source-map: 1.9.0 - dev: true - - /w3c-xmlserializer@4.0.0: - resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} - engines: {node: '>=14'} - dependencies: - xml-name-validator: 4.0.0 - dev: true - - /webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - dev: true - - /whatwg-encoding@2.0.0: - resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} - engines: {node: '>=12'} - dependencies: - iconv-lite: 0.6.3 - dev: true - - /whatwg-mimetype@3.0.0: - resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} - engines: {node: '>=12'} - dev: true - - /whatwg-url@12.0.1: - resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==} - engines: {node: '>=14'} - dependencies: - tr46: 4.1.1 - webidl-conversions: 7.0.0 - dev: true - - /which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} - dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 - dev: true - - /which-typed-array@1.1.9: - resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - is-typed-array: 1.1.10 - dev: true - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: true - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true - - /ws@8.13.0: - resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - - /xml-name-validator@4.0.0: - resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} - engines: {node: '>=12'} - dev: true - - /xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - dev: true - - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - - /yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - dev: true - - /yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - dependencies: - cliui: 7.0.4 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - dev: true - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true diff --git a/src/browser/index.ts b/src/browser/index.ts new file mode 100644 index 0000000..d3c2090 --- /dev/null +++ b/src/browser/index.ts @@ -0,0 +1,6 @@ +export * from './runtime'; + +export * from '../events'; +export * from '../store'; +export * from '../types'; +export * from '../utils'; diff --git a/src/browser/runtime.ts b/src/browser/runtime.ts new file mode 100644 index 0000000..45a2395 --- /dev/null +++ b/src/browser/runtime.ts @@ -0,0 +1,110 @@ +import type { InferRefs, LowercaseKeys, Refs } from '../types'; +import { create } from '../utils'; + +interface RefMeta { + /** Ref key name. */ + readonly k: string; + /** Distance from previous ref node or root. */ + readonly d: number; +} + +export interface View extends Node, ChildNode { + /** @private */ + $$refs: readonly RefMeta[]; +} + +const compilerTemplate = create('template'); +const treeWalker = document.createTreeWalker(compilerTemplate); +let str: string | null | undefined; + +/* @__NOINLINE__ */ +const collector = (node: Node): string | undefined => { + // 1 = Node.ELEMENT_NODE + if (node.nodeType === 1) { + const attrs = (node as Element).attributes; + let index = attrs.length; + + while (index--) { + str = attrs[index].name; + if (str[0] === '@') { + (node as Element).removeAttribute(str); + return str.slice(1); + } + } + return; + } + + str = node.nodeValue; + if (str && str[0] === '@') { + node.nodeValue = ''; + return str.slice(1); + } +}; + +/** + * Creates a DOM node from a template and collects ref node metadata. + * @param template - HTML template string. + */ +export const h = ( + template: string, +): View & T => { + compilerTemplate.innerHTML = template + // reduce any whitespace to a single space + .replace(/\s+/g, ' ') + // remove space adjacent to tags + .replace(/> /g, '>') + .replace(/ ( + template: TemplateStringsArray, + ...substitutions: unknown[] +): View & T => h(String.raw(template, ...substitutions)); + +/** + * Collects node refs from a compiled template view. + * @param root - Root node. + * @param view - Compiled template view. + * @returns An object mapping ref nodes keyed by their ref name. Note that some + * browsers lowercase rendered HTML element attribute names so we lowercase the + * typed key names to bring awareness to this. + */ +/* @__NOINLINE__ */ +export const collect = = Refs>( + root: Node, + view: View, +): LowercaseKeys => { + const walker = treeWalker; // local var is faster in some JS engines + const refs: Refs = {}; + const len = view.$$refs.length; + let index = 0; + let metadata: RefMeta; + let distance: number; + walker.currentNode = root; + + for (; index < len; index++) { + metadata = view.$$refs[index]; + distance = metadata.d; + while (distance--) walker.nextNode(); + refs[metadata.k] = walker.currentNode; + } + + return refs as LowercaseKeys; +}; diff --git a/src/compile.ts b/src/compile.ts deleted file mode 100644 index fc74cce..0000000 --- a/src/compile.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { Ref, Refs, S1Node } from './types'; -import { create } from './utils'; - -const compilerTemplate = create('template'); -const treeWalker = document.createTreeWalker(compilerTemplate); - -const collector = (node: Node): string | void => { - // 1 = Node.ELEMENT_NODE - if (node.nodeType === 1) { - const attrs = (node as Element).attributes; - let index = attrs.length; - - while (index--) { - const aname = attrs[index].name; - if (aname[0] === '#') { - (node as Element).removeAttribute(aname); - return aname.slice(1); - } - } - return; - } - - const content = node.nodeValue; - if (content && content[0] === '#') { - node.nodeValue = ''; - return content.slice(1); - } -}; - -const roll = (n: number) => { - while (--n) treeWalker.nextNode(); - return treeWalker.currentNode; -}; - -const genPath = (node: Node) => { - const indices: Ref[] = []; - let ref: string | void; - let index = 0; - treeWalker.currentNode = node; - - while (node) { - if ((ref = collector(node))) { - indices.push({ i: index + 1, ref }); - index = 1; - } else { - index++; - } - (node as Node | null) = treeWalker.nextNode(); - } - - return indices; -}; - -// eslint-disable-next-line func-names -const collect = function (this: S1Node, node: Node): T { - const refs: Refs = {}; - treeWalker.currentNode = node; - - for (const x of this._refs) { - refs[x.ref] = roll(x.i); - } - - return refs as T; -}; - -export const h = (template: string): S1Node => { - // Compatible template literal minifier is mandatory for production consumers! - compilerTemplate.innerHTML = - process.env.NODE_ENV === 'production' - ? template - : template - // to get the correct first node - .trim() - // faster genPath and cleaner test snapshots - .replace(/\n\s+/g, '\n') - // remove whitespace around ref tags in Text nodes - .replace(/>\s+#(\w+)\s+#$1<'); - - const node = compilerTemplate.content.firstChild as S1Node; - node._refs = genPath(node); - node.collect = collect; - - return node; -}; - -export const html = ( - template: TemplateStringsArray, - ...substitutions: unknown[] -): S1Node => h(String.raw(template, ...substitutions)); diff --git a/src/events.ts b/src/events.ts index 6a7b066..68fe5f5 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,4 +1,4 @@ -const configuredEvents: Record = {}; +const configuredEvents: Record = {}; const nativeToSyntheticEvent = (event: Event) => { // eslint-disable-next-line prefer-template @@ -9,22 +9,20 @@ const nativeToSyntheticEvent = (event: Event) => { // @ts-expect-error - unavoidable string indexing if (node[eventKey]) { // @ts-expect-error - unavoidable string indexing - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - node[eventKey](event); - return; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return + return node[eventKey](event); } node = node.parentNode; } }; export const setupSyntheticEvent = (type: keyof DocumentEventMap): void => { - if (configuredEvents[type]) return; - - document.addEventListener(type, nativeToSyntheticEvent); - configuredEvents[type] = true; + configuredEvents[type] ??= + // biome-ignore lint/style/noCommaOperator: code compactness + (document.addEventListener(type, nativeToSyntheticEvent), true); }; export const deleteSyntheticEvent = (type: keyof DocumentEventMap): void => { + configuredEvents[type] = null; document.removeEventListener(type, nativeToSyntheticEvent); - configuredEvents[type] = false; }; diff --git a/src/index.ts b/src/index.ts index 9145b25..a37d932 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ -export * from './compile'; +export * from './runtime'; + export * from './events'; +export * from './store'; export * from './types'; export * from './utils'; diff --git a/src/macro.ts b/src/macro.ts new file mode 100644 index 0000000..1143bfe --- /dev/null +++ b/src/macro.ts @@ -0,0 +1,121 @@ +export interface CompileOptions { + /** + * Whether to keep HTML comments in output HTML. When keepComments is true, + * comments can be used as refs. + * @default false + */ + keepComments?: boolean; + /** + * Whether to keep spaces adjacent to tags in output HTML. When keepSpaces + * is false, `
x
` becomes `
x
`. + * @default false + */ + keepSpaces?: boolean; +} + +/** + * Bun macro which compiles a template string at build-time into a format that + * can be used by the runtime. + * @param template - HTML template string. + * @param options - Compile options. + */ +export function compile( + template: string, + { keepComments, keepSpaces }: CompileOptions = {}, +): { html: string; k: readonly string[]; d: readonly number[] } { + const k: string[] = []; + const d: number[] = []; + let distance = 0; + let whitespaceSensitiveBlock = false; + let root: boolean | undefined; + + const html = new HTMLRewriter() + .onDocument({ + comments(node) { + if (keepComments) { + // TODO: Add documentation that precompiled mode supports using + // comments as refs. Requires the keepComments option to be true. + const text = node.text.trim(); + if (text[0] === '@') { + k.push(text.slice(1)); + d.push(distance); + distance = 0; + // TODO: use node.replace() once lol-html fixes it for comments + // node.replace('', { html: true }); + node.remove(); + node.after('', { html: true }); + } + distance++; + } else { + node.remove(); + } + }, + }) + .on('*', { + element(node) { + if (!root) { + if (root === undefined) { + root = true; + node.onEndTag(() => { + root = false; + }); + } else { + // eslint-disable-next-line no-console + console.error( + 'Expected template to have a single root element:', + template, + ); + } + } + + if (node.tagName === 'pre' || node.tagName === 'code') { + whitespaceSensitiveBlock = true; + node.onEndTag(() => { + whitespaceSensitiveBlock = false; + }); + } + for (const [name] of node.attributes) { + if (name[0] === '@') { + k.push(name.slice(1)); + d.push(distance); + distance = 0; + node.removeAttribute(name); + break; + } + } + distance++; + }, + // This text handler is invoked twice for each Text node: first with the + // actual text, then with an empty last chunk. This behaviour stems from + // the fact that the data provided to HTMLRewriter.transform() can be + // streamed; where the last empty chunk signals the end of the text. + text(chunk) { + if (!chunk.lastInTextNode) { + const text = chunk.text.trim(); + if (!text) { + if (!whitespaceSensitiveBlock) { + chunk.remove(); + } + return; + } + if (text[0] === '@') { + k.push(text.slice(1)); + d.push(distance); + distance = 0; + // replace with single space which renders a Text node at runtime + chunk.replace(' ', { html: true }); + } else if (!whitespaceSensitiveBlock) { + // reduce any whitespace to a single space + chunk.replace( + (keepSpaces ? chunk.text : text).replace(/\s+/g, ' '), + { html: true }, + ); + } + distance++; + } + }, + }) + .transform(template.trim()); + + return { html, k, d }; +} diff --git a/src/reconcile/keyed.ts b/src/reconcile/keyed.ts index 9b10ff5..60c5193 100644 --- a/src/reconcile/keyed.ts +++ b/src/reconcile/keyed.ts @@ -1,20 +1,65 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unused-expressions */ -/* eslint-disable @typescript-eslint/no-use-before-define */ -/* eslint-disable block-scoped-var */ -/* eslint-disable no-continue */ -/* eslint-disable no-labels */ -/* eslint-disable no-multi-assign */ -/* eslint-disable no-sequences */ -/* eslint-disable no-var */ -/* eslint-disable vars-on-top */ +/* eslint-disable no-continue, no-labels, unicorn/no-for-loop, unicorn/no-new-array */ import { noop } from '../utils'; +// https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L396C10-L396C30 +const findGreatestIndexLEQ = (seq: number[], n: number) => { + // invariant: lo is guaranteed to be index of a value <= n, hi to be > + // therefore, they actually start out of range: (-1, last + 1) + let lo = -1; + let hi = seq.length; + + // fast path for simple increasing sequences + if (hi > 0 && seq[hi - 1] <= n) return hi - 1; + + while (hi - lo > 1) { + const mid = Math.floor((lo + hi) / 2); + if (seq[mid] > n) { + hi = mid; + } else { + lo = mid; + } + } + + return lo; +}; + +// return an array of the indices of ns that comprise the longest increasing subsequence within ns +// https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L368C10-L368C46 +const longestPositiveIncreasingSubsequence = ( + ns: number[], + newStart: number, +) => { + const seq: number[] = []; + const is: number[] = []; + const pre = new Array(ns.length); + const len = ns.length; + let i = newStart; + let l = -1; + + for (; i < len; i++) { + const n = ns[i]; + if (n < 0) continue; + const j = findGreatestIndexLEQ(seq, n); + if (j !== -1) pre[i] = is[j]; + if (j === l) { + l++; + seq[l] = n; + is[l] = i; + } else if (n < seq[j + 1]) { + seq[j + 1] = n; + is[j + 1] = i; + } + } + + for (i = is[l]; l >= 0; i = pre[i], l--) { + seq[l] = i; + } + + return seq; +}; + // This is almost straightforward implementation of reconcillation algorithm // based on ivi documentation: // https://github.com/localvoid/ivi/blob/2c81ead934b9128e092cc2a5ef2d3cabc73cb5dd/packages/ivi/src/vdom/implementation.ts#L1366 @@ -24,30 +69,30 @@ import { noop } from '../utils'; // How this implementation differs from others, is that it's working with data directly, // without maintaining nodes arrays, and uses dom props firstChild/lastChild/nextSibling // for markers moving. -export const reconcile = ( - key: keyof N, +export const reconcile = ( + key: keyof T, parent: Element, - renderedValues: any[], - data: any[], - createFn: (...args: T) => N, - updateFn: (node: N, ...args: T) => void = noop, + renderedData: T[], + data: T[], + createFn: (itemData: T) => N, + updateFn: (node: N, itemData: T) => void = noop, beforeNode?: Node, afterNode?: Node | null, ): void => { + const dataLen = data.length; + // Fast path for clear - if (data.length === 0) { + if (dataLen === 0) { if (beforeNode !== undefined || afterNode !== undefined) { let node = - beforeNode !== undefined ? beforeNode.nextSibling : parent.firstChild; - let tmp; + beforeNode === undefined ? parent.firstChild : beforeNode.nextSibling; + let tmp: ChildNode | null; if (afterNode === undefined) afterNode = null; while (node !== afterNode) { - // @ts-expect-error - FIXME:! - tmp = node.nextSibling; - // @ts-expect-error - FIXME:! - parent.removeChild(node); + tmp = node!.nextSibling; + node!.remove(); node = tmp; } } else { @@ -57,14 +102,17 @@ export const reconcile = ( } // Fast path for create - if (renderedValues.length === 0) { - let node; - const mode = afterNode !== undefined ? 1 : 0; - for (let i = 0, len = data.length; i < len; i++) { - // @ts-expect-error - FIXME:! - node = createFn(data[i]); - // @ts-expect-error - FIXME:! - mode ? parent.insertBefore(node, afterNode) : parent.appendChild(node); + if (renderedData.length === 0) { + const insert = afterNode !== undefined; + let index = 0; + let node: Node; + for (; index < dataLen; index++) { + node = createFn(data[index]); + if (insert) { + parent.insertBefore(node, afterNode!); + } else { + parent.appendChild(node); + } } return; } @@ -72,81 +120,80 @@ export const reconcile = ( let prevStart = 0; let newStart = 0; let loop = true; - let prevEnd = renderedValues.length - 1; - let newEnd = data.length - 1; - let a; - let b; - let prevStartNode = beforeNode ? beforeNode.nextSibling : parent.firstChild; + let prevEnd = renderedData.length - 1; + let newEnd = dataLen - 1; + let a: T; + let b: T; + let prevStartNode: Node | null = beforeNode + ? beforeNode.nextSibling + : parent.firstChild; let newStartNode = prevStartNode; - let prevEndNode = afterNode ? afterNode.previousSibling : parent.lastChild; + let prevEndNode: Node | null = afterNode + ? afterNode.previousSibling + : parent.lastChild; fixes: while (loop) { loop = false; - let _node; + let tmpNode: ChildNode | null; // Skip prefix - (a = renderedValues[prevStart]), (b = data[newStart]); + a = renderedData[prevStart]; + b = data[newStart]; while (a[key] === b[key]) { - // @ts-expect-error - FIXME:! + // @ts-expect-error - FIXME: prevStartNode type updateFn(prevStartNode, b); prevStart++; newStart++; - // @ts-expect-error - FIXME:! - newStartNode = prevStartNode = prevStartNode.nextSibling; + newStartNode = prevStartNode = prevStartNode!.nextSibling; if (prevEnd < prevStart || newEnd < newStart) break fixes; - a = renderedValues[prevStart]; + a = renderedData[prevStart]; b = data[newStart]; } // Skip suffix - (a = renderedValues[prevEnd]), (b = data[newEnd]); + a = renderedData[prevEnd]; + b = data[newEnd]; while (a[key] === b[key]) { - // @ts-expect-error - FIXME:! - updateFn(prevEndNode, b); + updateFn(prevEndNode as N, b); prevEnd--; newEnd--; afterNode = prevEndNode; - // @ts-expect-error - FIXME:! - prevEndNode = prevEndNode.previousSibling; + prevEndNode = prevEndNode!.previousSibling; if (prevEnd < prevStart || newEnd < newStart) break fixes; - a = renderedValues[prevEnd]; + a = renderedData[prevEnd]; b = data[newEnd]; } // Fast path to swap backward - (a = renderedValues[prevEnd]), (b = data[newStart]); + a = renderedData[prevEnd]; + b = data[newStart]; while (a[key] === b[key]) { loop = true; - // @ts-expect-error - FIXME:! - updateFn(prevEndNode, b); - // @ts-expect-error - FIXME:! - _node = prevEndNode.previousSibling; - // @ts-expect-error - FIXME:! - parent.insertBefore(prevEndNode, newStartNode); - prevEndNode = _node; + updateFn(prevEndNode as N, b); + tmpNode = prevEndNode!.previousSibling; + parent.insertBefore(prevEndNode!, newStartNode); + prevEndNode = tmpNode; newStart++; prevEnd--; if (prevEnd < prevStart || newEnd < newStart) break fixes; - a = renderedValues[prevEnd]; + a = renderedData[prevEnd]; b = data[newStart]; } // Fast path to swap forward - (a = renderedValues[prevStart]), (b = data[newEnd]); + a = renderedData[prevStart]; + b = data[newEnd]; while (a[key] === b[key]) { loop = true; - // @ts-expect-error - FIXME:! - updateFn(prevStartNode, b); - // @ts-expect-error - FIXME:! - _node = prevStartNode.nextSibling; - // @ts-expect-error - FIXME:! - parent.insertBefore(prevStartNode, afterNode); + updateFn(prevStartNode as N, b); + tmpNode = prevStartNode!.nextSibling; + parent.insertBefore(prevStartNode!, afterNode!); prevStart++; afterNode = prevStartNode; - prevStartNode = _node; + prevStartNode = tmpNode; newEnd--; if (prevEnd < prevStart || newEnd < newStart) break fixes; - a = renderedValues[prevStart]; + a = renderedData[prevStart]; b = data[newEnd]; } } @@ -154,16 +201,13 @@ export const reconcile = ( // Fast path for shrink if (newEnd < newStart) { if (prevStart <= prevEnd) { - let next; + let next: ChildNode | null; while (prevStart <= prevEnd) { if (prevEnd === 0) { - // @ts-expect-error - FIXME:! - parent.removeChild(prevEndNode); + (prevEndNode as ChildNode).remove(); } else { - // @ts-expect-error - FIXME:! - next = prevEndNode.previousSibling; - // @ts-expect-error - FIXME:! - parent.removeChild(prevEndNode); + next = prevEndNode!.previousSibling; + (prevEndNode as ChildNode).remove(); prevEndNode = next; } prevEnd--; @@ -175,13 +219,15 @@ export const reconcile = ( // Fast path for add if (prevEnd < prevStart) { if (newStart <= newEnd) { - let node; + let node: Node; const mode = afterNode ? 1 : 0; while (newStart <= newEnd) { - // @ts-expect-error - FIXME:! node = createFn(data[newStart]); - // @ts-expect-error - FIXME:! - mode ? parent.insertBefore(node, afterNode) : parent.appendChild(node); + if (mode) { + parent.insertBefore(node, afterNode!); + } else { + parent.appendChild(node); + } newStart++; } } @@ -189,19 +235,19 @@ export const reconcile = ( } // Positions for reusing nodes from current DOM state - const P = new Array(newEnd + 1 - newStart); + const P = new Array(newEnd + 1 - newStart); for (let i = newStart; i <= newEnd; i++) P[i] = -1; // Index to resolve position from current to new const I = new Map(); for (let i = newStart; i <= newEnd; i++) I.set(data[i][key], i); - let reusingNodes = newStart + data.length - 1 - newEnd; - const toRemove = []; + let reusingNodes = newStart + dataLen - 1 - newEnd; + const toRemove: number[] = []; for (let i = prevStart; i <= prevEnd; i++) { - if (I.has(renderedValues[i][key])) { - P[I.get(renderedValues[i][key])] = i; + if (I.has(renderedData[i][key])) { + P[I.get(renderedData[i][key])] = i; reusingNodes++; } else { toRemove.push(i); @@ -212,16 +258,14 @@ export const reconcile = ( if (reusingNodes === 0) { if (beforeNode !== undefined || afterNode !== undefined) { let node = - beforeNode !== undefined ? beforeNode.nextSibling : parent.firstChild; - let tmp; + beforeNode === undefined ? parent.firstChild : beforeNode.nextSibling; + let tmp: ChildNode | null; if (afterNode === undefined) afterNode = null; while (node !== afterNode) { - // @ts-expect-error - FIXME:! - tmp = node.nextSibling; - // @ts-expect-error - FIXME:! - parent.removeChild(node); + tmp = node!.nextSibling; + node!.remove(); node = tmp; prevStart++; } @@ -229,13 +273,15 @@ export const reconcile = ( parent.textContent = ''; } - let node; + let node: Node; const mode = afterNode ? 1 : 0; for (let i = newStart; i <= newEnd; i++) { - // @ts-expect-error - FIXME:! node = createFn(data[i]); - // @ts-expect-error - FIXME:! - mode ? parent.insertBefore(node, afterNode) : parent.appendChild(node); + if (mode) { + parent.insertBefore(node, afterNode!); + } else { + parent.appendChild(node); + } } return; @@ -245,92 +291,33 @@ export const reconcile = ( const longestSeq = longestPositiveIncreasingSubsequence(P, newStart); // Collect nodes to work with them - const nodes = []; + const nodes: (Node | null)[] = []; let tmpC = prevStartNode; for (let i = prevStart; i <= prevEnd; i++) { nodes[i] = tmpC; - // @ts-expect-error - FIXME:! - tmpC = tmpC.nextSibling; + tmpC = tmpC!.nextSibling; } for (let i = 0; i < toRemove.length; i++) { - // @ts-expect-error - FIXME:! - parent.removeChild(nodes[toRemove[i]]); + (nodes[toRemove[i]] as ChildNode).remove(); } let lisIdx = longestSeq.length - 1; - let tmpD; + let tmpD: Node; for (let i = newEnd; i >= newStart; i--) { if (longestSeq[lisIdx] === i) { afterNode = nodes[P[longestSeq[lisIdx]]]; - // @ts-expect-error - FIXME:! - updateFn(afterNode, data[i]); + updateFn(afterNode as N, data[i]); lisIdx--; } else { if (P[i] === -1) { - // @ts-expect-error - FIXME:! tmpD = createFn(data[i]); } else { - tmpD = nodes[P[i]]; - // @ts-expect-error - FIXME:! - updateFn(tmpD, data[i]); + tmpD = nodes[P[i]]!; + updateFn(tmpD as N, data[i]); } - // @ts-expect-error - FIXME:! - parent.insertBefore(tmpD, afterNode); + parent.insertBefore(tmpD, afterNode!); afterNode = tmpD; } } }; - -// Picked from -// https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L368 - -// return an array of the indices of ns that comprise the longest increasing subsequence within ns -const longestPositiveIncreasingSubsequence = (ns: any[], newStart: number) => { - const seq = []; - const is = []; - let l = -1; - const pre = new Array(ns.length); - - for (var i = newStart, len = ns.length; i < len; i++) { - const n = ns[i]; - if (n < 0) continue; - const j = findGreatestIndexLEQ(seq, n); - if (j !== -1) pre[i] = is[j]; - if (j === l) { - l++; - seq[l] = n; - is[l] = i; - } else if (n < seq[j + 1]) { - seq[j + 1] = n; - is[j + 1] = i; - } - } - - for (i = is[l]; l >= 0; i = pre[i], l--) { - seq[l] = i; - } - - return seq; -}; - -const findGreatestIndexLEQ = (seq: any[], n: number) => { - // invariant: lo is guaranteed to be index of a value <= n, hi to be > - // therefore, they actually start out of range: (-1, last + 1) - let lo = -1; - let hi = seq.length; - - // fast path for simple increasing sequences - if (hi > 0 && seq[hi - 1] <= n) return hi - 1; - - while (hi - lo > 1) { - const mid = Math.floor((lo + hi) / 2); - if (seq[mid] > n) { - hi = mid; - } else { - lo = mid; - } - } - - return lo; -}; diff --git a/src/reconcile/non-keyed.ts b/src/reconcile/non-keyed.ts index 6421f0a..f60808d 100644 --- a/src/reconcile/non-keyed.ts +++ b/src/reconcile/non-keyed.ts @@ -1,20 +1,65 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unused-expressions */ -/* eslint-disable @typescript-eslint/no-use-before-define */ -/* eslint-disable block-scoped-var */ -/* eslint-disable no-continue */ -/* eslint-disable no-labels */ -/* eslint-disable no-multi-assign */ -/* eslint-disable no-sequences */ -/* eslint-disable no-var */ -/* eslint-disable vars-on-top */ +/* eslint-disable no-continue, no-labels, unicorn/no-for-loop, unicorn/no-new-array */ import { noop } from '../utils'; +// https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L396C10-L396C30 +const findGreatestIndexLEQ = (seq: number[], n: number) => { + // invariant: lo is guaranteed to be index of a value <= n, hi to be > + // therefore, they actually start out of range: (-1, last + 1) + let lo = -1; + let hi = seq.length; + + // fast path for simple increasing sequences + if (hi > 0 && seq[hi - 1] <= n) return hi - 1; + + while (hi - lo > 1) { + const mid = Math.floor((lo + hi) / 2); + if (seq[mid] > n) { + hi = mid; + } else { + lo = mid; + } + } + + return lo; +}; + +// return an array of the indices of ns that comprise the longest increasing subsequence within ns +// https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L368C10-L368C46 +const longestPositiveIncreasingSubsequence = ( + ns: number[], + newStart: number, +) => { + const seq: number[] = []; + const is: number[] = []; + const pre = new Array(ns.length); + const len = ns.length; + let i = newStart; + let l = -1; + + for (; i < len; i++) { + const n = ns[i]; + if (n < 0) continue; + const j = findGreatestIndexLEQ(seq, n); + if (j !== -1) pre[i] = is[j]; + if (j === l) { + l++; + seq[l] = n; + is[l] = i; + } else if (n < seq[j + 1]) { + seq[j + 1] = n; + is[j + 1] = i; + } + } + + for (i = is[l]; l >= 0; i = pre[i], l--) { + seq[l] = i; + } + + return seq; +}; + // This is almost straightforward implementation of reconcillation algorithm // based on ivi documentation: // https://github.com/localvoid/ivi/blob/2c81ead934b9128e092cc2a5ef2d3cabc73cb5dd/packages/ivi/src/vdom/implementation.ts#L1366 @@ -24,12 +69,12 @@ import { noop } from '../utils'; // How this implementation differs from others, is that it's working with data directly, // without maintaining nodes arrays, and uses dom props firstChild/lastChild/nextSibling // for markers moving. -export const reconcile = ( +export const reconcile = ( parent: Element, - renderedValues: any[], - data: any[], - createFn: (...args: T) => N, - updateFn: (node: N, ...args: T) => void = noop, + renderedData: T[], + data: T[], + createFn: (itemData: T) => N, + updateFn: (node: N, itemData: T) => void = noop, beforeNode?: Node, afterNode?: Node | null, ): void => { @@ -37,16 +82,14 @@ export const reconcile = ( if (data.length === 0) { if (beforeNode !== undefined || afterNode !== undefined) { let node = - beforeNode !== undefined ? beforeNode.nextSibling : parent.firstChild; - let tmp; + beforeNode === undefined ? parent.firstChild : beforeNode.nextSibling; + let tmp: ChildNode | null; if (afterNode === undefined) afterNode = null; while (node !== afterNode) { - // @ts-expect-error - FIXME:! - tmp = node.nextSibling; - // @ts-expect-error - FIXME:! - parent.removeChild(node); + tmp = node!.nextSibling; + node!.remove(); node = tmp; } } else { @@ -56,14 +99,16 @@ export const reconcile = ( } // Fast path for create - if (renderedValues.length === 0) { - let node; - const mode = afterNode !== undefined ? 1 : 0; + if (renderedData.length === 0) { + let node: Node; + const mode = afterNode === undefined ? 0 : 1; for (let i = 0, len = data.length; i < len; i++) { - // @ts-expect-error - FIXME:! node = createFn(data[i]); - // @ts-expect-error - FIXME:! - mode ? parent.insertBefore(node, afterNode) : parent.appendChild(node); + if (mode) { + parent.insertBefore(node, afterNode!); + } else { + parent.appendChild(node); + } } return; } @@ -71,81 +116,79 @@ export const reconcile = ( let prevStart = 0; let newStart = 0; let loop = true; - let prevEnd = renderedValues.length - 1; + let prevEnd = renderedData.length - 1; let newEnd = data.length - 1; - let a; - let b; - let prevStartNode = beforeNode ? beforeNode.nextSibling : parent.firstChild; + let a: T; + let b: T; + let prevStartNode: Node | null = beforeNode + ? beforeNode.nextSibling + : parent.firstChild; let newStartNode = prevStartNode; - let prevEndNode = afterNode ? afterNode.previousSibling : parent.lastChild; + let prevEndNode: Node | null = afterNode + ? afterNode.previousSibling + : parent.lastChild; fixes: while (loop) { loop = false; - let _node; + let tmpNode: ChildNode | null; // Skip prefix - (a = renderedValues[prevStart]), (b = data[newStart]); + a = renderedData[prevStart]; + b = data[newStart]; while (a === b) { - // @ts-expect-error - FIXME:! - updateFn(prevStartNode, b); + updateFn(prevStartNode as N, b); prevStart++; newStart++; - // @ts-expect-error - FIXME:! - newStartNode = prevStartNode = prevStartNode.nextSibling; + newStartNode = prevStartNode = prevStartNode!.nextSibling; if (prevEnd < prevStart || newEnd < newStart) break fixes; - a = renderedValues[prevStart]; + a = renderedData[prevStart]; b = data[newStart]; } // Skip suffix - (a = renderedValues[prevEnd]), (b = data[newEnd]); + a = renderedData[prevEnd]; + b = data[newEnd]; while (a === b) { - // @ts-expect-error - FIXME:! - updateFn(prevEndNode, b); + updateFn(prevEndNode as N, b); prevEnd--; newEnd--; afterNode = prevEndNode; - // @ts-expect-error - FIXME:! - prevEndNode = prevEndNode.previousSibling; + prevEndNode = prevEndNode!.previousSibling; if (prevEnd < prevStart || newEnd < newStart) break fixes; - a = renderedValues[prevEnd]; + a = renderedData[prevEnd]; b = data[newEnd]; } // Fast path to swap backward - (a = renderedValues[prevEnd]), (b = data[newStart]); + a = renderedData[prevEnd]; + b = data[newStart]; while (a === b) { loop = true; - // @ts-expect-error - FIXME:! - updateFn(prevEndNode, b); - // @ts-expect-error - FIXME:! - _node = prevEndNode.previousSibling; - // @ts-expect-error - FIXME:! - parent.insertBefore(prevEndNode, newStartNode); - prevEndNode = _node; + updateFn(prevEndNode as N, b); + tmpNode = prevEndNode!.previousSibling; + parent.insertBefore(prevEndNode!, newStartNode); + prevEndNode = tmpNode; newStart++; prevEnd--; if (prevEnd < prevStart || newEnd < newStart) break fixes; - a = renderedValues[prevEnd]; + a = renderedData[prevEnd]; b = data[newStart]; } // Fast path to swap forward - (a = renderedValues[prevStart]), (b = data[newEnd]); + a = renderedData[prevStart]; + b = data[newEnd]; while (a === b) { loop = true; - // @ts-expect-error - FIXME:! - updateFn(prevStartNode, b); - // @ts-expect-error - FIXME:! - _node = prevStartNode.nextSibling; - // @ts-expect-error - FIXME:! - parent.insertBefore(prevStartNode, afterNode); + updateFn(prevStartNode as N, b); + tmpNode = prevStartNode!.nextSibling; + parent.insertBefore(prevStartNode!, afterNode!); prevStart++; afterNode = prevStartNode; - prevStartNode = _node; + prevStartNode = tmpNode; newEnd--; if (prevEnd < prevStart || newEnd < newStart) break fixes; - a = renderedValues[prevStart]; + a = renderedData[prevStart]; b = data[newEnd]; } } @@ -153,16 +196,13 @@ export const reconcile = ( // Fast path for shrink if (newEnd < newStart) { if (prevStart <= prevEnd) { - let next; + let next: ChildNode | null; while (prevStart <= prevEnd) { if (prevEnd === 0) { - // @ts-expect-error - FIXME:! - parent.removeChild(prevEndNode); + (prevEndNode as ChildNode).remove(); } else { - // @ts-expect-error - FIXME:! - next = prevEndNode.previousSibling; - // @ts-expect-error - FIXME:! - parent.removeChild(prevEndNode); + next = prevEndNode!.previousSibling; + (prevEndNode as ChildNode).remove(); prevEndNode = next; } prevEnd--; @@ -174,13 +214,15 @@ export const reconcile = ( // Fast path for add if (prevEnd < prevStart) { if (newStart <= newEnd) { - let node; + let node: Node; const mode = afterNode ? 1 : 0; while (newStart <= newEnd) { - // @ts-expect-error - FIXME:! node = createFn(data[newStart]); - // @ts-expect-error - FIXME:! - mode ? parent.insertBefore(node, afterNode) : parent.appendChild(node); + if (mode) { + parent.insertBefore(node, afterNode!); + } else { + parent.appendChild(node); + } newStart++; } } @@ -188,7 +230,7 @@ export const reconcile = ( } // Positions for reusing nodes from current DOM state - const P = new Array(newEnd + 1 - newStart); + const P = new Array(newEnd + 1 - newStart); for (let i = newStart; i <= newEnd; i++) P[i] = -1; // Index to resolve position from current to new @@ -196,11 +238,11 @@ export const reconcile = ( for (let i = newStart; i <= newEnd; i++) I.set(data[i], i); let reusingNodes = newStart + data.length - 1 - newEnd; - const toRemove = []; + const toRemove: number[] = []; for (let i = prevStart; i <= prevEnd; i++) { - if (I.has(renderedValues[i])) { - P[I.get(renderedValues[i])] = i; + if (I.has(renderedData[i])) { + P[I.get(renderedData[i])] = i; reusingNodes++; } else { toRemove.push(i); @@ -211,16 +253,14 @@ export const reconcile = ( if (reusingNodes === 0) { if (beforeNode !== undefined || afterNode !== undefined) { let node = - beforeNode !== undefined ? beforeNode.nextSibling : parent.firstChild; - let tmp; + beforeNode === undefined ? parent.firstChild : beforeNode.nextSibling; + let tmp: ChildNode | null; if (afterNode === undefined) afterNode = null; while (node !== afterNode) { - // @ts-expect-error - FIXME:! - tmp = node.nextSibling; - // @ts-expect-error - FIXME:! - parent.removeChild(node); + tmp = node!.nextSibling; + node!.remove(); node = tmp; prevStart++; } @@ -228,13 +268,15 @@ export const reconcile = ( parent.textContent = ''; } - let node; + let node: Node; const mode = afterNode ? 1 : 0; for (let i = newStart; i <= newEnd; i++) { - // @ts-expect-error - FIXME:! node = createFn(data[i]); - // @ts-expect-error - FIXME:! - mode ? parent.insertBefore(node, afterNode) : parent.appendChild(node); + if (mode) { + parent.insertBefore(node, afterNode!); + } else { + parent.appendChild(node); + } } return; @@ -244,92 +286,33 @@ export const reconcile = ( const longestSeq = longestPositiveIncreasingSubsequence(P, newStart); // Collect nodes to work with them - const nodes = []; + const nodes: (Node | null)[] = []; let tmpC = prevStartNode; for (let i = prevStart; i <= prevEnd; i++) { nodes[i] = tmpC; - // @ts-expect-error - FIXME:! - tmpC = tmpC.nextSibling; + tmpC = tmpC!.nextSibling; } for (let i = 0; i < toRemove.length; i++) { - // @ts-expect-error - FIXME:! - parent.removeChild(nodes[toRemove[i]]); + (nodes[toRemove[i]] as ChildNode).remove(); } let lisIdx = longestSeq.length - 1; - let tmpD; + let tmpD: Node; for (let i = newEnd; i >= newStart; i--) { if (longestSeq[lisIdx] === i) { afterNode = nodes[P[longestSeq[lisIdx]]]; - // @ts-expect-error - FIXME:! - updateFn(afterNode, data[i]); + updateFn(afterNode as N, data[i]); lisIdx--; } else { if (P[i] === -1) { - // @ts-expect-error - FIXME:! tmpD = createFn(data[i]); } else { - tmpD = nodes[P[i]]; - // @ts-expect-error - FIXME:! - updateFn(tmpD, data[i]); + tmpD = nodes[P[i]]!; + updateFn(tmpD as N, data[i]); } - // @ts-expect-error - FIXME:! - parent.insertBefore(tmpD, afterNode); + parent.insertBefore(tmpD, afterNode!); afterNode = tmpD; } } }; - -// Picked from -// https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L368 - -// return an array of the indices of ns that comprise the longest increasing subsequence within ns -const longestPositiveIncreasingSubsequence = (ns: any[], newStart: number) => { - const seq = []; - const is = []; - let l = -1; - const pre = new Array(ns.length); - - for (var i = newStart, len = ns.length; i < len; i++) { - const n = ns[i]; - if (n < 0) continue; - const j = findGreatestIndexLEQ(seq, n); - if (j !== -1) pre[i] = is[j]; - if (j === l) { - l++; - seq[l] = n; - is[l] = i; - } else if (n < seq[j + 1]) { - seq[j + 1] = n; - is[j + 1] = i; - } - } - - for (i = is[l]; l >= 0; i = pre[i], l--) { - seq[l] = i; - } - - return seq; -}; - -const findGreatestIndexLEQ = (seq: any[], n: number) => { - // invariant: lo is guaranteed to be index of a value <= n, hi to be > - // therefore, they actually start out of range: (-1, last + 1) - let lo = -1; - let hi = seq.length; - - // fast path for simple increasing sequences - if (hi > 0 && seq[hi - 1] <= n) return hi - 1; - - while (hi - lo > 1) { - const mid = Math.floor((lo + hi) / 2); - if (seq[mid] > n) { - hi = mid; - } else { - lo = mid; - } - } - - return lo; -}; diff --git a/src/reconcile/reuse-nodes.ts b/src/reconcile/reuse-nodes.ts index e4fedee..0f68784 100644 --- a/src/reconcile/reuse-nodes.ts +++ b/src/reconcile/reuse-nodes.ts @@ -1,32 +1,26 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unused-expressions */ - import { noop } from '../utils'; -export const reconcile = ( +export const reconcile = ( parent: Element, - renderedValues: any[], - data: any[], - createFn: (...args: T) => N, - updateFn: (node: N, ...args: T) => void = noop, + renderedData: T[], + data: T[], + createFn: (itemData: T) => N, + updateFn: (node: N, itemData: T) => void = noop, beforeNode?: Node, afterNode?: Node | null, ): void => { - if (data.length === 0) { + const len = data.length; + if (len === 0) { if (beforeNode !== undefined || afterNode !== undefined) { let node = - beforeNode !== undefined ? beforeNode.nextSibling : parent.firstChild; - let tmp; + beforeNode === undefined ? parent.firstChild : beforeNode.nextSibling; + let tmp: ChildNode | null; if (afterNode === undefined) afterNode = null; while (node !== afterNode) { - // @ts-expect-error - FIXME:! - tmp = node.nextSibling; - // @ts-expect-error - FIXME:! - parent.removeChild(node); + tmp = node!.nextSibling; + node!.remove(); node = tmp; } } else { @@ -34,39 +28,38 @@ export const reconcile = ( } return; } - if (renderedValues.length > data.length) { - let i = renderedValues.length; - let tail = - // @ts-expect-error - FIXME:! - afterNode !== undefined ? afterNode.previousSibling : parent.lastChild; - let tmp; - while (i > data.length) { - // @ts-expect-error - FIXME:! - tmp = tail.previousSibling; - // @ts-expect-error - FIXME:! - parent.removeChild(tail); + if (renderedData.length > len) { + let index = renderedData.length; + let tail = afterNode == null ? parent.lastChild : afterNode.previousSibling; + let tmp: ChildNode | null; + while (index > len) { + tmp = tail!.previousSibling; + tail!.remove(); tail = tmp; - i--; + index--; } } - let _head = beforeNode ? beforeNode.nextSibling : parent.firstChild; - // @ts-expect-error - FIXME:! - if (_head === afterNode) _head = undefined; + let head: Node | null = beforeNode + ? beforeNode.nextSibling + : parent.firstChild; + if (head === afterNode) head = null; - const _mode = afterNode ? 1 : 0; - for (let i = 0, item, head = _head, mode = _mode; i < data.length; i++) { - item = data[i]; + const mode = afterNode ? 1 : 0; + let index = 0; + let item: T; + for (; index < len; index++) { + item = data[index]; if (head) { - // @ts-expect-error - FIXME:! - updateFn(head, item); + updateFn(head as N, item); } else { - // @ts-expect-error - FIXME:! head = createFn(item); - // @ts-expect-error - FIXME:! - mode ? parent.insertBefore(head, afterNode) : parent.appendChild(head); + if (mode) { + parent.insertBefore(head, afterNode!); + } else { + parent.appendChild(head); + } } - // @ts-expect-error - FIXME:! head = head.nextSibling; if (head === afterNode) head = null; } diff --git a/src/runtime.ts b/src/runtime.ts new file mode 100644 index 0000000..0210567 --- /dev/null +++ b/src/runtime.ts @@ -0,0 +1,45 @@ +import type { InferRefs, LowercaseKeys, Refs } from './types'; +import { create } from './utils'; + +const template = create('template'); +const treeWalker = document.createTreeWalker(template); + +/** + * Creates a DOM node from a compiled template. + * @param html - HTML string. + */ +export const h = (html: string): T => { + template.innerHTML = html; + return template.content.firstChild as T; +}; + +/** + * Collects node refs from a compiled template. + * @param root - Root node. + * @param k - Ref key names. + * @param d - Distances from previous ref node or root. + * @returns An object mapping ref nodes keyed by their ref name. Note that some + * browsers lowercase rendered HTML element attribute names so we lowercase the + * typed key names to bring awareness to this. + */ +/* @__NOINLINE__ */ +export const collect = >( + root: Node, + k: readonly string[], + d: readonly number[], +): LowercaseKeys => { + const walker = treeWalker; // local var is faster in some JS engines + const refs: Refs = {}; + const len = k.length; + let index = 0; + let distance: number; + walker.currentNode = root; + + for (; index < len; index++) { + distance = d[index]; + while (distance--) walker.nextNode(); + refs[k[index]] = walker.currentNode; + } + + return refs as LowercaseKeys; +}; diff --git a/src/store.ts b/src/store.ts index 16ac23e..5980c67 100644 --- a/src/store.ts +++ b/src/store.ts @@ -7,11 +7,13 @@ type Store = T & { }; /** - * Creates a proxied state object that triggers callback functions when its - * properties are set. + * Create a reactive data store. * - * @param initialState - An initial store state. The provided object should not - * have an `on` property because it is used internally. + * @param initialState - An initial store state object. It must not have an `on` + * property because that is used to register callback functions. + * + * @returns A proxied state object that triggers registered callback handler + * functions when its properties are set. */ export const store = >( initialState: T & { on?: never }, @@ -21,12 +23,13 @@ export const store = >( return new Proxy( // proxied state object { + // shallow copy to prevent mutation of the initial state object ...initialState, on(key, fn) { (handlers[key] ??= []).push(fn); return /** off */ () => { // eslint-disable-next-line no-bitwise - handlers[key]?.splice(handlers[key]!.indexOf(fn) >>> 0, 1); + handlers[key]?.splice(handlers[key].indexOf(fn) >>> 0, 1); }; }, }, diff --git a/src/types.ts b/src/types.ts index d2843fd..62bdece 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,18 +1,11 @@ -/** @internal */ -export interface Ref { - readonly i: number; - readonly ref: string; -} - export type Refs = Record; +export type InferRefs = { + [K in keyof T]: T[K] extends Node + ? T[K] + : /* `never` is more accurate but Node is useful for feedback */ Node; +}; + export type LowercaseKeys = { [K in keyof T as Lowercase]: T[K]; }; - -export interface S1Node extends Node, ChildNode { - _refs: Ref[]; - // Some browsers lowercase rendered HTMLElement attribute names so we - // lowercase the ref keys to bring awareness to this. - collect(node: Node): LowercaseKeys; -} diff --git a/src/utils.ts b/src/utils.ts index 792e881..87f2b6b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,21 +1,37 @@ export const noop = (): void => {}; // DOM utilities -export const createFragment = (): DocumentFragment => new DocumentFragment(); + +export const fragment = (): DocumentFragment => + document.createDocumentFragment(); +export const text = (str: string): Text => document.createTextNode(str); export const create = ( tagName: K, ): HTMLElementTagNameMap[K] => document.createElement(tagName); +export const clone = (node: T): T => node.cloneNode(true) as T; +/** Append a node to the end of the parent node. */ export const append = (node: T, parent: Node): T => parent.appendChild(node); +/** Prepend a node to the beginning of the parent node. */ export const prepend = (node: T, parent: Node): T => parent.insertBefore(node, parent.firstChild); +/** Insert a node after the target node. Target must have a parent node! */ +export const insert = (node: T, target: Node): T => + target.parentNode!.insertBefore(node, target.nextSibling); +/** Replace a target node with a new node. Target must have a parent node! */ +export const replace = (node: T, target: Node): T => + ( + // biome-ignore lint/style/noCommaOperator: save bytes on return statement + target.parentNode!.replaceChild(node, target), node // eslint-disable-line no-sequences + ); /** * Runs callback function when a specified node is removed from the DOM. * - * @remarks Use sparingly to minimize performance overhead. + * @remarks Somewhat computationally expensive, especially when there are many + * DOM mutations. Use sparingly to minimize performance overhead. */ -export const onNodeRemove = (node: Node, fn: () => void): void => { +export const onRemove = (node: Node, fn: () => void): void => { new MutationObserver((mutations, observer) => { for (const mutation of mutations) { for (const removedNode of mutation.removedNodes) { @@ -26,7 +42,7 @@ export const onNodeRemove = (node: Node, fn: () => void): void => { } } } - }).observe(document.documentElement, { + }).observe(document, { childList: true, subtree: true, }); diff --git a/test/TestComponent.ts b/test/TestComponent.ts new file mode 100644 index 0000000..78eaa79 --- /dev/null +++ b/test/TestComponent.ts @@ -0,0 +1,28 @@ +import { compile } from '../src/macro' with { type: 'macro' }; +import { collect, h } from '../src/runtime'; + +type TestComponent = HTMLDivElement; + +interface TestProps { + text: string; +} + +interface Refs { + t: Text; +} + +const meta = compile(` +
+ @t +
+`); +const view = h(meta.html); + +export function Test(props: TestProps): TestComponent { + const root = view; + const refs = collect(root, meta.k, meta.d); + + refs.t.nodeValue = props.text; + + return root; +} diff --git a/test/TestComponent_browser.ts b/test/TestComponent_browser.ts new file mode 100644 index 0000000..1c462e1 --- /dev/null +++ b/test/TestComponent_browser.ts @@ -0,0 +1,26 @@ +import { collect, h } from '../src/browser/runtime'; + +type TestComponent = HTMLDivElement; + +interface TestProps { + text: string; +} + +interface Refs { + t: Text; +} + +const view = h(` +
+ @t +
+`); + +export function Test(props: TestProps): TestComponent { + const root = view; + const refs = collect(root, view); + + refs.t.nodeValue = props.text; + + return root; +} diff --git a/test/compile.test.ts b/test/compile.test.ts deleted file mode 100644 index 541d2ba..0000000 --- a/test/compile.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import * as assert from 'uvu/assert'; -import { h, html } from '../src/compile'; -import { cleanup, describe, render } from './utils'; - -describe('h', (test) => { - test.after.each(cleanup); - - test('renders basic template', () => { - const view = h(` -
    -
  • A
  • -
  • B
  • -
  • C
  • -
- `); - const rendered = render(view); - assert.fixture(rendered.container.innerHTML, '
    \n
  • A
  • \n
  • B
  • \n
  • C
  • \n
'); - }); - - test('renders SVG template', () => { - const view = h(` - - - - `); - const rendered = render(view); - assert.fixture( - rendered.container.innerHTML, - '\n\n', - ); - assert.instance(view, window.SVGElement); - }); - - test('returns root element', () => { - const view = h(` -
    -
  • A
  • -
  • B
  • -
  • C
  • -
- `); - assert.is(view.nodeName, 'UL'); - assert.is((view as unknown as HTMLUListElement).id, 'root'); - }); - - test('removes refs in template from output DOM', () => { - const view = h(` -
    -
  • A
  • -
  • B
  • -
- `); - const rendered = render(view); - assert.fixture(rendered.container.innerHTML, '
    \n
  • A
  • \n
  • B
  • \n
'); - }); - - test('collects all refs', () => { - const view = h(` -
-
- -
-
-

Test

-

This is a test.

-
    -
  1. One
  2. -
  3. Two
  4. -
-
- - - -
-
-
- #s -
-
- `); - const nodes = view.collect(view); - assert.instance(nodes.a, window.HTMLDivElement); - assert.is(nodes.a.nodeName, 'DIV'); - assert.instance(nodes.b, window.HTMLElement); - assert.is(nodes.b.nodeName, 'HEADER'); - assert.instance(nodes.c, window.HTMLElement); - assert.is(nodes.c.nodeName, 'NAV'); - assert.instance(nodes.d, window.HTMLAnchorElement); - assert.is(nodes.d.nodeName, 'A'); - assert.instance(nodes.e, window.HTMLAnchorElement); - assert.is(nodes.e.nodeName, 'A'); - assert.instance(nodes.f, window.HTMLElement); - assert.is(nodes.f.nodeName, 'MAIN'); - assert.instance(nodes.g, window.HTMLHeadingElement); - assert.is(nodes.g.nodeName, 'H1'); - assert.instance(nodes.h, window.HTMLParagraphElement); - assert.is(nodes.h.nodeName, 'P'); - assert.instance(nodes.i, window.HTMLElement); - assert.is(nodes.i.nodeName, 'B'); - assert.instance(nodes.j, window.HTMLAnchorElement); - assert.is(nodes.j.nodeName, 'A'); - assert.instance(nodes.k, window.HTMLOListElement); - assert.is(nodes.k.nodeName, 'OL'); - assert.instance(nodes.l, window.HTMLLIElement); - assert.is(nodes.l.nodeName, 'LI'); - assert.instance(nodes.m, window.HTMLLIElement); - assert.is(nodes.m.nodeName, 'LI'); - assert.instance(nodes.n, window.HTMLFormElement); - assert.is(nodes.n.nodeName, 'FORM'); - assert.instance(nodes.o, window.HTMLInputElement); - assert.is(nodes.o.nodeName, 'INPUT'); - assert.instance(nodes.p, window.HTMLTextAreaElement); - assert.is(nodes.p.nodeName, 'TEXTAREA'); - assert.instance(nodes.q, window.HTMLButtonElement); - assert.is(nodes.q.nodeName, 'BUTTON'); - assert.instance(nodes.r, window.HTMLElement); - assert.is(nodes.r.nodeName, 'FOOTER'); - assert.instance(nodes.s, window.Text); - assert.is(nodes.s.nodeName, '#text'); - }); - - test('collects ref at start of element attributes', () => { - const view = h(` -
- -
- `); - const nodes = view.collect(view); - assert.instance(nodes.search, window.HTMLInputElement); - }); - - test('collects ref at end of element attributes', () => { - const view = h(` -
- -
- `); - const nodes = view.collect(view); - assert.instance(nodes.search, window.HTMLInputElement); - }); - - test('collects ref in middle of element attributes', () => { - const view = h(` -
- -
- `); - const nodes = view.collect(view); - assert.instance(nodes.search, window.HTMLInputElement); - }); - - test('does not format template when NODE_ENV=production', () => { - const oldNodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = 'production'; - const view = h(`
    -
  • A
  • -
  • B
  • -
  • C
  • -
`); - const rendered = render(view); - assert.fixture( - rendered.container.innerHTML, - '
    \n
  • A
  • \n
  • B
  • \n
  • C
  • \n
', - ); - process.env.NODE_ENV = oldNodeEnv; - }); - - test('omits leading space to correctly render HTMLDivElement when not NODE_ENV=production', () => { - assert.is.not(process.env.NODE_ENV, 'production'); - const view = h('
'); - const rendered = render(view); - assert.instance(rendered.container.firstChild, window.HTMLDivElement); - }); - - test('does not omit leading space and renders Text node when NODE_ENV=production', () => { - const oldNodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = 'production'; - const view = h('
'); - const rendered = render(view); - assert.instance(rendered.container.firstChild, window.Text); - process.env.NODE_ENV = oldNodeEnv; - }); -}); - -describe('html', (test) => { - test.after.each(cleanup); - - test('renders basic template', () => { - const view = html` -
    -
  • A
  • -
  • B
  • -
  • C
  • -
- `; - const rendered = render(view); - assert.fixture(rendered.container.innerHTML, '
    \n
  • A
  • \n
  • B
  • \n
  • C
  • \n
'); - }); - - test('does not format template when NODE_ENV=production', () => { - const oldNodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = 'production'; - const view = html`
    -
  • A
  • -
  • B
  • -
  • C
  • -
`; - const rendered = render(view); - assert.fixture( - rendered.container.innerHTML, - '
    \n
  • A
  • \n
  • B
  • \n
  • C
  • \n
', - ); - process.env.NODE_ENV = oldNodeEnv; - }); - - test('omits leading space to correctly render HTMLDivElement when not NODE_ENV=production', () => { - assert.is.not(process.env.NODE_ENV, 'production'); - const view = html`
`; - const rendered = render(view); - assert.instance(rendered.container.firstChild, window.HTMLDivElement); - }); - - test('does not omit leading space and renders Text node when NODE_ENV=production', () => { - const oldNodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = 'production'; - const view = html`
`; - const rendered = render(view); - assert.instance(rendered.container.firstChild, window.Text); - process.env.NODE_ENV = oldNodeEnv; - }); -}); diff --git a/test/e2e/browser.spec.ts b/test/e2e/browser.spec.ts new file mode 100644 index 0000000..cdf7b7d --- /dev/null +++ b/test/e2e/browser.spec.ts @@ -0,0 +1,2 @@ +// TODO +console.warn('Not implemented'); diff --git a/test/e2e/index.spec.ts b/test/e2e/index.spec.ts new file mode 100644 index 0000000..7d8e2c3 --- /dev/null +++ b/test/e2e/index.spec.ts @@ -0,0 +1,5 @@ +import { expect, test } from '@playwright/test'; + +test('placeholder', () => { + expect(1 + 2).toBe(3); +}); diff --git a/test/e2e/runtime.spec.ts b/test/e2e/runtime.spec.ts new file mode 100644 index 0000000..cdf7b7d --- /dev/null +++ b/test/e2e/runtime.spec.ts @@ -0,0 +1,2 @@ +// TODO +console.warn('Not implemented'); diff --git a/test/events.test.ts b/test/events.test.ts deleted file mode 100644 index 33054a5..0000000 --- a/test/events.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { spy } from 'nanospy'; -import * as assert from 'uvu/assert'; -import { deleteSyntheticEvent, setupSyntheticEvent } from '../src/events'; -import { cleanup, describe, render } from './utils'; - -declare global { - interface HTMLElement { - /** `stage1` synthetic click event handler. */ - __click?(event: MouseEvent): void; - } -} - -describe('setupSyntheticEvent', (test) => { - test.after.each(cleanup); - - test('is a function', () => { - assert.type(setupSyntheticEvent, 'function'); - }); - - test('expects 1 parameter', () => { - assert.is(setupSyntheticEvent.length, 1); - }); - - test('returns undefined', () => { - assert.is(setupSyntheticEvent('abort'), undefined); - }); - - test('calls synthetic click handler on click event', () => { - const button = document.createElement('button'); - const callback = spy(); - button.__click = callback; - render(button); - setupSyntheticEvent('click'); - button.click(); - button.click(); - button.click(); - assert.is(callback.callCount, 3); - deleteSyntheticEvent('click'); - }); - - test('propagates click event from deeply nested element', () => { - const button = document.createElement('button'); - const div = document.createElement('div'); - const span = document.createElement('span'); - const img = document.createElement('img'); - const callback = spy(); - button.__click = callback; - button.appendChild(div); - div.appendChild(span); - span.appendChild(img); - render(button); - setupSyntheticEvent('click'); - img.click(); - img.click(); - img.click(); - assert.is(callback.callCount, 3); - deleteSyntheticEvent('click'); - }); - - test('propagates up to document body', () => { - const button = document.createElement('button'); - const callback = spy(); - document.body.__click = callback; - render(button); - setupSyntheticEvent('click'); - button.click(); - assert.is(callback.callCount, 1); - deleteSyntheticEvent('click'); - delete document.body.__click; - }); - - test('no longer propagates click event once handled', () => { - const div1 = document.createElement('div'); - const div2 = document.createElement('div'); - const callback = spy(); - div1.__click = callback; - div2.__click = callback; - div1.appendChild(div2); - render(div1); - setupSyntheticEvent('click'); - div2.click(); - assert.is(callback.callCount, 1); // only called once - deleteSyntheticEvent('click'); - }); - - test('does not call handler if synthetic event is not setup', () => { - const button = document.createElement('button'); - const callback = spy(); - button.__click = callback; - render(button); - button.click(); - assert.is(callback.callCount, 0); - }); - - test('does not call handler if event originates from another DOM tree branch', () => { - const div = document.createElement('div'); - const button1 = document.createElement('button'); - const button2 = document.createElement('button'); - const callback1 = spy(); - const callback2 = spy(); - button1.__click = callback1; - button2.__click = callback2; - render(div); - div.appendChild(button1); - div.appendChild(button2); - setupSyntheticEvent('click'); - button1.click(); - assert.is(callback1.callCount, 1); - assert.is(callback2.callCount, 0); - deleteSyntheticEvent('click'); - }); - - test('only registers synthetic click handler once', () => { - const button = document.createElement('button'); - const callback = spy(); - button.__click = callback; - render(button); - setupSyntheticEvent('click'); - setupSyntheticEvent('click'); - button.click(); - assert.is(callback.callCount, 1); - deleteSyntheticEvent('click'); - }); -}); - -describe('deleteSyntheticEvent', (test) => { - test('is a function', () => { - assert.type(deleteSyntheticEvent, 'function'); - }); - - test('expects 1 parameter', () => { - assert.is(deleteSyntheticEvent.length, 1); - }); - - test('returns undefined', () => { - assert.is(deleteSyntheticEvent('abort'), undefined); - }); - - test('does not call synthetic click handler after delete', () => { - const button = document.createElement('button'); - const callback = spy(); - button.__click = callback; - render(button); - setupSyntheticEvent('click'); - button.click(); - assert.is(callback.callCount, 1); - deleteSyntheticEvent('click'); - button.click(); - button.click(); - button.click(); - assert.is(callback.callCount, 1); // still only one call - cleanup(); - }); -}); diff --git a/test/exports.test.ts b/test/exports.test.ts deleted file mode 100644 index d3f8b56..0000000 --- a/test/exports.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-disable guard-for-in */ - -import * as assert from 'uvu/assert'; -import * as indexExports from '../src/index'; -import * as keyedExports from '../src/reconcile/keyed'; -import * as nonKeyedExports from '../src/reconcile/non-keyed'; -import * as reuseNodesExports from '../src/reconcile/reuse-nodes'; -import * as storeExports from '../src/store'; -import { describe } from './utils'; - -describe('index', (test) => { - const PUBLIC_EXPORTS = [ - ['h', 'Function'], - ['html', 'Function'], - ['setupSyntheticEvent', 'Function'], - ['deleteSyntheticEvent', 'Function'], - ['noop', 'Function'], - ['createFragment', 'Function'], - ['create', 'Function'], - ['append', 'Function'], - ['prepend', 'Function'], - ['onNodeRemove', 'Function'], - ] as const; - - for (const [name, type] of PUBLIC_EXPORTS) { - test(`exports public "${name}" ${type}`, () => { - assert.ok(Object.hasOwnProperty.call(indexExports, name)); - assert.is(Object.prototype.toString.call(indexExports[name]), `[object ${type}]`); - }); - } - - test('does not export any private internals', () => { - const publicExportNames = PUBLIC_EXPORTS.map((x) => x[0]); - assert.is(publicExportNames.length, Object.keys(indexExports).length); - for (const name in indexExports) { - assert.ok((publicExportNames as string[]).includes(name)); - } - }); - - test('has no default export', () => { - assert.not.ok(Object.hasOwnProperty.call(indexExports, 'default')); - }); -}); - -describe('store', (test) => { - const PUBLIC_EXPORTS = [['store', 'Function']] as const; - - for (const [name, type] of PUBLIC_EXPORTS) { - test(`exports public "${name}" ${type}`, () => { - assert.ok(Object.hasOwnProperty.call(storeExports, name)); - assert.is(Object.prototype.toString.call(storeExports[name]), `[object ${type}]`); - }); - } - - test('does not export any private internals', () => { - const publicExportNames = PUBLIC_EXPORTS.map((x) => x[0]); - assert.is(publicExportNames.length, Object.keys(storeExports).length); - for (const name in storeExports) { - assert.ok((publicExportNames as string[]).includes(name)); - } - }); - - test('has no default export', () => { - assert.not.ok(Object.hasOwnProperty.call(storeExports, 'default')); - }); -}); - -const RECONSILERS = [ - ['keyed', keyedExports], - ['non-keyed', nonKeyedExports], - ['reuse-nodes', reuseNodesExports], -] as const; - -for (const [reconsiler, exports] of RECONSILERS) { - describe(`reconcile/${reconsiler}`, (test) => { - test('exports public "reconcile" Function', () => { - assert.ok(Object.hasOwnProperty.call(exports, 'reconcile')); - assert.is(Object.prototype.toString.call(exports.reconcile), '[object Function]'); - }); - - test('does not export any private internals', () => { - const publicExportNames = ['reconcile']; - assert.is(publicExportNames.length, Object.keys(exports).length); - for (const name in exports) { - assert.ok(publicExportNames.includes(name)); - } - }); - - test('has no default export', () => { - assert.not.ok(Object.hasOwnProperty.call(exports, 'default')); - }); - }); -} diff --git a/test/setup.ts b/test/setup.ts index bddfaa6..41a31c3 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,4 +1,23 @@ -import { setupDOM, setupMocks } from './utils'; +import '@maxmilton/test-utils/extend'; + +import { setupDOM } from '@maxmilton/test-utils/dom'; + +const noop = () => {}; + +function setupMocks(): void { + // @ts-expect-error - noop stub + global.performance.mark = noop; + // @ts-expect-error - noop stub + global.performance.measure = noop; + + // TODO: Remove once happy-dom supports `HTMLLIElement` again. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (window.HTMLLIElement) { + throw new TypeError('HTMLLIElement already defined'); + } + // @ts-expect-error - mock element + window.HTMLLIElement = window.HTMLElement; +} setupDOM(); setupMocks(); diff --git a/test/tsconfig.json b/test/tsconfig.json deleted file mode 100644 index ac9be8b..0000000 --- a/test/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "rootDir": "..", - "noEmit": true - }, - "include": ["../**/*", "../.*.cjs", "../*.mjs"], - "exclude": ["../coverage", "../dist", "../node_modules"] -} diff --git a/test/unit/browser-runtime.test.ts b/test/unit/browser-runtime.test.ts new file mode 100644 index 0000000..0880600 --- /dev/null +++ b/test/unit/browser-runtime.test.ts @@ -0,0 +1,254 @@ +// XXX: This file has the same tests as test/unit/runtime.test.ts, keep them in sync. + +import { afterEach, describe, expect, test } from 'bun:test'; +import { cleanup, render } from '@maxmilton/test-utils/dom'; +import { collect, h, html } from '../../src/browser/runtime'; + +describe('h', () => { + test('is a function', () => { + expect.assertions(2); + expect(h).toBeFunction(); + expect(h).not.toBeClass(); + }); + + test('expects 1 parameters', () => { + expect.assertions(1); + expect(h).toHaveParameters(1, 0); + }); + + describe('render', () => { + afterEach(cleanup); + + test('renders basic template', () => { + expect.assertions(1); + const view = h(` +
    +
  • A
  • +
  • B
  • +
  • C
  • +
+ `); + const rendered = render(view); + expect(rendered.container.innerHTML).toBe('
  • A
  • B
  • C
'); + }); + + test('renders basic template with messy whitespace', () => { + expect.assertions(1); + const view = h(` +
    +
  • A
  • +
  • + B
  • +
  • C +
  • +
+ `); + const rendered = render(view); + expect(rendered.container.innerHTML).toBe('
  • A
  • B
  • C
'); + }); + + test('renders SVG template', () => { + expect.assertions(2); + const view = h(` + + + + `); + const rendered = render(view); + expect(view).toBeInstanceOf(window.SVGSVGElement); + expect(rendered.container.innerHTML).toBe( + '', + ); + }); + + test('returns root element', () => { + expect.assertions(3); + const view = h(` +
    +
  • A
  • +
  • B
  • +
  • C
  • +
+ `); + const rendered = render(view); + expect(view).toBeInstanceOf(window.HTMLUListElement); + expect(view.id).toBe('root'); + expect(rendered.container.firstChild).toBe(view); + }); + + test('removes refs in template from output DOM', () => { + expect.assertions(1); + const view = h(` +
    +
  • A
  • +
  • B
  • +
+ `); + const rendered = render(view); + expect(rendered.container.innerHTML).toBe('
  • A
  • B
'); + }); + + // NOTE: This is not supported by the current implementation of the h() + // function because it would be too slow. + test.skip('does not minify in whitespace-sensitive blocks', () => {}); + }); +}); + +describe('html', () => { + test('is a function', () => { + expect.assertions(2); + expect(html).toBeFunction(); + expect(html).not.toBeClass(); + }); + + test('expects 2 parameters (1 optional)', () => { + expect.assertions(1); + expect(html).toHaveParameters(1, 1); + }); + + describe('render', () => { + afterEach(cleanup); + + test('renders basic template', () => { + expect.assertions(1); + const view = html` +
    +
  • A
  • +
  • B
  • +
  • C
  • +
+ `; + const rendered = render(view); + expect(rendered.container.innerHTML).toBe('
  • A
  • B
  • C
'); + }); + }); +}); + +describe('collect', () => { + test('is a function', () => { + expect.assertions(2); + expect(collect).toBeFunction(); + expect(collect).not.toBeClass(); + }); + + test('expects 2 parameters', () => { + expect.assertions(1); + expect(collect).toHaveParameters(2, 0); + }); + + test('collects all refs', () => { + expect.assertions(39); + const view = h(` +
+
+ +
+
+

Test

+

This is a test.

+
    +
  1. One
  2. +
  3. Two
  4. +
+
+ + + +
+
+
+ @s +
+
+ `); + const refs = collect(view, view); + expect(refs.a.nodeName).toEqual('DIV'); + expect(refs.a).toBeInstanceOf(window.HTMLDivElement); + expect(refs.b.nodeName).toEqual('HEADER'); + expect(refs.b).toBeInstanceOf(window.HTMLElement); + expect(refs.c.nodeName).toEqual('NAV'); + expect(refs.c).toBeInstanceOf(window.HTMLElement); + expect(refs.d.nodeName).toEqual('A'); + expect(refs.d).toBeInstanceOf(window.HTMLAnchorElement); + expect(refs.e.nodeName).toEqual('A'); + expect(refs.e).toBeInstanceOf(window.HTMLAnchorElement); + expect(refs.f.nodeName).toEqual('MAIN'); + expect(refs.f).toBeInstanceOf(window.HTMLDivElement); + expect(refs.g.nodeName).toEqual('H1'); + expect(refs.g).toBeInstanceOf(window.HTMLHeadingElement); + expect(refs.h.nodeName).toEqual('P'); + expect(refs.h).toBeInstanceOf(window.HTMLParagraphElement); + expect(refs.i.nodeName).toEqual('B'); + expect(refs.i).toBeInstanceOf(window.HTMLElement); + expect(refs.j.nodeName).toEqual('A'); + expect(refs.j).toBeInstanceOf(window.HTMLAnchorElement); + expect(refs.k.nodeName).toEqual('OL'); + expect(refs.k).toBeInstanceOf(window.HTMLOListElement); + expect(refs.l.nodeName).toEqual('LI'); + expect(refs.l).toBeInstanceOf(window.HTMLLIElement); + expect(refs.m.nodeName).toEqual('LI'); + expect(refs.m).toBeInstanceOf(window.HTMLLIElement); + expect(refs.n.nodeName).toEqual('FORM'); + expect(refs.n).toBeInstanceOf(window.HTMLFormElement); + expect(refs.o.nodeName).toEqual('INPUT'); + expect(refs.o).toBeInstanceOf(window.HTMLInputElement); + expect(refs.p.nodeName).toEqual('TEXTAREA'); + expect(refs.p).toBeInstanceOf(window.HTMLTextAreaElement); + expect(refs.q.nodeName).toEqual('BUTTON'); + expect(refs.q).toBeInstanceOf(window.HTMLButtonElement); + expect(refs.r.nodeName).toEqual('FOOTER'); + expect(refs.r).toBeInstanceOf(window.HTMLElement); + expect(refs.s.nodeName).toEqual('#text'); + expect(refs.s).toBeInstanceOf(window.Text); + expect(Object.keys(refs)).toHaveLength(19); + }); + + test('collects ref at start of element attributes', () => { + expect.assertions(4); + const view = h(` +
+ +
+ `); + const refs = collect<{ search: HTMLInputElement }>(view, view); + expect(refs.search).toBeInstanceOf(window.HTMLInputElement); + expect(refs.search.id).toBe('search'); + expect(refs.search.name).toBe('q'); + expect(Object.keys(refs)).toHaveLength(1); + }); + + test('collects ref at end of element attributes', () => { + expect.assertions(4); + const view = h(` +
+ +
+ `); + const refs = collect<{ search: HTMLInputElement }>(view, view); + expect(refs.search).toBeInstanceOf(window.HTMLInputElement); + expect(refs.search.id).toBe('search'); + expect(refs.search.name).toBe('q'); + expect(Object.keys(refs)).toHaveLength(1); + }); + + test('collects ref in middle of element attributes', () => { + expect.assertions(4); + const view = h(` +
+ +
+ `); + const refs = collect<{ search: HTMLInputElement }>(view, view); + expect(refs.search).toBeInstanceOf(window.HTMLInputElement); + expect(refs.search.id).toBe('search'); + expect(refs.search.name).toBe('q'); + expect(Object.keys(refs)).toHaveLength(1); + }); + + // NOTE: The regular mode h() function does not support options like the + // runtime mode compile() macro does. So there's no need to test them here. +}); diff --git a/test/unit/events.test.ts b/test/unit/events.test.ts new file mode 100644 index 0000000..8a058ca --- /dev/null +++ b/test/unit/events.test.ts @@ -0,0 +1,178 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test'; +import { cleanup, render } from '@maxmilton/test-utils/dom'; +import { deleteSyntheticEvent, setupSyntheticEvent } from '../../src/events'; + +declare global { + interface HTMLElement { + /** `stage1` synthetic click event handler. */ + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + __click?(event: MouseEvent): void | false | Promise; + } +} + +describe('setupSyntheticEvent', () => { + test('is a function', () => { + expect.assertions(2); + expect(setupSyntheticEvent).toBeFunction(); + expect(setupSyntheticEvent).not.toBeClass(); + }); + + test('expects 1 parameter', () => { + expect.assertions(1); + expect(setupSyntheticEvent).toHaveParameters(1, 0); + }); + + test('returns undefined', () => { + expect.assertions(1); + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression + expect(setupSyntheticEvent('abort')).toBeUndefined(); + }); + + describe('rendered', () => { + afterEach(cleanup); + + test('calls synthetic click handler on click event', () => { + expect.assertions(1); + const button = document.createElement('button'); + const callback = mock(() => {}); + button.__click = callback; + render(button); + setupSyntheticEvent('click'); + button.click(); + button.click(); + button.click(); + expect(callback).toHaveBeenCalledTimes(3); + deleteSyntheticEvent('click'); + }); + + test('propagates click event from deeply nested element', () => { + expect.assertions(1); + const button = document.createElement('button'); + const div = document.createElement('div'); + const span = document.createElement('span'); + const img = document.createElement('img'); + const callback = mock(() => {}); + button.__click = callback; + button.appendChild(div); + div.appendChild(span); + span.appendChild(img); + render(button); + setupSyntheticEvent('click'); + img.click(); + img.click(); + img.click(); + expect(callback).toHaveBeenCalledTimes(3); + deleteSyntheticEvent('click'); + }); + + test('propagates up to document body', () => { + expect.assertions(1); + const button = document.createElement('button'); + const callback = mock(() => {}); + document.body.__click = callback; + render(button); + setupSyntheticEvent('click'); + button.click(); + expect(callback).toHaveBeenCalledTimes(1); + deleteSyntheticEvent('click'); + // biome-ignore lint/performance/noDelete: cleanup + delete document.body.__click; + }); + + test('no longer propagates click event once handled', () => { + expect.assertions(1); + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const callback = mock(() => {}); + div1.__click = callback; + div2.__click = callback; + div1.appendChild(div2); + render(div1); + setupSyntheticEvent('click'); + div2.click(); + expect(callback).toHaveBeenCalledTimes(1); // only called once + deleteSyntheticEvent('click'); + }); + + test('does not call handler if synthetic event is not setup', () => { + expect.assertions(1); + const button = document.createElement('button'); + const callback = mock(() => {}); + button.__click = callback; + render(button); + button.click(); + expect(callback).not.toHaveBeenCalled(); + }); + + test('does not call handler if event originates from another DOM tree branch', () => { + expect.assertions(2); + const div = document.createElement('div'); + const button1 = document.createElement('button'); + const button2 = document.createElement('button'); + const callback1 = mock(() => {}); + const callback2 = mock(() => {}); + button1.__click = callback1; + button2.__click = callback2; + render(div); + div.appendChild(button1); + div.appendChild(button2); + setupSyntheticEvent('click'); + button1.click(); + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback2).not.toHaveBeenCalled(); + deleteSyntheticEvent('click'); + }); + + test('only registers synthetic click handler once', () => { + expect.assertions(1); + const button = document.createElement('button'); + const callback = mock(() => {}); + button.__click = callback; + render(button); + setupSyntheticEvent('click'); + setupSyntheticEvent('click'); + button.click(); + expect(callback).toHaveBeenCalledTimes(1); + deleteSyntheticEvent('click'); + }); + }); +}); + +describe('deleteSyntheticEvent', () => { + test('is a function', () => { + expect.assertions(2); + expect(deleteSyntheticEvent).toBeFunction(); + expect(deleteSyntheticEvent).not.toBeClass(); + }); + + test('expects 1 parameter', () => { + expect.assertions(1); + expect(setupSyntheticEvent).toHaveParameters(1, 0); + }); + + test('returns undefined', () => { + expect.assertions(1); + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression + expect(deleteSyntheticEvent('abort')).toBeUndefined(); + }); + + describe('rendered', () => { + afterEach(cleanup); + + test('does not call synthetic click handler after delete', () => { + expect.assertions(2); + const button = document.createElement('button'); + const callback = mock(() => {}); + button.__click = callback; + render(button); + setupSyntheticEvent('click'); + button.click(); + expect(callback).toHaveBeenCalledTimes(1); + deleteSyntheticEvent('click'); + button.click(); + button.click(); + button.click(); + expect(callback).toHaveBeenCalledTimes(1); // still only one call + }); + }); +}); diff --git a/test/unit/exports.test.ts b/test/unit/exports.test.ts new file mode 100644 index 0000000..a03cba5 --- /dev/null +++ b/test/unit/exports.test.ts @@ -0,0 +1,150 @@ +/* eslint-disable guard-for-in */ + +import { describe, expect, test } from 'bun:test'; +import * as browserExports from '../../src/browser/index'; +import * as indexExports from '../../src/index'; +import * as macroExports from '../../src/macro'; +import * as keyedExports from '../../src/reconcile/keyed'; +import * as nonKeyedExports from '../../src/reconcile/non-keyed'; +import * as reuseNodesExports from '../../src/reconcile/reuse-nodes'; + +describe('browser', () => { + const publicExports = [ + ['h', Function], + ['html', Function], + ['collect', Function], + ['setupSyntheticEvent', Function], + ['deleteSyntheticEvent', Function], + ['noop', Function], + ['fragment', Function], + ['text', Function], + ['create', Function], + ['clone', Function], + ['append', Function], + ['prepend', Function], + ['insert', Function], + ['replace', Function], + ['onRemove', Function], + ['store', Function], + ] as const; + + for (const [name, type] of publicExports) { + test(`exports public "${name}" ${type.name}`, () => { + expect.assertions(2); + expect(browserExports).toHaveProperty(name); + expect(browserExports[name]).toBeInstanceOf(type); + }); + } + + test('does not export any private internals', () => { + expect.assertions(publicExports.length + 1); + const publicExportNames = publicExports.map((x) => x[0]); + for (const name in browserExports) { + expect(publicExportNames).toContain(name); + } + expect(publicExportNames).toHaveLength(Object.keys(browserExports).length); + }); + + test('has no default export', () => { + expect.assertions(1); + expect(browserExports).not.toHaveProperty('default'); + }); +}); + +describe('index', () => { + const publicExports = [ + ['h', Function], + ['collect', Function], + ['setupSyntheticEvent', Function], + ['deleteSyntheticEvent', Function], + ['noop', Function], + ['fragment', Function], + ['text', Function], + ['clone', Function], + ['create', Function], + ['append', Function], + ['prepend', Function], + ['insert', Function], + ['replace', Function], + ['onRemove', Function], + ['store', Function], + ] as const; + + for (const [name, type] of publicExports) { + test(`exports public "${name}" ${type.name}`, () => { + expect.assertions(2); + expect(indexExports).toHaveProperty(name); + expect(indexExports[name]).toBeInstanceOf(type); + }); + } + + test('does not export any private internals', () => { + expect.assertions(publicExports.length + 1); + const publicExportNames = publicExports.map((x) => x[0]); + for (const name in indexExports) { + expect(publicExportNames).toContain(name); + } + expect(publicExportNames).toHaveLength(Object.keys(indexExports).length); + }); + + test('has no default export', () => { + expect.assertions(1); + expect(indexExports).not.toHaveProperty('default'); + }); +}); + +describe('macro', () => { + const publicExports = [['compile', Function]] as const; + + for (const [name, type] of publicExports) { + test(`exports public "${name}" ${type.name}`, () => { + expect.assertions(2); + expect(macroExports).toHaveProperty(name); + expect(macroExports[name]).toBeInstanceOf(type); + }); + } + + test('does not export any private internals', () => { + expect.assertions(publicExports.length + 1); + const publicExportNames = publicExports.map((x) => x[0]); + for (const name in macroExports) { + expect(publicExportNames).toContain(name); + } + expect(publicExportNames).toHaveLength(Object.keys(macroExports).length); + }); + + test('has no default export', () => { + expect.assertions(1); + expect(macroExports).not.toHaveProperty('default'); + }); +}); + +const reconsilers = [ + ['keyed', keyedExports], + ['non-keyed', nonKeyedExports], + ['reuse-nodes', reuseNodesExports], +] as const; + +for (const [reconsiler, exports] of reconsilers) { + describe(`reconcile/${reconsiler}`, () => { + test('exports public "reconcile" Function', () => { + expect.assertions(2); + expect(exports).toHaveProperty('reconcile'); + expect(exports.reconcile).toBeInstanceOf(Function); + }); + + test('does not export any private internals', () => { + expect.assertions(2); + const publicExportNames = ['reconcile']; + for (const name in exports) { + expect(publicExportNames).toContain(name); + } + expect(publicExportNames).toHaveLength(Object.keys(exports).length); + }); + + test('has no default export', () => { + expect.assertions(1); + expect(exports).not.toHaveProperty('default'); + }); + }); +} diff --git a/test/unit/macro.test.ts b/test/unit/macro.test.ts new file mode 100644 index 0000000..89aaa29 --- /dev/null +++ b/test/unit/macro.test.ts @@ -0,0 +1,299 @@ +// XXX: This file has the same tests as test/unit/compile.test.ts, keep them in sync. + +import { describe, expect, spyOn, test } from 'bun:test'; +import { compile } from '../../src/macro' with { type: 'macro' }; +import { compile as compileNoMacro } from '../../src/macro'; + +describe('compile', () => { + // FIXME: Test for each of the compile macro options; keepComments, keepSpace + // ↳ When keepComments, check refs metadata calculations are still correct. + // ↳ Currently blocked by bun bug; https://github.com/oven-sh/bun/issues/3832 + + test('returns an object', () => { + expect.assertions(1); + const meta = compile('
'); + expect(meta).toBePlainObject(); + }); + test('returns html property with string value', () => { + expect.assertions(2); + const meta = compile('
'); + expect(meta).toHaveProperty('html'); + expect(typeof meta.html).toBe('string'); + }); + test('returns k property with array value', () => { + expect.assertions(2); + const meta = compile('
'); + expect(meta).toHaveProperty('k'); + expect(meta.k).toBeArray(); + }); + test('returns d property with array value', () => { + expect.assertions(2); + const meta = compile('
'); + expect(meta).toHaveProperty('d'); + expect(meta.d).toBeArray(); + }); + + test('has empty k and d properties when no node refs', () => { + expect.assertions(2); + const meta = compile('
'); + expect(meta.k).toHaveLength(0); + expect(meta.d).toHaveLength(0); + }); + + test('has 3 k and d properties when 3 node refs', () => { + expect.assertions(2); + const meta = compile('
'); + expect(meta.k).toHaveLength(3); + expect(meta.d).toHaveLength(3); + }); + + test('has 3 k and d properties when 3 node refs with whitespace', () => { + expect.assertions(2); + const meta = compile(` +
+
+
+
+
+ `); + expect(meta.k).toHaveLength(3); + expect(meta.d).toHaveLength(3); + }); + + test('has 3 k and d properties when 3 node refs with messy whitespace', () => { + expect.assertions(2); + const meta = compile( + '\n\n\t
\t\t\n\n\n
\n\n
\n', + ); + expect(meta.k).toHaveLength(3); + expect(meta.d).toHaveLength(3); + }); + + test('has 1 k and d properties when 1 text ref', () => { + expect.assertions(2); + const meta = compile('
@a
'); + expect(meta.k).toHaveLength(1); + expect(meta.d).toHaveLength(1); + }); + + // TODO: Add documentation about this since it differs from the default compile.ts h() behaviour + test('has 1 k and d properties when 1 text ref with whitespace', () => { + expect.assertions(2); + const meta = compile('
@a
'); + expect(meta.k).toHaveLength(1); + expect(meta.d).toHaveLength(1); + }); + + test('has empty k and d properties when escaped node ref', () => { + expect.assertions(2); + const meta = compile('
'); + expect(meta.k).toHaveLength(0); + expect(meta.d).toHaveLength(0); + }); + + test('has empty k and d properties when escaped text ref', () => { + expect.assertions(2); + const meta = compile('
\\@a
'); + expect(meta.k).toHaveLength(0); + expect(meta.d).toHaveLength(0); + }); + + test('does not minify in whitespace-sensitive blocks', () => { + expect.assertions(1); + const meta = compile(` +
+
+          a
+           b
+          c
+
+
+          <span> Foo  </span>
+        
+ + Bar + + + <span> + Baz + </span> + + +
+ `); + expect(meta.html).toBe( + '
\n          a\n           b\n          c\n\n\n          <span> Foo  </span>\n        
Bar\n <span>\n Baz\n </span>\n
', + ); + }); + + // FIXME: Uncomment once bun string handling in macros bug is fixed. + // test('does not escape HTML entities', () => { + // expect.assertions(1); + // const template = '
<span>Foo</span>
'; + // const meta = compile(template); + // expect(meta.html).toBe(template); + // }); + + test('logs error when more than one root element', () => { + expect.assertions(2); + const spy = spyOn(console, 'error').mockImplementation(() => {}); + const template = '
'; + compileNoMacro(template); + expect(spy).toHaveBeenCalledWith('Expected template to have a single root element:', template); + expect(spy).toHaveBeenCalledTimes(1); + spy.mockRestore(); + }); + + test('returns expected html for basic template', () => { + expect.assertions(1); + const meta = compile(` +
    +
  • A
  • +
  • B
  • +
  • C
  • +
+ `); + expect(meta.html).toBe('
  • A
  • B
  • C
'); + }); + + // TODO: Test once lol-html (which powers bun's HTMLRewriter) fix their whitespace handling + test.skip('returns expected html for basic template with messy whitespace', () => { + expect.assertions(1); + const meta = compile(` +
    +
  • A
  • +
  • + B
  • +
  • C +
  • +
+ `); + expect(meta.html).toBe('
  • A
  • B
  • C
'); + }); + + test('returns expected html for SVG template', () => { + expect.assertions(1); + const meta = compile(` + + + + `); + expect(meta.html).toBe(''); + }); + + describe('keepComments option', () => { + test('removes comments by default', () => { + expect.assertions(1); + const meta = compile('
'); + expect(meta.html).toBe('
'); + }); + + test('keeps comment when option is true', () => { + expect.assertions(1); + const meta = compile('
', { keepComments: true }); + expect(meta.html).toBe('
'); + }); + + test('removes comment when option is false', () => { + expect.assertions(1); + const meta = compile('
', { keepComments: false }); + expect(meta.html).toBe('
'); + }); + + test('keeps multiple comments when option is true', () => { + expect.assertions(1); + const meta = compile('
', { + keepComments: true, + }); + expect(meta.html).toBe('
'); + }); + + test('removes multiple comments when option is false', () => { + expect.assertions(1); + const meta = compile('
', { + keepComments: false, + }); + expect(meta.html).toBe('
'); + }); + + test('keeps comment when option is true and template is only comment', () => { + expect.assertions(1); + const meta = compile('', { keepComments: true }); + expect(meta.html).toBe(''); + }); + + test('removes comment when option is false and template is only comment', () => { + expect.assertions(1); + const meta = compile('', { keepComments: false }); + expect(meta.html).toBe(''); + }); + + const templates = [ + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + ]; + + test.each(templates)('keeps comment when option is true for %j', (template) => { + expect.assertions(1); + const meta = compileNoMacro(template, { keepComments: true }); + expect(meta.html).toBe(template); + }); + + test.each(templates)('removes comment when option is false for %j', (template) => { + expect.assertions(1); + const meta = compileNoMacro(template, { keepComments: false }); + expect(meta.html).toBe('
'); + }); + + test('has 1 k and d properties when 1 comment ref when option is true', () => { + expect.assertions(2); + const meta = compile('
', { keepComments: true }); + expect(meta.k).toHaveLength(1); + expect(meta.d).toHaveLength(1); + }); + + test('returns expected html for template with comment ref when option is true', () => { + expect.assertions(1); + const meta = compile('
', { keepComments: true }); + expect(meta.html).toBe('
'); + }); + }); + + describe('keepSpaces option', () => { + test('removes spaces between tags and text by default', () => { + expect.assertions(1); + const meta = compile( + '
x \f\n\r\t\v\u0020\u00A0\u1680\u2000\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF
', + ); + expect(meta.html).toBe('
x
'); + }); + + test('keeps spaces between tags and text when option is true', () => { + expect.assertions(1); + const meta = compile( + '
x \f\n\r\t\v\u0020\u00A0\u1680\u2000\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF
', + { keepSpaces: true }, + ); + expect(meta.html).toBe('
x
'); + }); + + test('removes spaces between tags and text when option is false', () => { + expect.assertions(1); + const meta = compile( + '
x \f\n\r\t\v\u0020\u00A0\u1680\u2000\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF
', + { keepSpaces: false }, + ); + expect(meta.html).toBe('
x
'); + }); + }); +}); diff --git a/test/unit/runtime.test.ts b/test/unit/runtime.test.ts new file mode 100644 index 0000000..1d4f226 --- /dev/null +++ b/test/unit/runtime.test.ts @@ -0,0 +1,476 @@ +// XXX: This file has the same tests as test/unit/compile.test.ts, keep them in sync. + +import { afterEach, describe, expect, test } from 'bun:test'; +import { cleanup, render } from '@maxmilton/test-utils/dom'; +import { compile } from '../../src/macro' with { type: 'macro' }; +import { collect, h } from '../../src/runtime'; +import type { Refs } from '../../src/types'; + +describe('h', () => { + test('is a function', () => { + expect.assertions(2); + expect(h).toBeFunction(); + expect(h).not.toBeClass(); + }); + + test('expects 1 parameters', () => { + expect.assertions(1); + expect(h).toHaveParameters(1, 0); + }); + + describe('render', () => { + afterEach(cleanup); + + test('renders basic template', () => { + expect.assertions(1); + const meta = compile(` +
    +
  • A
  • +
  • B
  • +
  • C
  • +
+ `); + const view = h(meta.html); + const rendered = render(view); + expect(rendered.container.innerHTML).toBe('
  • A
  • B
  • C
'); + }); + + test('renders basic template with messy whitespace', () => { + expect.assertions(1); + const meta = compile(` +
    +
  • A
  • +
  • + B
  • +
  • C +
  • +
+ `); + const view = h(meta.html); + const rendered = render(view); + expect(rendered.container.innerHTML).toBe('
  • A
  • B
  • C
'); + }); + + test('renders SVG template', () => { + expect.assertions(2); + const meta = compile(` + + + + `); + const view = h(meta.html); + const rendered = render(view); + expect(view).toBeInstanceOf(window.SVGSVGElement); + expect(rendered.container.innerHTML).toBe( + '', + ); + }); + + test('returns root element', () => { + expect.assertions(3); + const meta = compile(` +
    +
  • A
  • +
  • B
  • +
  • C
  • +
+ `); + const view = h(meta.html); + const rendered = render(view); + expect(view).toBeInstanceOf(window.HTMLUListElement); + expect(view.id).toBe('root'); + expect(rendered.container.firstChild).toBe(view); + }); + + test('removes refs in template from output DOM', () => { + expect.assertions(1); + const meta = compile(` +
    +
  • A
  • +
  • B
  • +
+ `); + const view = h(meta.html); + const rendered = render(view); + expect(rendered.container.innerHTML).toBe('
  • A
  • B
'); + }); + + test('does not minify in whitespace-sensitive blocks', () => { + expect.assertions(1); + const meta = compile(` +
+
+            a
+            b
+            c
+
+
+            <span> Foo  </span>
+          
+ + Bar + + + <span> + Baz + </span> + + +
+ `); + const view = h(meta.html); + const rendered = render(view); + expect(rendered.container.innerHTML).toBe( + '
\n            a\n            b\n            c\n\n\n             Foo  \n          
Bar\n \n Baz\n \n
', + ); + }); + }); +}); + +// TODO: Once bun supports macros used as template literals tag functions, we +// could consider adding a html function similar to the standard runtime. + +// describe('html', () => { +// test('is a function', () => { +// expect.assertions(2); +// expect(html).toBeFunction(); +// expect(html).not.toBeClass(); +// }); +// +// test('expects 2 parameters (1 optional)', () => { +// expect.assertions(1); +// expect(html).toHaveParameters(1, 1); +// }); +// +// describe('render', () => { +// afterEach(cleanup); +// +// test('renders basic template', () => { +// expect.assertions(1); +// const view = html` +//
    +//
  • A
  • +//
  • B
  • +//
  • C
  • +//
+// `; +// const view = h(meta.html); +// const rendered = render(view); +// expect(rendered.container.innerHTML).toBe('
  • A
  • B
  • C
'); +// }); +// }); +// }); + +describe('collect', () => { + test('is a function', () => { + expect.assertions(2); + expect(collect).toBeFunction(); + expect(collect).not.toBeClass(); + }); + + test('expects 3 parameters', () => { + expect.assertions(1); + expect(collect).toHaveParameters(3, 0); + }); + + test('collects all refs', () => { + expect.assertions(39); + const meta = compile(` +
+
+ +
+
+

Test

+

This is a test.

+
    +
  1. One
  2. +
  3. Two
  4. +
+
+ + + +
+
+
+ @s +
+
+ `); + const view = h(meta.html); + const refs = collect(view, meta.k, meta.d); + expect(refs.a.nodeName).toEqual('DIV'); + expect(refs.a).toBeInstanceOf(window.HTMLDivElement); + expect(refs.b.nodeName).toEqual('HEADER'); + expect(refs.b).toBeInstanceOf(window.HTMLElement); + expect(refs.c.nodeName).toEqual('NAV'); + expect(refs.c).toBeInstanceOf(window.HTMLElement); + expect(refs.d.nodeName).toEqual('A'); + expect(refs.d).toBeInstanceOf(window.HTMLAnchorElement); + expect(refs.e.nodeName).toEqual('A'); + expect(refs.e).toBeInstanceOf(window.HTMLAnchorElement); + expect(refs.f.nodeName).toEqual('MAIN'); + expect(refs.f).toBeInstanceOf(window.HTMLElement); + expect(refs.g.nodeName).toEqual('H1'); + expect(refs.g).toBeInstanceOf(window.HTMLHeadingElement); + expect(refs.h.nodeName).toEqual('P'); + expect(refs.h).toBeInstanceOf(window.HTMLParagraphElement); + expect(refs.i.nodeName).toEqual('B'); + expect(refs.i).toBeInstanceOf(window.HTMLElement); + expect(refs.j.nodeName).toEqual('A'); + expect(refs.j).toBeInstanceOf(window.HTMLAnchorElement); + expect(refs.k.nodeName).toEqual('OL'); + expect(refs.k).toBeInstanceOf(window.HTMLOListElement); + expect(refs.l.nodeName).toEqual('LI'); + expect(refs.l).toBeInstanceOf(window.HTMLLIElement); + expect(refs.m.nodeName).toEqual('LI'); + expect(refs.m).toBeInstanceOf(window.HTMLLIElement); + expect(refs.n.nodeName).toEqual('FORM'); + expect(refs.n).toBeInstanceOf(window.HTMLFormElement); + expect(refs.o.nodeName).toEqual('INPUT'); + expect(refs.o).toBeInstanceOf(window.HTMLInputElement); + expect(refs.p.nodeName).toEqual('TEXTAREA'); + expect(refs.p).toBeInstanceOf(window.HTMLTextAreaElement); + expect(refs.q.nodeName).toEqual('BUTTON'); + expect(refs.q).toBeInstanceOf(window.HTMLButtonElement); + expect(refs.r.nodeName).toEqual('FOOTER'); + expect(refs.r).toBeInstanceOf(window.HTMLElement); + expect(refs.s.nodeName).toEqual('#text'); + expect(refs.s).toBeInstanceOf(window.Text); + expect(Object.keys(refs)).toHaveLength(19); + }); + + test('collects ref at start of element attributes', () => { + expect.assertions(4); + const meta = compile(` +
+ +
+ `); + const view = h(meta.html); + const refs = collect<{ search: HTMLInputElement }>(view, meta.k, meta.d); + expect(refs.search).toBeInstanceOf(window.HTMLInputElement); + expect(refs.search.id).toBe('search'); + expect(refs.search.name).toBe('q'); + expect(Object.keys(refs)).toHaveLength(1); + }); + + test('collects ref at end of element attributes', () => { + expect.assertions(4); + const meta = compile(` +
+ +
+ `); + const view = h(meta.html); + const refs = collect<{ search: HTMLInputElement }>(view, meta.k, meta.d); + expect(refs.search).toBeInstanceOf(window.HTMLInputElement); + expect(refs.search.id).toBe('search'); + expect(refs.search.name).toBe('q'); + expect(Object.keys(refs)).toHaveLength(1); + }); + + test('collects ref in middle of element attributes', () => { + expect.assertions(4); + const meta = compile(` +
+ +
+ `); + const view = h(meta.html); + const refs = collect<{ search: HTMLInputElement }>(view, meta.k, meta.d); + expect(refs.search).toBeInstanceOf(window.HTMLInputElement); + expect(refs.search.id).toBe('search'); + expect(refs.search.name).toBe('q'); + expect(Object.keys(refs)).toHaveLength(1); + }); + + // TODO: Instead of repeating similar tests multiple times, we should create + // a reusable test suite and create a test matrix that covers all the + // different combinations of options. + + describe('keepComments option', () => { + test('collects refs when option is default', () => { + expect.assertions(11); + const meta = compile(` +
+ + @a + + +
+ + @d + + +
+
+
+ `); + const view = h(meta.html); + const refs = collect(view, meta.k, meta.d); + expect(refs.a.nodeName).toEqual('#text'); + expect(refs.a).toBeInstanceOf(window.Text); + expect(refs.b).toBeUndefined(); + expect(refs.c.nodeName).toEqual('DIV'); + expect(refs.c).toBeInstanceOf(window.HTMLDivElement); + expect(refs.d.nodeName).toEqual('#text'); + expect(refs.d).toBeInstanceOf(window.Text); + expect(refs.e).toBeUndefined(); + expect(refs.f.nodeName).toEqual('DIV'); + expect(refs.f).toBeInstanceOf(window.HTMLDivElement); + expect(Object.keys(refs)).toHaveLength(4); + }); + + test('collects refs when option is true', () => { + expect.assertions(13); + const meta = compile( + ` +
+ + @a + + +
+ + @d + + +
+
+
+ `, + { keepComments: true }, + ); + const view = h(meta.html); + const refs = collect(view, meta.k, meta.d); + expect(refs.a.nodeName).toEqual('#text'); + expect(refs.a).toBeInstanceOf(window.Text); + expect(refs.b.nodeName).toEqual('#comment'); + expect(refs.b).toBeInstanceOf(window.Comment); + expect(refs.c.nodeName).toEqual('DIV'); + expect(refs.c).toBeInstanceOf(window.HTMLDivElement); + expect(refs.d.nodeName).toEqual('#text'); + expect(refs.d).toBeInstanceOf(window.Text); + expect(refs.e.nodeName).toEqual('#comment'); + expect(refs.e).toBeInstanceOf(window.Comment); + expect(refs.f.nodeName).toEqual('DIV'); + expect(refs.f).toBeInstanceOf(window.HTMLDivElement); + expect(Object.keys(refs)).toHaveLength(6); + }); + + test('collects refs when option is false', () => { + expect.assertions(11); + const meta = compile( + ` +
+ + @a + + +
+ + @d + + +
+
+
+ `, + { keepComments: false }, + ); + const view = h(meta.html); + const refs = collect(view, meta.k, meta.d); + expect(refs.a.nodeName).toEqual('#text'); + expect(refs.a).toBeInstanceOf(window.Text); + expect(refs.b).toBeUndefined(); + expect(refs.c.nodeName).toEqual('DIV'); + expect(refs.c).toBeInstanceOf(window.HTMLDivElement); + expect(refs.d.nodeName).toEqual('#text'); + expect(refs.d).toBeInstanceOf(window.Text); + expect(refs.e).toBeUndefined(); + expect(refs.f.nodeName).toEqual('DIV'); + expect(refs.f).toBeInstanceOf(window.HTMLDivElement); + expect(Object.keys(refs)).toHaveLength(4); + }); + }); + + describe('keepSpaces option', () => { + test('collects refs when option is default', () => { + expect.assertions(7); + const meta = compile(` +
+ @a +
+ @c +
+
+
+ `); + const view = h(meta.html); + const refs = collect(view, meta.k, meta.d); + expect(refs.a.nodeName).toEqual('#text'); + expect(refs.a).toBeInstanceOf(window.Text); + expect(refs.b.nodeName).toEqual('DIV'); + expect(refs.b).toBeInstanceOf(window.HTMLDivElement); + expect(refs.c.nodeName).toEqual('#text'); + expect(refs.c).toBeInstanceOf(window.Text); + expect(Object.keys(refs)).toHaveLength(4); + }); + + test('collects refs when option is true', () => { + expect.assertions(7); + const meta = compile( + ` +
+ @a +
+ @c +
+
+
+ `, + { keepSpaces: true }, + ); + const view = h(meta.html); + const refs = collect(view, meta.k, meta.d); + expect(refs.a.nodeName).toEqual('#text'); + expect(refs.a).toBeInstanceOf(window.Text); + expect(refs.b.nodeName).toEqual('DIV'); + expect(refs.b).toBeInstanceOf(window.HTMLDivElement); + expect(refs.c.nodeName).toEqual('#text'); + expect(refs.c).toBeInstanceOf(window.Text); + expect(Object.keys(refs)).toHaveLength(4); + }); + + test('collects refs when option is false', () => { + expect.assertions(7); + const meta = compile( + ` +
+ @a +
+ @c +
+
+
+ `, + { keepSpaces: false }, + ); + const view = h(meta.html); + const refs = collect(view, meta.k, meta.d); + expect(refs.a.nodeName).toEqual('#text'); + expect(refs.a).toBeInstanceOf(window.Text); + expect(refs.b.nodeName).toEqual('DIV'); + expect(refs.b).toBeInstanceOf(window.HTMLDivElement); + expect(refs.c.nodeName).toEqual('#text'); + expect(refs.c).toBeInstanceOf(window.Text); + expect(Object.keys(refs)).toHaveLength(4); + }); + }); +}); diff --git a/test/unit/store.test.ts b/test/unit/store.test.ts new file mode 100644 index 0000000..c05d519 --- /dev/null +++ b/test/unit/store.test.ts @@ -0,0 +1,190 @@ +import { describe, expect, mock, test } from 'bun:test'; +import { isProxy } from 'node:util/types'; +import { store } from '../../src/store'; + +describe('store', () => { + test('is a function', () => { + expect.assertions(2); + expect(store).toBeFunction(); + expect(store).not.toBeClass(); + }); + + test('expects 1 parameter', () => { + expect.assertions(1); + expect(store).toHaveParameters(1, 0); + }); + + test('returns a Proxy', () => { + expect.assertions(1); + const state = store({}); + expect(isProxy(state)).toBeTrue(); + }); + + test('returns an object with the same properties', () => { + expect.assertions(25); + // eslint-disable-next-line @typescript-eslint/no-extraneous-class + class TestClass {} + const s = Symbol('s'); + const initialState = { + a: 1, + b: 2, + c: null, + d: undefined, + e: 'hello', + f: true, + g: false, + h: () => {}, + i: [1, 2, 3], + j: { ja: Number.POSITIVE_INFINITY, jb: Number.NEGATIVE_INFINITY }, + k: Symbol('k'), + l: new Date(), + m: new Map(), + n: new Set(), + o: document.createElement('div'), + p: new Error('test'), + q: Promise.resolve(), + r: new Promise(() => {}), + [s]: 'symbol', + t: new Uint8Array(), + u: window.location, + v: new TestClass(), + w: TestClass, + x: /test/, + // biome-ignore lint/complexity/useRegexLiterals: + y: new RegExp('test'), // eslint-disable-line prefer-regex-literals + z: window, + }; + const state = store(initialState); + // eslint-disable-next-line guard-for-in + for (const key in initialState) { + expect(state).toHaveProperty(key, initialState[key as keyof typeof initialState]); + } + }); + + test('returns an object with an on() function', () => { + expect.assertions(3); + const state = store({}); + expect(state.on).toBeFunction(); + expect(state.on).not.toBeClass(); + expect(state.on).toHaveParameters(2, 0); + }); + + test('returns off() function from on()', () => { + expect.assertions(3); + const state = store({ a: 1 }); + const off = state.on('a', () => {}); + expect(off).toBeFunction(); + expect(off).not.toBeClass(); + expect(off).toHaveParameters(0, 0); + }); + + test('mutating initial state does not mutate store state', () => { + expect.assertions(1); + const initialState = { a: 1 }; + const state = store(initialState); + initialState.a = 2; + expect(state.a).toBe(1); + }); + + test('mutating store state does not mutate initial state', () => { + expect.assertions(1); + const initialState = { a: 1 }; + const state = store(initialState); + state.a = 2; + expect(initialState.a).toBe(1); + }); + + test('mutating store state triggers callback', () => { + expect.assertions(4); + const initialState = { a: 0 }; + const state = store(initialState); + const callback = mock(() => {}); + state.on('a', callback); + state.a = 1; + expect(callback).toHaveBeenCalledWith(1, 0); + state.a = 2; + expect(callback).toHaveBeenCalledWith(2, 1); + state.a = 3; + expect(callback).toHaveBeenCalledWith(3, 2); + expect(callback).toHaveBeenCalledTimes(3); + }); + + test('mutating store state does not trigger callback after off()', () => { + expect.assertions(2); + const initialState = { a: 0 }; + const state = store(initialState); + const callback = mock(() => {}); + const off = state.on('a', callback); + state.a = 1; + expect(callback).toHaveBeenCalledTimes(1); + off(); + state.a = 2; + state.a = 3; + expect(callback).toHaveBeenCalledTimes(1); // still called only once + }); + + test('calls callback with new value and previous value', () => { + expect.assertions(1); + const initialState = { a: 'old' }; + const state = store(initialState); + const callback = mock(() => {}); + state.on('a', callback); + state.a = 'new'; + expect(callback).toHaveBeenCalledWith('new', 'old'); + }); + + test('calls all callbacks for mutated property', () => { + expect.assertions(9); + const initialState = { a: 0 }; + const state = store(initialState); + const callback1 = mock(() => {}); + const callback2 = mock(() => {}); + const callback3 = mock(() => {}); + state.on('a', callback1); + state.on('a', callback2); + state.on('a', callback3); + state.a = 1; + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledTimes(1); + expect(callback3).toHaveBeenCalledTimes(1); + state.a = 2; + expect(callback1).toHaveBeenCalledTimes(2); + expect(callback2).toHaveBeenCalledTimes(2); + expect(callback3).toHaveBeenCalledTimes(2); + state.a = 3; + state.a = 4; + expect(callback1).toHaveBeenCalledTimes(4); + expect(callback2).toHaveBeenCalledTimes(4); + expect(callback3).toHaveBeenCalledTimes(4); + }); + + test('calls only callbacks for mutated property', () => { + expect.assertions(12); + const initialState = { a: 0, b: 0, c: 0 }; + const state = store(initialState); + const callbackA = mock(() => {}); + const callbackB = mock(() => {}); + const callbackC1 = mock(() => {}); + const callbackC2 = mock(() => {}); + state.on('a', callbackA); + state.on('b', callbackB); + state.on('c', callbackC1); + state.on('c', callbackC2); + state.a = 1; + expect(callbackA).toHaveBeenCalledTimes(1); + expect(callbackB).toHaveBeenCalledTimes(0); + expect(callbackC1).toHaveBeenCalledTimes(0); + expect(callbackC2).toHaveBeenCalledTimes(0); + state.b = 2; + expect(callbackA).toHaveBeenCalledTimes(1); + expect(callbackB).toHaveBeenCalledTimes(1); + expect(callbackC1).toHaveBeenCalledTimes(0); + expect(callbackC2).toHaveBeenCalledTimes(0); + state.c = 3; + state.c = 4; + expect(callbackA).toHaveBeenCalledTimes(1); + expect(callbackB).toHaveBeenCalledTimes(1); + expect(callbackC1).toHaveBeenCalledTimes(2); + expect(callbackC2).toHaveBeenCalledTimes(2); + }); +}); diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts new file mode 100644 index 0000000..7407fac --- /dev/null +++ b/test/unit/utils.test.ts @@ -0,0 +1,586 @@ +import { describe, expect, mock, test } from 'bun:test'; +import { + append, + clone, + create, + fragment, + insert, + noop, + onRemove, + prepend, + replace, + text, +} from '../../src/utils'; + +const ul = document.createElement('ul'); +const liA = document.createElement('li'); +liA.className = 'a'; +const liB = document.createElement('li'); +liB.className = 'b'; +const liC = document.createElement('li'); +liC.className = 'c'; + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +class Sample {} + +const NOT_DOM_NODES = [ + null, + undefined, + '', + 'div', + '
', + '
', + 0, + 1, + -1, + 1n, + -1n, + Number.MAX_VALUE, + Number.MIN_VALUE, + Number.POSITIVE_INFINITY, + Number.NEGATIVE_INFINITY, + Number.NaN, + BigInt(Number.MAX_SAFE_INTEGER + 1), + true, + false, + {}, + [], + [liA.cloneNode()], + () => {}, + () => liA.cloneNode(), + function div() {}, + Sample, + new Sample(), + Symbol('test'), + new Map(), + new Set(), + new Int8Array(1), + new Uint8Array(1), +] as const; + +describe('noop', () => { + test('is a function', () => { + expect.assertions(2); + expect(noop).toBeFunction(); + expect(noop).not.toBeClass(); + }); + + test('expects no parameters', () => { + expect.assertions(1); + expect(noop).toHaveParameters(0, 0); + }); + + test('returns undefined', () => { + expect.assertions(1); + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression + expect(noop()).toBeUndefined(); + }); + + test('is an empty function', () => { + expect.assertions(1); + expect(noop.toString()).toBe('() => {\n}'); + }); +}); + +describe('fragment', () => { + test('is a function', () => { + expect.assertions(2); + expect(fragment).toBeFunction(); + expect(fragment).not.toBeClass(); + }); + + test('expects no parameters', () => { + expect.assertions(1); + expect(fragment).toHaveParameters(0, 0); + }); + + test('returns a DocumentFragment', () => { + expect.assertions(1); + expect(fragment()).toBeInstanceOf(window.DocumentFragment); + }); +}); + +describe('text', () => { + test('is a function', () => { + expect.assertions(2); + expect(text).toBeFunction(); + expect(text).not.toBeClass(); + }); + + test('expects 1 parameter', () => { + expect.assertions(1); + expect(text).toHaveParameters(1, 0); + }); + + test('returns a Text node', () => { + expect.assertions(1); + expect(text('test')).toBeInstanceOf(window.Text); + }); + + test('sets text content', () => { + expect.assertions(1); + expect(text('test').textContent).toBe('test'); + }); +}); + +describe('create', () => { + test('is a function', () => { + expect.assertions(2); + expect(create).toBeFunction(); + expect(create).not.toBeClass(); + }); + + test('expects 1 parameter', () => { + expect.assertions(1); + expect(create).toHaveParameters(1, 0); + }); + + const inputs = [ + ['x', window.HTMLUnknownElement], + ['div', window.HTMLDivElement], + ['span', window.HTMLSpanElement], + ['a', window.HTMLAnchorElement], + ['img', window.HTMLImageElement], + ['h1', window.HTMLHeadingElement], + ['h2', window.HTMLHeadingElement], + ['h3', window.HTMLHeadingElement], + ['h4', window.HTMLHeadingElement], + ['h5', window.HTMLHeadingElement], + ['h6', window.HTMLHeadingElement], + ['p', window.HTMLParagraphElement], + ['input', window.HTMLInputElement], + ['textarea', window.HTMLTextAreaElement], + ['button', window.HTMLButtonElement], + ['select', window.HTMLSelectElement], + ['option', window.HTMLOptionElement], + ['optgroup', window.HTMLOptGroupElement], + ['datalist', window.HTMLDataListElement], + ['form', window.HTMLFormElement], + ['fieldset', window.HTMLFieldSetElement], + ['legend', window.HTMLLegendElement], + ['label', window.HTMLLabelElement], + ['ul', window.HTMLUListElement], + ['ol', window.HTMLOListElement], + ['li', window.HTMLLIElement], + ['dl', window.HTMLDListElement], + ['dt', window.HTMLElement], + ['dd', window.HTMLElement], + ['table', window.HTMLTableElement], + ['caption', window.HTMLTableCaptionElement], + ['thead', window.HTMLTableSectionElement], + ['tbody', window.HTMLTableSectionElement], + ['tfoot', window.HTMLTableSectionElement], + ['colgroup', window.HTMLTableColElement], + ['col', window.HTMLTableColElement], + ['tr', window.HTMLTableRowElement], + ['td', window.HTMLTableCellElement], + ['th', window.HTMLTableCellElement], + ['hr', window.HTMLHRElement], + ['br', window.HTMLBRElement], + ['pre', window.HTMLPreElement], + ['blockquote', window.HTMLQuoteElement], + ['q', window.HTMLQuoteElement], + ['ins', window.HTMLModElement], + ['del', window.HTMLModElement], + ['iframe', window.HTMLIFrameElement], + ['embed', window.HTMLEmbedElement], + ['object', window.HTMLObjectElement], + ['video', window.HTMLVideoElement], + ['audio', window.HTMLAudioElement], + ['source', window.HTMLSourceElement], + ['track', window.HTMLTrackElement], + ['canvas', window.HTMLCanvasElement], + ['map', window.HTMLMapElement], + ['area', window.HTMLAreaElement], + ['time', window.HTMLTimeElement], + ['template', window.HTMLTemplateElement], + ['slot', window.HTMLSlotElement], + ] as const; + + for (const [input, expected] of inputs) { + test(`returns ${expected.name} for "${input}" argument`, () => { + expect.assertions(1); + // @ts-expect-error - "x" is an intentional invalid element name + expect(create(input)).toBeInstanceOf(expected); + }); + } +}); + +describe('clone', () => { + test('is a function', () => { + expect.assertions(2); + expect(clone).toBeFunction(); + expect(clone).not.toBeClass(); + }); + + test('expects 1 parameter', () => { + expect.assertions(1); + expect(clone).toHaveParameters(1, 0); + }); + + test('throws without parameters', () => { + expect.assertions(3); + // @ts-expect-error - intentional invalid parameters + expect(() => clone()).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => clone(null)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + // eslint-disable-next-line unicorn/no-useless-undefined + expect(() => clone(undefined)).toThrow(window.TypeError); + }); + + const inputs = [ + document.createElement('div'), + document.createElement('span'), + document.createElement('p'), + document.createElement('a'), + document.createElement('button'), + document.createElement('input'), + document.createElement('textarea'), + document.createElement('select'), + ] as const; + + for (const input of inputs) { + test(`returns cloned ${input.tagName} element`, () => { + expect.assertions(2); + const result = clone(input); + expect(result).not.toBe(input); + expect(result.tagName).toBe(input.tagName); + }); + } +}); + +describe('append', () => { + test('is a function', () => { + expect.assertions(2); + expect(append).toBeFunction(); + expect(append).not.toBeClass(); + }); + + test('expects 2 parameters', () => { + expect.assertions(1); + expect(append).toHaveParameters(2, 0); + }); + + test('throws without parameters', () => { + expect.assertions(5); + // @ts-expect-error - intentional invalid parameters + expect(() => append()).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => append(null)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + // eslint-disable-next-line unicorn/no-useless-undefined + expect(() => append(undefined)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => append(null, null)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + // eslint-disable-next-line unicorn/no-useless-undefined + expect(() => append(undefined, undefined)).toThrow(window.TypeError); + }); + + test('throws when parameters are not an element', () => { + expect.assertions(NOT_DOM_NODES.length * 2); + for (const input of NOT_DOM_NODES) { + // @ts-expect-error - intentional invalid parameters + expect(() => append(ul.cloneNode(), input)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => append(input, ul.cloneNode())).toThrow(window.TypeError); + } + }); + + test('appends child element', () => { + expect.assertions(1); + const root = ul.cloneNode() as HTMLUListElement; + append(liA.cloneNode(), root); + append(liB.cloneNode(), root); + append(liC.cloneNode(), root); + expect(root.outerHTML).toBe( + '
', + ); + }); +}); + +describe('prepend', () => { + test('is a function', () => { + expect.assertions(2); + expect(prepend).toBeFunction(); + expect(prepend).not.toBeClass(); + }); + + test('expects 2 parameters', () => { + expect.assertions(1); + expect(prepend).toHaveParameters(2, 0); + }); + + test('throws without parameters', () => { + expect.assertions(5); + // @ts-expect-error - intentional invalid parameters + expect(() => prepend()).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => prepend(null)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + // eslint-disable-next-line unicorn/no-useless-undefined + expect(() => prepend(undefined)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => prepend(null, null)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + // eslint-disable-next-line unicorn/no-useless-undefined + expect(() => prepend(undefined, undefined)).toThrow(window.TypeError); + }); + + test('throws when parameters are not an element', () => { + expect.assertions(NOT_DOM_NODES.length * 2 - 1); // skip null as child node + for (const input of NOT_DOM_NODES) { + // @ts-expect-error - intentional invalid parameters + expect(() => prepend(ul.cloneNode(), input)).toThrow(window.TypeError); + // FIXME: happy-dom does not handle this as per spec. + // eslint-disable-next-line no-continue + if (input === null) continue; + // @ts-expect-error - intentional invalid parameters + expect(() => prepend(input, ul.cloneNode())).toThrow(window.TypeError); + } + }); + + test('prepends child element', () => { + expect.assertions(1); + const root = ul.cloneNode() as HTMLUListElement; + prepend(liA.cloneNode(), root); + prepend(liB.cloneNode(), root); + prepend(liC.cloneNode(), root); + expect(root.outerHTML).toBe( + '
', + ); + }); +}); + +describe('insert', () => { + test('is a function', () => { + expect.assertions(2); + expect(insert).toBeFunction(); + expect(insert).not.toBeClass(); + }); + + test('expects 2 parameters', () => { + expect.assertions(1); + expect(insert).toHaveParameters(2, 0); + }); + + test('throws without parameters', () => { + expect.assertions(5); + // @ts-expect-error - intentional invalid parameters + expect(() => insert()).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => insert(null)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + // eslint-disable-next-line unicorn/no-useless-undefined + expect(() => insert(undefined)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => insert(null, null)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + // eslint-disable-next-line unicorn/no-useless-undefined + expect(() => insert(undefined, undefined)).toThrow(window.TypeError); + }); + + test('throws when parameters are not an element', () => { + expect.assertions(NOT_DOM_NODES.length * 2); + for (const input of NOT_DOM_NODES) { + // @ts-expect-error - intentional invalid parameters + expect(() => insert(ul.cloneNode(), input)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => insert(input, ul.cloneNode())).toThrow(window.TypeError); + } + }); + + test('throws when target element has no parent', () => { + expect.assertions(1); + const target = ul.cloneNode() as HTMLUListElement; + const node = liA.cloneNode() as HTMLLIElement; + expect(() => insert(node, target)).toThrow(window.TypeError); + }); + + test('inserts child element after target element', () => { + expect.assertions(1); + const root = ul.cloneNode() as HTMLUListElement; + const target = liA.cloneNode() as HTMLLIElement; + root.appendChild(target); + insert(liB.cloneNode(), target); + insert(liC.cloneNode(), target); + expect(root.outerHTML).toBe( + '
', + ); + }); + + // FIXME: Check DOM node is moved to new parent and is in fact the same node + removed from old parent. + // test('moves existing element to new parent', () => { + // expect.assertions(1); + // const root = ul.cloneNode() as HTMLUListElement; + // const target = liA.cloneNode() as HTMLLIElement; + // root.appendChild(target); + // const newParent = ul.cloneNode() as HTMLUListElement; + // newParent.appendChild(target); + // insert(liB.cloneNode(), target); + // insert(liC.cloneNode(), target); + // expect(root.outerHTML).toBe('
'); + // }); +}); + +describe('replace', () => { + test('is a function', () => { + expect.assertions(2); + expect(replace).toBeFunction(); + expect(replace).not.toBeClass(); + }); + + test('expects 2 parameters', () => { + expect.assertions(1); + expect(replace).toHaveParameters(2, 0); + }); + + test('throws without parameters', () => { + expect.assertions(5); + // @ts-expect-error - intentional invalid parameters + expect(() => replace()).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => replace(null)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + // eslint-disable-next-line unicorn/no-useless-undefined + expect(() => replace(undefined)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => replace(null, null)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + // eslint-disable-next-line unicorn/no-useless-undefined + expect(() => replace(undefined, undefined)).toThrow(window.TypeError); + }); + + test('throws when parameters are not an element', () => { + expect.assertions(NOT_DOM_NODES.length * 2); + for (const input of NOT_DOM_NODES) { + // @ts-expect-error - intentional invalid parameters + expect(() => replace(ul.cloneNode(), input)).toThrow(window.TypeError); + // @ts-expect-error - intentional invalid parameters + expect(() => replace(input, ul.cloneNode())).toThrow(window.TypeError); + } + }); + + test('throws when target element has no parent', () => { + expect.assertions(1); + const target = ul.cloneNode() as HTMLUListElement; + const node = liA.cloneNode() as HTMLLIElement; + expect(() => replace(node, target)).toThrow(window.TypeError); + }); + + test('replaces target element with child element', () => { + expect.assertions(1); + const root = ul.cloneNode() as HTMLUListElement; + const target = liA.cloneNode() as HTMLLIElement; + root.appendChild(target); + replace(liB.cloneNode(), target); + replace(liC.cloneNode(), target); + expect(root.outerHTML).toBe('
'); + }); + + // FIXME: Check DOM node is moved to new parent and is in fact the same node + removed from old parent. + // test('moves existing element to new parent', () => { + // expect.assertions(1); + // const root = ul.cloneNode() as HTMLUListElement; + // const target = liA.cloneNode() as HTMLLIElement; + // root.appendChild(target); + // const newParent = ul.cloneNode() as HTMLUListElement; + // newParent.appendChild(target); + // replace(liB.cloneNode(), target); + // replace(liC.cloneNode(), target); + // expect(root.outerHTML).toBe('
'); + // }); +}); + +describe('onRemove', () => { + test('is a function', () => { + expect.assertions(2); + expect(onRemove).toBeFunction(); + expect(onRemove).not.toBeClass(); + }); + + test('expects 2 parameters', () => { + expect.assertions(1); + expect(onRemove).toHaveParameters(2, 0); + }); + + test('calls callback when watched element is removed', async () => { + expect.assertions(1); + const spy = mock(() => {}); + const root = document.createElement('div'); + document.body.appendChild(root); + onRemove(root, spy); + root.remove(); + await happyDOM.waitUntilComplete(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + test('calls callback when watched element is moved (remove + add)', async () => { + expect.assertions(1); + const spy = mock(() => {}); + const root = document.createElement('div'); + document.body.appendChild(root); + onRemove(root, spy); + document.body.appendChild(root); + await happyDOM.waitUntilComplete(); + expect(spy).toHaveBeenCalledTimes(1); + root.remove(); // cleanup + }); + + test('calls callback when parent parent element is removed', async () => { + expect.assertions(1); + const spy = mock(() => {}); + const root = document.createElement('div'); + const parent = document.createElement('div'); + const child = document.createElement('div'); + parent.appendChild(child); + root.appendChild(parent); + document.body.appendChild(root); + onRemove(child, spy); + root.remove(); + await happyDOM.waitUntilComplete(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + test('does not call callback when nested child element is removed', async () => { + expect.assertions(1); + const spy = mock(() => {}); + const root = document.createElement('div'); + const child = document.createElement('div'); + root.appendChild(child); + document.body.appendChild(root); + onRemove(root, spy); + child.remove(); + await happyDOM.waitUntilComplete(); + expect(spy).not.toHaveBeenCalled(); + root.remove(); // cleanup + }); + + test('does not call callback when element is added', async () => { + expect.assertions(1); + const spy = mock(() => {}); + const root = document.createElement('div'); + const child = document.createElement('div'); + onRemove(root, spy); + document.body.appendChild(root); + root.appendChild(child); + await happyDOM.waitUntilComplete(); + expect(spy).not.toHaveBeenCalled(); + root.remove(); // cleanup + }); + + test('does not call callback when child element is moved', async () => { + expect.assertions(1); + const spy = mock(() => {}); + const root = document.createElement('div'); + const child = document.createElement('div'); + document.body.appendChild(root); + onRemove(root, spy); + root.appendChild(child); + root.appendChild(child); + await happyDOM.waitUntilComplete(); + expect(spy).not.toHaveBeenCalled(); + root.remove(); // cleanup + }); +}); diff --git a/test/utils.test.ts b/test/utils.test.ts deleted file mode 100644 index bf936ad..0000000 --- a/test/utils.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import * as assert from 'uvu/assert'; -import { append, create, createFragment, noop, prepend } from '../src/utils'; -import { describe } from './utils'; - -const ul = document.createElement('ul'); -const liA = document.createElement('li'); -liA.className = 'a'; -const liB = document.createElement('li'); -liB.className = 'b'; -const liC = document.createElement('li'); -liC.className = 'c'; - -class Sample {} - -const NOT_DOM_NODES = [ - null, - undefined, - '', - 'div', - '
', - '
', - 0, - 1, - -1, - 1n, - -1n, - Number.MAX_VALUE, - Number.MIN_VALUE, - Number.POSITIVE_INFINITY, - Number.NEGATIVE_INFINITY, - Number.NaN, - true, - false, - {}, - [], - [liA.cloneNode()], - () => {}, - () => liA.cloneNode(), - function div() {}, - Sample, - new Sample(), - Symbol('test'), - new Map(), - new Set(), - new Int8Array(1), - new Uint8Array(1), -] as const; - -describe('noop', (test) => { - test('is a function', () => { - assert.type(noop, 'function'); - }); - - test('expects no parameters', () => { - assert.is(noop.length, 0); - }); - - test('returns undefined', () => { - assert.is(noop(), undefined); - }); - - test('is an empty function', () => { - assert.fixture(noop.toString(), '() => {\n}'); - }); -}); - -describe('createFragment', (test) => { - test('is a function', () => { - assert.type(createFragment, 'function'); - }); - - test('expects no parameters', () => { - assert.is(createFragment.length, 0); - }); - - test('returns a DocumentFragment', () => { - assert.instance(createFragment(), DocumentFragment); - }); -}); - -describe('create', (test) => { - test('is a function', () => { - assert.type(create, 'function'); - }); - - test('expects 1 parameter', () => { - assert.is(create.length, 1); - }); - - test('returns expected element', () => { - // @ts-expect-error - intentional invalid element name - assert.instance(create('x'), window.HTMLUnknownElement); - assert.instance(create('div'), window.HTMLDivElement); - assert.instance(create('span'), window.HTMLSpanElement); - assert.instance(create('a'), window.HTMLAnchorElement); - assert.instance(create('img'), window.HTMLImageElement); - assert.instance(create('h1'), window.HTMLHeadingElement); - assert.instance(create('h2'), window.HTMLHeadingElement); - assert.instance(create('h3'), window.HTMLHeadingElement); - assert.instance(create('h4'), window.HTMLHeadingElement); - assert.instance(create('h5'), window.HTMLHeadingElement); - assert.instance(create('h6'), window.HTMLHeadingElement); - assert.instance(create('p'), window.HTMLParagraphElement); - assert.instance(create('input'), window.HTMLInputElement); - assert.instance(create('textarea'), window.HTMLTextAreaElement); - assert.instance(create('button'), window.HTMLButtonElement); - assert.instance(create('select'), window.HTMLSelectElement); - assert.instance(create('option'), window.HTMLOptionElement); - assert.instance(create('optgroup'), window.HTMLOptGroupElement); - assert.instance(create('form'), window.HTMLFormElement); - assert.instance(create('fieldset'), window.HTMLFieldSetElement); - assert.instance(create('legend'), window.HTMLLegendElement); - assert.instance(create('label'), window.HTMLLabelElement); - assert.instance(create('ul'), window.HTMLUListElement); - assert.instance(create('ol'), window.HTMLOListElement); - assert.instance(create('li'), window.HTMLLIElement); - assert.instance(create('dl'), window.HTMLDListElement); - assert.instance(create('dt'), window.HTMLElement); - assert.instance(create('dd'), window.HTMLElement); - assert.instance(create('table'), window.HTMLTableElement); - assert.instance(create('caption'), window.HTMLTableCaptionElement); - assert.instance(create('thead'), window.HTMLTableSectionElement); - assert.instance(create('tbody'), window.HTMLTableSectionElement); - assert.instance(create('tfoot'), window.HTMLTableSectionElement); - assert.instance(create('colgroup'), window.HTMLTableColElement); - assert.instance(create('col'), window.HTMLTableColElement); - assert.instance(create('tr'), window.HTMLTableRowElement); - assert.instance(create('td'), window.HTMLTableCellElement); - assert.instance(create('th'), window.HTMLTableCellElement); - assert.instance(create('hr'), window.HTMLHRElement); - assert.instance(create('br'), window.HTMLBRElement); - assert.instance(create('pre'), window.HTMLPreElement); - assert.instance(create('blockquote'), window.HTMLQuoteElement); - assert.instance(create('q'), window.HTMLQuoteElement); - assert.instance(create('ins'), window.HTMLModElement); - assert.instance(create('del'), window.HTMLModElement); - assert.instance(create('iframe'), window.HTMLIFrameElement); - assert.instance(create('embed'), window.HTMLEmbedElement); - assert.instance(create('object'), window.HTMLObjectElement); - assert.instance(create('video'), window.HTMLVideoElement); - assert.instance(create('audio'), window.HTMLAudioElement); - assert.instance(create('source'), window.HTMLSourceElement); - assert.instance(create('track'), window.HTMLTrackElement); - assert.instance(create('canvas'), window.HTMLCanvasElement); - assert.instance(create('map'), window.HTMLMapElement); - assert.instance(create('area'), window.HTMLAreaElement); - assert.instance(create('time'), window.HTMLTimeElement); - }); -}); - -describe('append', (test) => { - test('is a function', () => { - assert.type(append, 'function'); - }); - - test('expects 2 parameters', () => { - assert.is(append.length, 2); - }); - - test('throws without parameters', () => { - // @ts-expect-error - intentional invalid parameters - assert.throws(() => append()); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => append(null)); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => append(undefined)); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => append(null, null)); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => append(undefined, undefined)); - }); - - test('throws when parameters are not an element', () => { - for (const input of NOT_DOM_NODES) { - // @ts-expect-error - intentional invalid parameters - assert.throws(() => append(ul.cloneNode(), input)); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => append(input, ul.cloneNode())); - } - }); - - test('appends child element', () => { - const root = ul.cloneNode() as HTMLUListElement; - append(liA.cloneNode(), root); - append(liB.cloneNode(), root); - append(liC.cloneNode(), root); - assert.snapshot( - root.outerHTML, - '
', - ); - }); -}); - -describe('prepend', (test) => { - test('is a function', () => { - assert.type(prepend, 'function'); - }); - - test('expects 2 parameters', () => { - assert.is(prepend.length, 2); - }); - - test('throws without parameters', () => { - // @ts-expect-error - intentional invalid parameters - assert.throws(() => prepend()); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => prepend(null)); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => prepend(undefined)); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => prepend(null, null)); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => prepend(undefined, undefined)); - }); - - test('throws when parameters are not an element', () => { - for (const input of NOT_DOM_NODES) { - // @ts-expect-error - intentional invalid parameters - assert.throws(() => prepend(ul.cloneNode(), input)); - // @ts-expect-error - intentional invalid parameters - assert.throws(() => prepend(input, ul.cloneNode())); - } - }); - - test('prepends child element', () => { - const root = ul.cloneNode() as HTMLUListElement; - prepend(liA.cloneNode(), root); - prepend(liB.cloneNode(), root); - prepend(liC.cloneNode(), root); - assert.snapshot( - root.outerHTML, - '
', - ); - }); -}); diff --git a/test/utils.ts b/test/utils.ts deleted file mode 100644 index f0c3f16..0000000 --- a/test/utils.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { JSDOM } from 'jsdom'; -import { suite, type Context, type Test } from 'uvu'; - -// increase limit from 10 -global.Error.stackTraceLimit = 100; - -const mountedContainers = new Set(); - -export function describe( - name: string, - fn: (test: Test) => void, -): void { - const test = suite(name); - fn(test); - test.run(); -} - -export function setupDOM(): void { - const dom = new JSDOM('', { - pretendToBeVisual: true, - runScripts: 'dangerously', - url: 'http://localhost/', - }); - - global.window = dom.window.document.defaultView!; - global.document = global.window.document; -} - -export function setupMocks(): void { - global.DocumentFragment = global.window.DocumentFragment; -} - -export interface RenderResult { - /** A wrapper DIV which contains your mounted component. */ - container: HTMLDivElement; - /** - * A helper to print the HTML structure of the mounted container. The HTML is - * prettified and may not accurately represent your actual HTML. It's intended - * for debugging tests only and should not be used in any assertions. - * - * @param el - An element to inspect. Default is the mounted container. - */ - debug(el?: Element): void; - unmount(): void; -} - -export function render(component: Node): RenderResult { - const container = document.createElement('div'); - - container.appendChild(component); - document.body.appendChild(container); - - mountedContainers.add(container); - - return { - container, - debug(el = container) { - /* prettier-ignore */ // eslint-disable-next-line - console.log('DEBUG:\n' + require('prettier').format(el.innerHTML, { parser: 'html' })); - }, - unmount() { - container.removeChild(component); - }, - }; -} - -export function cleanup(): void { - if (!mountedContainers || mountedContainers.size === 0) { - throw new Error( - 'No mounted components exist, did you forget to call render()?', - ); - } - - mountedContainers.forEach((container) => { - if (container.parentNode === document.body) { - container.remove(); - } - - mountedContainers.delete(container); - }); -} diff --git a/tsconfig.json b/tsconfig.json index 54d6869..4493895 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,13 @@ { "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "declaration": true, - "target": "esnext", "module": "esnext", + "moduleDetection": "force", "moduleResolution": "bundler", - "esModuleInterop": true, + "esModuleInterop": false, "resolveJsonModule": true, "skipLibCheck": true, - "allowJs": true, - "checkJs": true, + "allowJs": false, "strict": true, "allowUnreachableCode": false, @@ -19,9 +15,18 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": false, // covered by eslint "noImplicitOverride": true, + "noUncheckedSideEffectImports": true, "noUnusedLocals": false, // covered by eslint "noUnusedParameters": false, // covered by eslint "verbatimModuleSyntax": true }, - "exclude": ["coverage", "dist", "node_modules", "test", "build.mjs"] + "include": [ + "*.config.mjs", + "*.config.ts", + "*.d.ts", + "build.ts", + "src", + "test" + ], + "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..04e5bfc --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true, + "moduleResolution": "node10", + "allowSyntheticDefaultImports": true + }, + "include": [ + "**/*.cjs", + "package.json" // included in package.json#exports + ] +}