diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000000..43c7bc7deaa --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,27 @@ +# Changesets + +This project uses [changesets](https://github.com/changesets/changesets) for version management and changelog generation. + +## Adding a changeset + +When you make a change that should be released, run: + +```bash +pnpm changeset +``` + +This will prompt you to: +1. Select which packages are affected +2. Choose the bump type (patch/minor/major) +3. Write a summary of the changes + +## Lockstep versioning + +All `@stencil/*` packages are configured for **lockstep versioning** - they will always have the same version number. When any package changes, all packages are bumped together. + +## Release process + +1. Changesets accumulate in `.changeset/` as PRs are merged +2. When ready to release, run `pnpm changeset:version` to consume changesets and bump versions +3. Review the generated CHANGELOG.md files +4. Run `pnpm changeset:publish` to publish all packages diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000000..c0054d88e07 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [["@stencil/core", "@stencil/cli", "@stencil/dev-server", "@stencil/mock-doc"]], + "linked": [], + "access": "public", + "baseBranch": "v5", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 248472f0ceb..00000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,106 +0,0 @@ -module.exports = { - root: true, - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'jsdoc', 'jest', 'simple-import-sort', 'wdio'], - extends: [ - 'plugin:jest/recommended', - // including prettier here ensures that we don't set any rules which will conflict - // with Prettier's formatting. Keep it last in the list so that nothing else messes - // with it! - 'prettier', - ], - rules: { - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_', - // TODO(STENCIL-452): Investigate using eslint-plugin-react to remove the need for varsIgnorePattern - varsIgnorePattern: '^(h|Fragment)$', - }, - ], - /** - * Configuration for Jest rules can be found here: - * https://github.com/jest-community/eslint-plugin-jest/tree/main/docs/rules - */ - 'jest/expect-expect': [ - 'error', - { - // we set this to `expect*` so that any function whose name starts with expect will be counted - // as an assertion function, allowing us to use functions to DRY up test suites. - assertFunctionNames: ['expect*'], - }, - ], - // we...have a number of things disabled :) - // TODO(STENCIL-488): Turn this rule back on once there are no violations of it remaining - 'jest/no-disabled-tests': ['off'], - // we use this in enough places that we don't want to do per-line disables - 'jest/no-conditional-expect': ['off'], - // this enforces that Jest hooks (e.g. `beforeEach`) are declared in test files in their execution order - // see here for details: https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/prefer-hooks-in-order.md - 'jest/prefer-hooks-in-order': ['warn'], - // this enforces that Jest hooks (e.g. `beforeEach`) are declared at the top of `describe` blocks - 'jest/prefer-hooks-on-top': ['warn'], - /** - * Configuration for the JSDoc plugin rules can be found at: - * https://github.com/gajus/eslint-plugin-jsdoc - */ - // validates that the name immediately following `@param` matches the parameter name in the function signature - // this works in conjunction with "jsdoc/require-param" - 'jsdoc/check-param-names': [ - 'error', - { - // if `checkStructured` is `true`, it asks that the JSDoc describe the fields being destructured. - // turn this off to not leak function internals/discourage describing them - checkDestructured: false, - }, - ], - // require that jsdoc attached to a method/function require one `@param` per parameter - 'jsdoc/require-param': [ - 'error', - { - // if `checkStructured` is `true`, it asks that the JSDoc describe the fields being destructured. - // turn this off to not leak function internals/discourage describing them - checkDestructured: false, - // always check setters as they should require a parameter (by definition) - checkSetters: true, - }, - ], - 'jsdoc/require-param-description': ['error'], - // rely on TypeScript types to be the source of truth, minimize verbosity in comments - 'jsdoc/require-param-type': ['off'], - 'jsdoc/require-returns': ['error'], - 'jsdoc/require-returns-check': ['error'], - 'jsdoc/require-returns-description': ['error'], - // rely on TypeScript types to be the source of truth, minimize verbosity in comments - 'jsdoc/require-returns-type': ['off'], - 'no-cond-assign': 'error', - 'no-var': 'error', - 'prefer-const': 'error', - 'prefer-rest-params': 'error', - 'prefer-spread': 'error', - 'simple-import-sort/exports': 'error', - 'simple-import-sort/imports': 'error', - }, - overrides: [ - { - // the stencil entry point still uses `var`, ignore errors related to it - files: 'bin/**', - rules: { - 'no-var': 'off', - }, - }, - { - // we don't want to use jest-related lint rules in the wdio tests - files: 'test/wdio/**/*.tsx', - rules: { - 'jest/expect-expect': 'off', - 'wdio/await-expect': 'error', - }, - }, - ], - // inform ESLint about the global variables defined in a Jest context - // see https://github.com/jest-community/eslint-plugin-jest/#usage - env: { - 'jest/globals': true, - }, -}; diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 3c4e5ccbc7c..3f4072a1824 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,133 +1,72 @@ # Stencil Continuous Integration (CI) -Continuous integration (CI) is an important aspect of any project, and is used to verify and validate the changes to the -codebase work as intended, to avoid introducing regressions (bugs), and to adhere to coding standards (e.g. formatting -rules). It provides a consistent means of performing a series of checks over the entire codebase on behalf of the team. - -This document explains Stencil's CI setup. +This document explains Stencil's CI setup for the v5 monorepo. ## CI Environment -Stencil's CI system runs on GitHub Actions. -GitHub Actions allow developers to declare a series of _workflows_ to run following an _event_ in the repository, or on -a set schedule. - -The workflows that are run as a part of Stencil's CI process are declared as YAML files, and are stored in the same -directory as this file. -Each workflow file is explained in greater depth in the [workflows section](#workflows) of this document. +Stencil's CI runs on GitHub Actions using pnpm and supports Node.js 22 and 24. -## Workflows +## Workflow Structure -This section describes each of Stencil's GitHub Actions workflows. -Each of these tasks below are codified as [reusable workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows). +```mermaid +graph TD; + build[Build] + + build --> quality[Quality] + build --> unit[Unit Tests] + build --> test-build[Build Tests] + build --> test-integration[Integration Tests] + build --> test-runtime[Runtime Tests] + build --> test-special-config[Special Config Tests] + build --> test-ssr[SSR Tests] + build --> test-starter[Component Starter] +``` -Generally speaking, workflows are designed to be declarative in nature. -As such, this section does not intend to duplicate the details of each workflow, but rather give a high level overview -of each one and mention nuances of each. +## Workflows ### Main (`main.yml`) -The main workflow for Stencil can be found in `main.yml` in this directory. -This workflow is the entrypoint of Stencil's CI system, and initializes every workflow & job that runs. +The orchestrator workflow that runs on push to `main`/`v5` branches and on pull requests. ### Build (`build.yml`) -This workflow is responsible for building Stencil and validating the resultant artifact. - -### Format (`format.yml`) - -This workflow is responsible for validating that the code adheres to the Stencil team's formatting configuration before -a pull request is merged. - -### Dev Release (`release-dev.yml`) - -This workflow initiates a developer build of Stencil from the `main` branch. -It is intended to be manually invoked by a member of the Stencil team. - -### Nightly Release (`release-nightly.yml`) - -This workflow initiates a nightly build of Stencil from the `main` branch. -A nightly build is similar to a 'Dev Release', except that: -- it is run on a set cadence (it is not expectedthat a developer to manually invoke it) -- it is published to the npm registry under the 'nightly' tag - -### Test Analysis (`test-analysis.yml`) - -This workflow is responsible for running the Stencil analysis testing suite. +Builds all packages and uploads artifacts for downstream jobs. -### Test End-to-End (`test-e2e.yml`) +### Quality (`quality.yml`) -This workflow is responsible for running the Stencil end-to-end testing suite. -This suite does _not_ run Stencil's BrowserStack tests. -Those are handled by a [separate workflow](#browserstack-browserstackyml). +Runs quality checks (Linux only): +- `pnpm format:check` - Code formatting (oxfmt) +- `pnpm lint:check` - Linting (oxlint) +- `pnpm typecheck` - TypeScript type checking +- `pnpm knip` - Unused code detection -### Test Unit (`test-unit.yml`) +### Test Workflows -This workflow is responsible for running the Stencil unit testing suite. +| Workflow | Matrix | Description | +|----------|--------|-------------| +| `test-unit.yml` | Linux | Unit tests for packages (`pnpm test`) | +| `test-build.yml` | Linux/Windows × Node 22/24 | Build test suite (`test/build`) | +| `test-integration.yml` | Linux/Windows × Node 22/24 | Integration tests (`test/integration`) | +| `test-runtime.yml` | Linux/Windows × Node 22/24 | Runtime tests (`test/runtime`) | +| `test-special-config.yml` | Linux/Windows × Node 22/24 | Special config tests (`test/special-config`) | +| `test-ssr.yml` | Linux/Windows × Node 22/24 | SSR tests (`test/ssr`) | +| `test-component-starter.yml` | Linux/Windows × Node 22/24 | Smoke test with component starter template | -### WebdriverIO Tests (`test-wdio.yml`) +## Release Workflows -This workflow runs our integration tests which assert that various Stencil -features work correctly when components using them are built and then rendered -in actual browsers. We run these tests using -[WebdriverIO](https://webdriver.io/) against Firefox, Chrome, and Edge. - -For more information on how those tests are set up please see the [WebdriverIO -test README](../../test/wdio/README.md). - -### Design - -#### Overview - -Most of the workflows above are contingent on the build finishing (otherwise there would be nothing to run against). -The diagram below displays the dependencies between each workflow. - -```mermaid -graph LR; - build-core-->test-analysis; - build-core-->test-e2e; - build-core-->test-unit; - format; -``` - -Making each 'task' a reusable workflow allows CI to run more jobs in parallel, improving the throughput of Stencil's CI. -All resusable workflows can be found in the [workflows directory](.). -This is a GitHub Actions convention that cannot be overridden. - -#### Running Tests - -All test-related jobs require the build to finish first. -Upon successful completion of the build workflow, each test workflow will start. - -The test-running workflows have been designed to run in parallel and are configured to run against several operating -systems & versions of node. -For a test workflow that theoretically runs on Ubuntu and Windows operating systems and targets Node v14, v16 and v18, a -single test workflow may spawn several jobs: - -```mermaid -graph LR; - test-analysis-->ubuntu-node14; - test-analysis-->ubuntu-node16; - test-analysis-->ubuntu-node18; - test-analysis-->windows-node14; - test-analysis-->windows-node16; - test-analysis-->windows-node18; -``` +Release workflows are managed separately and support both v4 (legacy) and v5 (monorepo with changesets). -These 'os-node jobs' (e.g. `ubuntu-node16`) are designed to _not_ prematurely stop their sibling jobs should one of -them fail. -This allows the opportunity for the sibling test jobs to potentially pass, and reduce the number of runners that need to -be spun up again should a developer wish to 're-run failed jobs'. -Should a developer feel that it is more appropriate to re-run all os-node jobs, they may do so using GitHub's 're-run -all jobs' options in the GitHub Actions UI. +| Workflow | Description | +|----------|-------------| +| `release-dev.yml` | Developer builds from main | +| `release-nightly.yml` | Nightly builds | +| `release-production.yml` | Production releases | +| `publish-npm.yml` | NPM publishing | -#### Concurrency +## Test Matrix -When a `git push` is made to a branch, Stencil's CI is designed to stop existing job(s) associated with the workflow + -branch. -A new CI run (of each workflow) will begin upon stopping the existing job(s) using the new `HEAD` of the branch. +Integration test workflows use `fail-fast: false` so sibling jobs continue even if one fails. This reduces the need to re-run all jobs when investigating failures. -## Repository Configuration +## Concurrency -Each of the workflows described in the [workflows section](#workflows) of this document must be configured in the -Stencil GitHub repository to be _required_ to pass in order to land code in the `main` branch. \ No newline at end of file +When a `git push` is made to a branch, existing CI jobs for that branch are cancelled and a new run begins. diff --git a/.github/workflows/actions/check-git-context/action.yml b/.github/workflows/actions/check-git-context/action.yml deleted file mode 100644 index f7907ac7ef3..00000000000 --- a/.github/workflows/actions/check-git-context/action.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: 'Check Git Context' -description: 'checks for a dirty git context, failing if the context is dirty' -runs: - using: composite - steps: - - name: Git status check - # here we check that there are no changed / new files. - # we use `git status`, grep out the build zip used throughout CI, - # and check if there are more than 0 lines in the output. - run: if [[ $(git status --short | grep -c -v stencil-core-build.zip) -ne 0 ]]; then STATUS=$(git status --verbose); printf "%s" "$STATUS"; git diff | cat; exit 1; fi - shell: bash diff --git a/.github/workflows/actions/download-archive/action.yml b/.github/workflows/actions/download-archive/action.yml deleted file mode 100644 index 26d92589ba5..00000000000 --- a/.github/workflows/actions/download-archive/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Stencil Archive Download' -description: 'downloads and decompresses an archive from a previous job' -inputs: - path: - description: 'location to decompress the archive to' - filename: - description: 'the name of the decompressed artifact' - name: - description: 'name of the archive to decompress' -runs: - using: 'composite' - steps: - - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 - with: - name: ${{ inputs.name }} - path: ${{ inputs.path }} - - - name: Extract Archive - run: unzip -q -o ${{ inputs.path }}/${{ inputs.filename }} -d ${{ inputs.path }} - shell: bash diff --git a/.github/workflows/actions/get-core-dependencies/action.yml b/.github/workflows/actions/get-core-dependencies/action.yml index 186ae4e2053..d0bc8e04b2a 100644 --- a/.github/workflows/actions/get-core-dependencies/action.yml +++ b/.github/workflows/actions/get-core-dependencies/action.yml @@ -1,20 +1,17 @@ name: 'Get Core Dependencies' -description: 'sets the node version & initializes core dependencies' +description: 'Sets up pnpm, node version & installs dependencies' runs: using: composite steps: - # this overrides previous versions of the node runtime that was set. - # jobs that need a different version of the Node runtime should explicitly - # set their node version after running this step + - name: Setup pnpm + uses: pnpm/action-setup@v4 + - name: Use Node Version from Volta - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './package.json' - cache: 'npm' + cache: 'pnpm' - name: Install Dependencies - run: | - npm ci \ - && npm run install.jest - + run: pnpm install --frozen-lockfile shell: bash diff --git a/.github/workflows/actions/install-playwright/action.yml b/.github/workflows/actions/install-playwright/action.yml new file mode 100644 index 00000000000..0298ba34e33 --- /dev/null +++ b/.github/workflows/actions/install-playwright/action.yml @@ -0,0 +1,37 @@ +name: 'Install Playwright Browsers' +description: 'Installs Playwright browsers with caching' +inputs: + working-directory: + description: 'Directory containing the Playwright installation to use' + required: false + default: 'test/ssr' +runs: + using: composite + steps: + - name: Get Playwright version + id: playwright-version + working-directory: ${{ inputs.working-directory }} + run: echo "version=$(npx playwright --version)" >> $GITHUB_OUTPUT + shell: bash + + - name: Cache Playwright browsers + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + ~/Library/Caches/ms-playwright + ~/AppData/Local/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + working-directory: ${{ inputs.working-directory }} + run: npx playwright install --with-deps chromium + shell: bash + + - name: Install Playwright system deps (cache hit) + if: steps.playwright-cache.outputs.cache-hit == 'true' && runner.os == 'Linux' + working-directory: ${{ inputs.working-directory }} + run: npx playwright install-deps chromium + shell: bash diff --git a/.github/workflows/actions/upload-archive/action.yml b/.github/workflows/actions/upload-archive/action.yml deleted file mode 100644 index 8142eefee8f..00000000000 --- a/.github/workflows/actions/upload-archive/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Stencil Archive Upload' -description: 'compresses and uploads an archive to be reused across jobs' -inputs: - paths: - description: 'paths to files or directories to archive (recursive)' - output: - description: 'output file name' - name: - description: 'name of the archive to upload' -runs: - using: 'composite' - steps: - - name: Create Archive - run: zip -q -r ${{ inputs.output }} ${{ inputs.paths }} - shell: bash - - - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 - with: - name: ${{ inputs.name }} - path: ${{ inputs.output }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c1041d7456..7e19b573afe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,43 +1,30 @@ -name: Build Stencil +name: Build on: workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows permissions: contents: read jobs: - build_core: - name: Core - strategy: - matrix: - os: ['ubuntu-22.04', 'windows-latest'] - runs-on: ${{ matrix.os }} + build: + name: Build + runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Get Core Dependencies uses: ./.github/workflows/actions/get-core-dependencies - - name: Core Build - run: npm run build -- --ci - shell: bash - - - name: Validate Build - run: npm run test.dist - shell: bash - - - name: Validate Testing - run: npm run test.testing - shell: bash + - name: Build + run: pnpm build - name: Upload Build Artifacts - if: ${{ matrix.os == 'ubuntu-22.04' }} - uses: ./.github/workflows/actions/upload-archive + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: stencil-core - output: stencil-core-build.zip - paths: cli compiler dev-server internal mock-doc scripts/build screenshot sys testing + name: stencil-build + path: | + packages/*/dist/ + packages/*/bin/ + retention-days: 1 diff --git a/.github/workflows/create-production-pr.yml b/.github/workflows/create-production-pr.yml index 0f9f728f57f..9ce8f78166a 100644 --- a/.github/workflows/create-production-pr.yml +++ b/.github/workflows/create-production-pr.yml @@ -21,7 +21,6 @@ on: default: main options: - main - - v3-maintenance jobs: create-stencil-release-pull-request: @@ -49,7 +48,6 @@ jobs: - name: Get Core Dependencies uses: ./.github/workflows/actions/get-core-dependencies - # TODO(STENCIL-927): Backport changes to the v3 branch - name: Run Publish Preparation Script run: npm run release.ci.prepare -- --version ${{ inputs.version }} shell: bash diff --git a/.github/workflows/lint-and-format.yml b/.github/workflows/lint-and-format.yml deleted file mode 100644 index 299f384e976..00000000000 --- a/.github/workflows/lint-and-format.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Lint and Format Stencil (Check) - -on: - merge_group: - workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows - -permissions: - contents: read - -jobs: - format: - name: Check - runs-on: 'ubuntu-22.04' - steps: - - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: Get Core Dependencies - uses: ./.github/workflows/actions/get-core-dependencies - - - name: ESLint - run: npm run lint - - - name: Prettier Check - run: npm run prettier.dry-run - shell: bash - - - name: Spellcheck - run: npm run spellcheck diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 939712874a6..e7fc1f6392c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,7 @@ on: push: branches: - 'main' - - 'stencil/v4-dev' + - 'v5' pull_request: branches: - '**' @@ -18,55 +18,53 @@ permissions: contents: read jobs: - build_core: + # Build runs first + build: name: Build uses: ./.github/workflows/build.yml - lint_and_format: - name: Lint and Format - uses: ./.github/workflows/lint-and-format.yml + # Everything else runs in parallel after build + quality: + name: Quality + needs: [build] + uses: ./.github/workflows/quality.yml - type_tests: - name: Type Tests - needs: [build_core] - uses: ./.github/workflows/test-types.yml + unit_tests: + name: Unit Tests + needs: [build] + uses: ./.github/workflows/test-unit.yml - analysis_tests: - name: Analysis Tests - needs: [build_core] - uses: ./.github/workflows/test-analysis.yml + build_tests: + name: Build Tests + needs: [build] + uses: ./.github/workflows/test-build.yml - docs_build_tests: - name: Docs Build Tests - needs: [build_core] - uses: ./.github/workflows/test-docs-build.yml + integration_tests: + name: Integration Tests + needs: [build] + uses: ./.github/workflows/test-integration.yml - bundler_tests: - name: Bundler Tests - needs: [build_core] - uses: ./.github/workflows/test-bundlers.yml + runtime_tests: + name: Runtime Tests + needs: [build] + uses: ./.github/workflows/test-runtime.yml - copytask_tests: - name: Copy Task Tests - needs: [build_core] - uses: ./.github/workflows/test-copytask.yml + special_config_tests: + name: Special Config Tests + needs: [build] + uses: ./.github/workflows/test-special-config.yml + + ssr_tests: + name: SSR Tests + needs: [build] + uses: ./.github/workflows/test-ssr.yml + + ssr_wasm_tests: + name: SSR WASM Tests + needs: [build] + uses: ./.github/workflows/test-ssr-wasm.yml component_starter_tests: name: Component Starter Smoke Test - needs: [build_core] + needs: [build] uses: ./.github/workflows/test-component-starter.yml - - e2e_tests: - name: E2E Tests - needs: [build_core] - uses: ./.github/workflows/test-e2e.yml - - unit_tests: - name: Unit Tests - needs: [build_core] - uses: ./.github/workflows/test-unit.yml - - wdio_tests: - name: WebdriverIO Tests - needs: [build_core] - uses: ./.github/workflows/test-wdio.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 00000000000..58603dbe166 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,39 @@ +name: Quality + +on: + workflow_call: + +permissions: + contents: read + +jobs: + quality: + name: Quality + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Get Core Dependencies + uses: ./.github/workflows/actions/get-core-dependencies + + - name: Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: stencil-build + path: packages/ + + - name: Format Check + run: pnpm format:check + + - name: Lint Check + run: pnpm lint:check + + - name: Type Check + run: pnpm typecheck + + - name: Knip + run: pnpm knip + + - name: Spellcheck + run: pnpm spellcheck diff --git a/.github/workflows/release-orchestrator.yml b/.github/workflows/release-orchestrator.yml index 3c4452fc506..89a7eadb54e 100644 --- a/.github/workflows/release-orchestrator.yml +++ b/.github/workflows/release-orchestrator.yml @@ -41,7 +41,6 @@ on: default: main options: - main - - v3-maintenance - v5 permissions: diff --git a/.github/workflows/release-production.yml b/.github/workflows/release-production.yml index 0ad0b13f824..80937a96364 100644 --- a/.github/workflows/release-production.yml +++ b/.github/workflows/release-production.yml @@ -10,7 +10,7 @@ on: base: required: true type: string - description: Which base branch should be targeted? (main or v3-maintenance) + description: Which base branch should be targeted? (main or v5) default: main permissions: diff --git a/.github/workflows/test-analysis.yml b/.github/workflows/test-analysis.yml deleted file mode 100644 index 2091967d2c6..00000000000 --- a/.github/workflows/test-analysis.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Analysis Tests - -on: - workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows - -permissions: - contents: read - -jobs: - analysis_test: - name: (${{ matrix.os }}.${{ matrix.node }}) - strategy: - fail-fast: false - matrix: - node: ['20', '22'] - os: ['ubuntu-latest', 'windows-latest'] - runs-on: ${{ matrix.os }} - steps: - - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: Get Core Dependencies - uses: ./.github/workflows/actions/get-core-dependencies - - - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 - with: - node-version: ${{ matrix.node }} - cache: 'npm' - - - name: Download Build Archive - uses: ./.github/workflows/actions/download-archive - with: - name: stencil-core - path: . - filename: stencil-core-build.zip - - - name: Bundle Size Test - run: npm run test.bundle-size - shell: bash - - - name: Check Git Context - uses: ./.github/workflows/actions/check-git-context diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml new file mode 100644 index 00000000000..cfeb1350a97 --- /dev/null +++ b/.github/workflows/test-build.yml @@ -0,0 +1,38 @@ +name: Build Tests + +on: + workflow_call: + +permissions: + contents: read + +jobs: + test_build: + name: Build (${{ matrix.os }}, node ${{ matrix.node }}) + strategy: + fail-fast: false + matrix: + node: ['22', '24'] + os: ['ubuntu-latest', 'windows-latest'] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Get Core Dependencies + uses: ./.github/workflows/actions/get-core-dependencies + + - name: Use Node ${{ matrix.node }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node }} + + - name: Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: stencil-build + path: packages/ + + - name: Build Tests + run: pnpm --filter "@stencil-core-tests/build" test + shell: bash diff --git a/.github/workflows/test-bundlers.yml b/.github/workflows/test-bundlers.yml deleted file mode 100644 index 3a80a6cbd30..00000000000 --- a/.github/workflows/test-bundlers.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Bundler Tests - -on: - workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows - -permissions: - contents: read - -jobs: - bundler_tests: - name: Verify Bundlers - runs-on: 'ubuntu-22.04' - steps: - - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: Get Core Dependencies - uses: ./.github/workflows/actions/get-core-dependencies - - - name: Download Build Archive - uses: ./.github/workflows/actions/download-archive - with: - name: stencil-core - path: . - filename: stencil-core-build.zip - - - name: Bundler Tests - run: npm run test.bundlers - shell: bash - - - name: Check Git Context - uses: ./.github/workflows/actions/check-git-context diff --git a/.github/workflows/test-component-starter.yml b/.github/workflows/test-component-starter.yml index 9d03188cc23..5f591bceac6 100644 --- a/.github/workflows/test-component-starter.yml +++ b/.github/workflows/test-component-starter.yml @@ -2,76 +2,45 @@ name: Component Starter Smoke Test on: workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows permissions: contents: read jobs: - analysis_test: - name: (${{ matrix.os }}.node-${{ matrix.node }}) + component_starter: + name: Component Starter (${{ matrix.os }}, node ${{ matrix.node }}) strategy: fail-fast: false matrix: - node: ['20', '22'] + node: ['22', '24'] os: ['ubuntu-latest', 'windows-latest'] runs-on: ${{ matrix.os }} steps: - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Get Core Dependencies uses: ./.github/workflows/actions/get-core-dependencies - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node }} - cache: 'npm' - - name: Create Pack Directory - # `mkdir` will fail if this directory already exists. - # in the next steps, we'll immediately put the packed build archive in this directory. - # between that and excluding `*.tgz` files in `.gitignore`, that _should_ make it safe enough for us to later - # use `mv` to rename the `npm pack`ed tarball - run: mkdir stencil-pack-destination - shell: bash - - - name: Download Build Archive - uses: ./.github/workflows/actions/download-archive + - name: Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: stencil-core - path: ./stencil-pack-destination - filename: stencil-core-build.zip - - - name: Copy package.json - # need `package.json` in order to run `npm pack` - run: cp package.json ./stencil-pack-destination - shell: bash - - - name: Copy bin - # `bin/` isn't a part of the compiled output (therefore not in the build archive). - # we need this entrypoint for stencil to run. - run: cp -R bin ./stencil-pack-destination - shell: bash - - - name: Remove node_modules - # clear out our local `node_modules/` so that they're not linked to in any way when `npm pack` is run - run: rm -rf node_modules/ - shell: bash + name: stencil-build + path: packages/ - - name: Pack the Build Archive - run: npm pack - working-directory: ./stencil-pack-destination + - name: Pack @stencil/core + run: pnpm pack --pack-destination ../../ + working-directory: ./packages/core shell: bash - - name: Move the Stencil Build Artifact - # there isn't a great way to get the output of `npm pack`, just grab the most recent from our destination - # directory and hope for the best. - # - # we don't set the working-directory here to avoid having to deal with relative paths in the destination arg - run: mv $(ls -t stencil-pack-destination/*.tgz | head -1) stencil-eval.tgz + - name: Pack @stencil/cli + run: pnpm pack --pack-destination ../../ + working-directory: ./packages/cli shell: bash - name: Initialize Component Starter @@ -79,17 +48,24 @@ jobs: shell: bash - name: Install Component Starter Dependencies - run: npm install + run: npm install --legacy-peer-deps working-directory: ./tmp-component-starter shell: bash - - name: Install Stencil Eval - run: npm i ../stencil-eval.tgz + - name: Install Stencil from Pack + run: npm install ../stencil-cli-*.tgz ../stencil-core-*.tgz --legacy-peer-deps working-directory: ./tmp-component-starter shell: bash - name: Install Playwright Browsers - run: npx playwright install + uses: ./.github/workflows/actions/install-playwright + with: + working-directory: ./tmp-component-starter + + - name: Run Stencil Migrations + run: npx stencil migrate + working-directory: ./tmp-component-starter + shell: bash - name: Build Starter Project run: npm run build @@ -97,40 +73,6 @@ jobs: shell: bash - name: Test Starter Project - run: npm run test -- --no-build # the project was just built, don't build it again + run: npm run test -- --no-build working-directory: ./tmp-component-starter shell: bash - - # TEMPORARILY DISABLE - # Disable until we update the generate task in v5 to work with new testing setup. - - # - name: Test npx stencil generate - # # `stencil generate` doesn't have a way to skip file generation, so we provide it with a component name and run - # # `echo` with a newline to select "all files" to generate (and use -e to interpret that backslash for a newline) - # run: echo -e '\n' | npm run generate -- hello-world - # working-directory: ./tmp-component-starter - # shell: bash - - # - name: Verify Files Exist - # run: | - # file_list=( - # src/components/hello-world/hello-world.tsx - # src/components/hello-world/hello-world.css - # src/components/hello-world/test/hello-world.spec.tsx - # src/components/hello-world/test/hello-world.e2e.ts - # ) - # for file in "${file_list[@]}"; do - # if [ -f "$file" ]; then - # echo "File '$file' exists." - # else - # echo "File '$file' does not exist." - # exit 1 - # fi - # done - # working-directory: ./tmp-component-starter - # shell: bash - - # - name: Test Generated Files - # run: npm run test - # working-directory: ./tmp-component-starter - # shell: bash diff --git a/.github/workflows/test-copytask.yml b/.github/workflows/test-copytask.yml deleted file mode 100644 index 9ee47f28eb8..00000000000 --- a/.github/workflows/test-copytask.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Copy Task Tests - -on: - workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows - -permissions: - contents: read - -jobs: - bundler_tests: - name: Verify Copy Task - runs-on: 'ubuntu-22.04' - steps: - - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: Get Core Dependencies - uses: ./.github/workflows/actions/get-core-dependencies - - - name: Download Build Archive - uses: ./.github/workflows/actions/download-archive - with: - name: stencil-core - path: . - filename: stencil-core-build.zip - - - name: Bundler Tests - run: npm run test.copytask - shell: bash - - - name: Check Git Context - uses: ./.github/workflows/actions/check-git-context diff --git a/.github/workflows/test-docs-build.yml b/.github/workflows/test-docs-build.yml deleted file mode 100644 index 2a6ab771304..00000000000 --- a/.github/workflows/test-docs-build.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Docs OT Build Tests - -on: - workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows - -permissions: - contents: read - -jobs: - docs_build_test: - name: (${{ matrix.os }}.${{ matrix.node }}) - strategy: - fail-fast: false - matrix: - node: ['20', '22'] - os: ['ubuntu-latest', 'windows-latest'] - runs-on: ${{ matrix.os }} - steps: - - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: Get Core Dependencies - uses: ./.github/workflows/actions/get-core-dependencies - - - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 - with: - node-version: ${{ matrix.node }} - cache: 'npm' - - - name: Download Build Archive - uses: ./.github/workflows/actions/download-archive - with: - name: stencil-core - path: . - filename: stencil-core-build.zip - - - name: Docs Build Tests - run: npm run test.docs-build - shell: bash - - - name: Check Git Context - uses: ./.github/workflows/actions/check-git-context diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml deleted file mode 100644 index 5c7cd91d057..00000000000 --- a/.github/workflows/test-e2e.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: E2E Tests - -on: - workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows - -permissions: - contents: read - -jobs: - e2e_test: - name: (${{ matrix.os }}.${{ matrix.node }}) - strategy: - fail-fast: false - matrix: - node: ['20', '22'] - os: ['ubuntu-latest', 'windows-latest'] - runs-on: ${{ matrix.os }} - steps: - - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: Get Core Dependencies - uses: ./.github/workflows/actions/get-core-dependencies - - - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 - with: - node-version: ${{ matrix.node }} - cache: 'npm' - - - name: Download Build Archive - uses: ./.github/workflows/actions/download-archive - with: - name: stencil-core - path: . - filename: stencil-core-build.zip - - - name: End-to-End Tests - uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # v3.0.0 - with: - timeout_minutes: 10 - max_attempts: 3 - command: npm run test.end-to-end -- --ci - - - name: Check Git Context - uses: ./.github/workflows/actions/check-git-context diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml new file mode 100644 index 00000000000..b80fb036c96 --- /dev/null +++ b/.github/workflows/test-integration.yml @@ -0,0 +1,41 @@ +name: Integration Tests + +on: + workflow_call: + +permissions: + contents: read + +jobs: + test_integration: + name: Integration (${{ matrix.os }}, node ${{ matrix.node }}) + strategy: + fail-fast: false + matrix: + node: ['22', '24'] + os: ['ubuntu-latest', 'windows-latest'] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Get Core Dependencies + uses: ./.github/workflows/actions/get-core-dependencies + + - name: Use Node ${{ matrix.node }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node }} + + - name: Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: stencil-build + path: packages/ + + - name: Install Playwright Browsers + uses: ./.github/workflows/actions/install-playwright + + - name: Integration Tests + run: pnpm --filter "@stencil-core-tests/integration" test + shell: bash diff --git a/.github/workflows/test-runtime.yml b/.github/workflows/test-runtime.yml new file mode 100644 index 00000000000..cb2f412ca2f --- /dev/null +++ b/.github/workflows/test-runtime.yml @@ -0,0 +1,41 @@ +name: Runtime Tests + +on: + workflow_call: + +permissions: + contents: read + +jobs: + test_runtime: + name: Runtime (${{ matrix.os }}, node ${{ matrix.node }}) + strategy: + fail-fast: false + matrix: + node: ['22', '24'] + os: ['ubuntu-latest', 'windows-latest'] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Get Core Dependencies + uses: ./.github/workflows/actions/get-core-dependencies + + - name: Use Node ${{ matrix.node }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node }} + + - name: Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: stencil-build + path: packages/ + + - name: Install Playwright Browsers + uses: ./.github/workflows/actions/install-playwright + + - name: Runtime Tests + run: pnpm --filter "@stencil-core-tests/runtime" test + shell: bash diff --git a/.github/workflows/test-special-config.yml b/.github/workflows/test-special-config.yml new file mode 100644 index 00000000000..4b5e446dc86 --- /dev/null +++ b/.github/workflows/test-special-config.yml @@ -0,0 +1,41 @@ +name: Special Config Tests + +on: + workflow_call: + +permissions: + contents: read + +jobs: + test_special_config: + name: Special Config (${{ matrix.os }}, node ${{ matrix.node }}) + strategy: + fail-fast: false + matrix: + node: ['22', '24'] + os: ['ubuntu-latest', 'windows-latest'] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Get Core Dependencies + uses: ./.github/workflows/actions/get-core-dependencies + + - name: Use Node ${{ matrix.node }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node }} + + - name: Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: stencil-build + path: packages/ + + - name: Install Playwright Browsers + uses: ./.github/workflows/actions/install-playwright + + - name: Special Config Tests + run: pnpm --filter "@stencil-core-tests/special-config" test + shell: bash diff --git a/.github/workflows/test-ssr-wasm.yml b/.github/workflows/test-ssr-wasm.yml new file mode 100644 index 00000000000..2b800d7536d --- /dev/null +++ b/.github/workflows/test-ssr-wasm.yml @@ -0,0 +1,53 @@ +name: SSR WASM Tests + +on: + workflow_call: + +permissions: + contents: read + +jobs: + test_ssr_wasm: + name: SSR WASM (${{ matrix.os }}, node ${{ matrix.node }}) + strategy: + fail-fast: false + matrix: + node: ['22', '24'] + os: ['ubuntu-latest'] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Get Core Dependencies + uses: ./.github/workflows/actions/get-core-dependencies + + - name: Use Node ${{ matrix.node }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node }} + + - name: Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: stencil-build + path: packages/ + + - name: Install Binaryen (Ubuntu) + if: runner.os == 'Linux' + run: sudo apt-get install -y binaryen + + - name: Install Binaryen (macOS) + if: runner.os == 'macOS' + run: brew install binaryen + + - name: Install extism-js + run: | + mkdir -p "$HOME/.local/bin" + export PATH="$HOME/.local/bin:$PATH" + curl -fsSL https://raw.githubusercontent.com/extism/js-pdk/main/install.sh | bash + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: SSR WASM Tests + run: pnpm --filter "@stencil-core-tests/ssr-wasm" test + shell: bash diff --git a/.github/workflows/test-ssr.yml b/.github/workflows/test-ssr.yml new file mode 100644 index 00000000000..f058517aec8 --- /dev/null +++ b/.github/workflows/test-ssr.yml @@ -0,0 +1,41 @@ +name: SSR Tests + +on: + workflow_call: + +permissions: + contents: read + +jobs: + test_ssr: + name: SSR (${{ matrix.os }}, node ${{ matrix.node }}) + strategy: + fail-fast: false + matrix: + node: ['22', '24'] + os: ['ubuntu-latest', 'windows-latest'] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Get Core Dependencies + uses: ./.github/workflows/actions/get-core-dependencies + + - name: Use Node ${{ matrix.node }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node }} + + - name: Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: stencil-build + path: packages/ + + - name: Install Playwright Browsers + uses: ./.github/workflows/actions/install-playwright + + - name: SSR Tests + run: pnpm --filter "@stencil-core-tests/ssr" test + shell: bash diff --git a/.github/workflows/test-types.yml b/.github/workflows/test-types.yml deleted file mode 100644 index ec695e06a2a..00000000000 --- a/.github/workflows/test-types.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Type Tests - -on: - workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows - -permissions: - contents: read - -jobs: - unit_test: - name: Type Tests - strategy: - fail-fast: false - matrix: - node: ['20', '22'] - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: Get Core Dependencies - uses: ./.github/workflows/actions/get-core-dependencies - - - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 - with: - node-version: ${{ matrix.node }} - cache: 'npm' - - - name: Download Build Archive - uses: ./.github/workflows/actions/download-archive - with: - name: stencil-core - path: . - filename: stencil-core-build.zip - - - name: Type Tests - run: npm run test.type-tests - shell: bash diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index fe117f0d9dc..d891ecee70e 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -2,44 +2,27 @@ name: Unit Tests on: workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows permissions: contents: read jobs: unit_test: - name: (${{ matrix.os }}.${{ matrix.node }}) - strategy: - fail-fast: false - matrix: - node: ['20', '22'] - os: ['ubuntu-latest', 'windows-latest'] - runs-on: ${{ matrix.os }} + name: Unit Tests + runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Get Core Dependencies uses: ./.github/workflows/actions/get-core-dependencies - - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + - name: Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - node-version: ${{ matrix.node }} - cache: 'npm' - - - name: Download Build Archive - uses: ./.github/workflows/actions/download-archive - with: - name: stencil-core - path: . - filename: stencil-core-build.zip + name: stencil-build + path: packages/ - name: Unit Tests - run: npm run test.jest + run: pnpm test shell: bash - - - name: Check Git Context - uses: ./.github/workflows/actions/check-git-context diff --git a/.github/workflows/test-wdio.yml b/.github/workflows/test-wdio.yml deleted file mode 100644 index 80f25aef4da..00000000000 --- a/.github/workflows/test-wdio.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: WebdriverIO Tests - -on: - workflow_call: - # Make this a reusable workflow, no value needed - # https://docs.github.com/en/actions/using-workflows/reusing-workflows - -permissions: - contents: read - -jobs: - wdio_test: - name: Run WebdriverIO Component Tests (${{ matrix.browser }}) - runs-on: ubuntu-22.04 - strategy: - matrix: - # browser: [CHROME, FIREFOX, EDGE] - browser: [CHROME] - - steps: - - name: Checkout Code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - - name: Get Core Dependencies - uses: ./.github/workflows/actions/get-core-dependencies - - - name: Use Node Version from Volta - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 - with: - # pull the version to use from the volta key in package.json - node-version-file: './test/wdio/package.json' - cache: 'npm' - - - name: Download Build Archive - uses: ./.github/workflows/actions/download-archive - with: - name: stencil-core - path: . - filename: stencil-core-build.zip - - - name: Run WebdriverIO Component Tests - run: npm run test.wdio - shell: bash - env: - BROWSER: ${{ matrix.browser }} - - - name: Check Git Context - uses: ./.github/workflows/actions/check-git-context diff --git a/.gitignore b/.gitignore index ce2837e8f8d..8e7f6899a6a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,42 +17,16 @@ log.txt .sass-cache/ .versions/ node_modules/ -coverage/ - -/build/ -/scripts/build/ -dist/ - -# submodule packages -/build-conditionals -/cli -/compiler -/hydrate -/dev-server -/internal -/mock-doc -/polyfills -/runtime -/server -/sys -/testing - -/screenshot/index.js -/screenshot/index.js.map -/screenshot/package.json -/screenshot/pixel-match.js -/screenshot/pixel-match.js.map -/screenshot/*.d.ts test/**/www/* test/**/hydrate/* .stencil coverage/** -# TODO(STENCIL-446): Remove these once `strictNullChecks` is enabled -null_errors*.json -# TODO(STENCIL-454): Remove or change this up once we've eliminated unused exports -unused-exports*.txt - -# readme file from docs-readme that is expected to be missing so it will be emitted in full -test/docs-readme/custom-readme-output-overwrite-if-missing-missing/components/styleurls-component/readme.md +dist +www +hydrate +loader +test-results +__screenshots__ +.vitest-attachments \ No newline at end of file diff --git a/.npmrc b/.npmrc deleted file mode 100644 index ca42cd5199b..00000000000 --- a/.npmrc +++ /dev/null @@ -1,8 +0,0 @@ -# By default, Node allocates 2 GB for a process to run. -# When building Stencil, it may reach that point before the garbage collector is invoked, causing an out-of-memory -# related failure. -# If this value is changed, please ensure that it works both locally and in a continuous integration environment in -# a repeatable manner (i.e. it can run many times, one after the other, without failing due to out-of-memory errors). -node_options=--max-old-space-size=4096 -# TODO(STENCIL-1141): remove `PUPPETEER_DOWNLOAD_BASE_URL` once support for Node v16 is dropped -PUPPETEER_DOWNLOAD_BASE_URL=https://storage.googleapis.com/chrome-for-testing-public diff --git a/.nvmrc b/.nvmrc index 2c022021b85..a4a7a41bca7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22.13.0 +v24.14.0 diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000000..0d01c36a9f4 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,24 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "singleQuote": true, + "jsxSingleQuote": true, + "ignorePatterns": ["**/dist/**", "**/*.d.ts", "**/*.md"], + "sortImports": { + "groups": [ + "value-builtin", + "value-external", + "type-external", + { "newlinesBetween": true }, + "value-internal", + "type-internal", + { "newlinesBetween": true }, + "value-parent", + "value-sibling", + "value-index", + "type-parent", + "type-sibling", + "type-index" + ], + "newlinesBetween": false + } +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000000..455fb730fe7 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,56 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "import", "jsdoc"], + "categories": { + "correctness": "error", + "suspicious": "warn", + "pedantic": "off", + "style": "off", + "perf": "warn" + }, + "rules": { + "no-cond-assign": "error", + "no-var": "error", + "prefer-const": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "no-async-promise-executor": "off", + "no-control-regex": "off", + "no-await-in-loop": "off", + "no-new": "off", + "no-unmodified-loop-condition": "off", + "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/triple-slash-reference": "off", + "import/no-unassigned-import": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^(_.*|h|Fragment|IntrinsicElements)$" + } + ], + "jsdoc/require-param": [ + "error", + { + "checkDestructured": false, + "checkSetters": true + } + ], + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off" + }, + "ignorePatterns": [ + "node_modules/**", + "dist/**", + "**/dist/**", + "**/*.d.ts", + "**/*.spec.ts", + "**/_test_/**", + "packages/core/src/client/polyfills/**", + "packages/core/src/declarations/stencil-public-runtime.ts" + ] +} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 02f004900cf..00000000000 --- a/.prettierignore +++ /dev/null @@ -1,69 +0,0 @@ -# npm packages in the root of the project and subdirectories -node_modules/ - -# submodule packages -/build/ -/cli/ -/compiler/ -/dev-server/ -/internal/ -/mock-doc/ -/sys/ -/testing/ - -# shims that are attributed to external authors, and are minified out of the box -/src/client/polyfills/core-js.js -/src/client/polyfills/dom.js -/src/client/polyfills/es5-html-element.js -/src/client/polyfills/index.js -/src/client/polyfills/system.js - -# project notes shared with the community -/notes/ - -# output of building various scripts that support the project -/scripts/build/ - -# these files are intentionally incomplete JavaScript files (they are parts of an Immediately Invoked Funciton -# Expression (IIFE)). They act as a 'header' and 'footer' that get prepended and appended to the Stencil compiler. -# Ignore them so Prettier doesn't fail and the 'prettier-ignore' pragma doesn't get put in the compiler output. -/scripts/bundles/helpers/compiler-cjs-intro.js -/scripts/bundles/helpers/compiler-cjs-outro.js - -# code coverage output -coverage/ - -# output from compiling Stencil projects for testing purposes -test/**/dist/ -test/**/dist-react/ -test/**/hydrate/ -test/**/test-output/ -test/**/www/ -test/**/components.d.ts -test/end-to-end/screenshot/ -test/end-to-end/docs.d.ts -test/end-to-end/docs.json -test/docs-json/docs.d.ts -test/docs-json/docs.json - -# minified angular that exists in the test directory - -# generated screenshot files -/screenshot/index.js -/screenshot/package.json -/screenshot/pixel-match.js -/screenshot/*.d.ts - -# third party scripts -src/mock-doc/third-party/jquery.ts -test/wdio/slot-ng-if/assets/ - - -# wdio test output -test/wdio/test-components -test/wdio/test-components-no-external-runtime -test/wdio/www-global-script/ -test/wdio/www-prerender-script -test/wdio/www-invisible-prehydration/ -test/wdio/test-ts-target-output -test/wdio/test-components-autoloader \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 639e38d8557..cb6c38ad1ae 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,18 @@ } ], "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Current Vitest File", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", + "args": ["run", "${file}"], + "smartStep": true, + "console": "integratedTerminal", + "cwd": "${workspaceRoot}/packages/core" + }, { "type": "node", "request": "launch", diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..c50dbb01467 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,27 @@ +This is Stencil - a toolchain for building reusable, scalable Design Systems built with Custom Elements. + +This is a major version development branch - breaking changes are ok. + +Read the `./V5_PLANNING.md` file at session start for more details on the goals and plans for this major version. Add and amend this document as needed to keep track of the major version planning and progress. + +Always seek to replace code with more modern standards and more modern 3rd party dependencies where possible, and remove older code and dependencies that are no longer needed - but please discuss this with the user before doing so. + +User should not have to ask you for your opinion explicitly. Always evaluate what the user is asking you to do, and voice your concerns before proceeding if you don’t think it's a good idea. If possible, propose a better solution, but you can voice concerns even without one. + +This applies even to direct requests to revert or simplify. Still evaluate whether your original approach was better. The user may be missing important context. If there was a solid reasoning you suggested that approach, push back with reasoning instead of silently complying. + +Assume any package starting with `@stencil/` is potentially updatable and suggest changes if you think it would be beneficial. + +`as any` is very rarely an acceptable solution. Check with the user before using it, and use better alternatives whenever possible - don't be lazy. + +Never commit changes without the user explicitly asking you to. Always ask for confirmation before committing, and provide a clear summary of the changes that will be committed. If the user asks for changes after you’ve provided a summary but before you’ve committed, update the summary to reflect the new changes before asking for confirmation again. + +Keep all code comments terse as you can ... but don't delete existing comments without good reason. + +Generally, non-trivial changes should pass - +- `pnpm build` +- `pnpm typecheck` +- `pnpm test` +- `pnpm lint` + +To run a specific unit test: `pnpm -F PACKAGE_NAME test TEST_NAME` \ No newline at end of file diff --git a/V5_PLANNING.md b/V5_PLANNING.md new file mode 100644 index 00000000000..d7066213750 --- /dev/null +++ b/V5_PLANNING.md @@ -0,0 +1,221 @@ +# Stencil v5 Planning Document + +> **Living Document** - Track progress on v5 modernization + +## Vision + +Modernize Stencil after 10 years: shed tech debt, embrace modern tooling, simplify architecture, streamline user experience. + +--- + +## Major Goals + +### 1. 🧪 Remove Integrated Testing +**Status:** 📋 Replacement packages ready - need to remove integrated testing +- `@stencil/vitest` + `@stencil/playwright` audited and ready +- Still need to migrate Stencil's internal tests from jest to vitest +- Still migrating integration / e2e test suites (in `packages/core/tests/`) + +### 2. 🗑️ Update / Remove Legacy Features +**Status:** In Progress +- ES5 builds → ✅ REMOVED +- Internal CommonJS → ✅ REMOVED (Pure ESM, Node 18+) +- Ancient polyfills → ✅ REMOVED +- In-browser compilation → REMOVE +- `*-sys` in-memory file-system → Replace with TypeScript incremental APIs (see Tasks) +- Hand-crafted dev server / HMR → modernize as `@stencil/dev-server` + +### 3. 🔧 Build System +**Status:** ✅ Complete +- **tsdown** for all package builds (single config per package) +- **pnpm -r** for build orchestration (no Turborepo) + +### 4. 📦 Mono-repo Restructure +**Status:** ✅ Complete (dev-server pending) +- `packages/core/` (@stencil/core), `packages/cli/` (@stencil/cli), `packages/mock-doc/` (@stencil/mock-doc) + +### 5. 🔗 CLI/Core Dependency Architecture +**Status:** ✅ Complete +- Broke circular dependency between CLI and Core. Core standalone, CLI thin. + +### 6. Update Public Build Chain +**Status:** 📋 Planned +- Migrate from rollup to rolldown +- Potentially move from typescript to tsgo + +### 7. 📤 Output Target Modernization +**Status:** ✅ Complete +- Renamed output targets for clarity (`dist` → `loader-bundle`, `dist-custom-elements` → `standalone`, etc.) +- Elevated sub-outputs to first-class citizens (`types`, `collection`) +- See Breaking Changes for full details + +### 8. 📁 Global Styles & Assets Modernization +**Status:** ✅ Complete +- New `global-style` and `assets` output targets (first-class, auto-generated) +- Unified `dist/assets/` location shared by all outputs +- See Breaking Changes for full details + +### 9. 🏷️ Release Management: Changesets +**Status:** 📋 Planned +- Adopt [Changesets](https://github.com/changesets/changesets) for monorepo release management with lockstep versioning + +--- + +## Breaking Changes + +- `@stencil/core/internal` → `@stencil/core/runtime` +- `@stencil/core/internal/client` → `@stencil/core/runtime/client` +- `@stencil/core/internal/hydrate` → `@stencil/core/runtime/server` +- `@stencil/core/cli` → `@stencil/cli` +- `@stencil/core/dev-server` → `@stencil/dev-server` +- `openBrowser` now defaults to `false`. Override with `--open` flag or `openBrowser: true` in config. +- **Output target renames:** + - `dist` → `loader-bundle` (default dir: `dist/loader-bundle/`) + - `dist-custom-elements` → `standalone` (default dir: `dist/standalone/`) + - `dist-hydrate-script` → `ssr` (default dir: `dist/ssr/`) + - `dist-collection` (sub-output) → `collection` (first-class output, default dir: `dist/collection/`, auto-generated in prod) + - `dist-types` (sub-output) → `types` (first-class output, default dir: `dist/types/`, auto-generated in prod) + - `collectionDir` and `typesDir` config options removed from `loader-bundle` config + - Run `stencil migrate` to automatically update your config +- `loader-bundle` and `ssr` output targets no longer generate CJS bundles by default. Add `cjs: true` to your output target config to restore CJS output. +- **`streamToString()` return type changed** from Node.js `Readable` to web-standard `ReadableStream`. Works in Node 22+, Cloudflare Workers, Deno, Bun, and all WinterCG runtimes. +- `ssr` no longer generates a `package.json` file. Use `exports` in your library's main `package.json` to expose the SSR script. +- **ES5 build output removed.** The `buildEs5` config option, `--es5` CLI flag, and all ES5-related output have been removed. Stencil now targets ES2017+ only. IE11 and Edge 18 and below are no longer supported. +- **@Component decorator `shadow`, `scoped`, and `formAssociated` properties removed.** Use the new unified `encapsulation` property instead: + - `shadow: true` → `encapsulation: { type: 'shadow' }` + - `shadow: { delegatesFocus: true }` → `encapsulation: { type: 'shadow', delegatesFocus: true }` + - `scoped: true` → `encapsulation: { type: 'scoped' }` + - Default (no encapsulation) → `encapsulation: { type: 'none' }` (optional, 'none' is default) + - **New feature:** `encapsulation: { type: 'shadow', mode: 'closed' }` enables closed shadow DOM + - **New feature:** Per-component slot patches via `encapsulation: { type: 'scoped', patches: ['children', 'clone', 'insert'] }` + - `formAssociated: true` → Use `@AttachInternals()` decorator instead (auto-sets `formAssociated: true`) + - To use `@AttachInternals` without form association: `@AttachInternals({ formAssociated: false })` + - Run `stencil migrate --dry-run` to preview automatic migration, or `stencil migrate` to apply changes +- **`buildDist` and `buildDocs` config options removed.** Use `skipInDev` on individual output targets for granular control. +- **`--esm` CLI flag removed.** Configure `skipInDev` on output targets instead. +- **`--prod` CLI flag removed.** Production is the default. Use `--dev` to opt into a development build. +- **`devMode` config option removed from `stencil.config.ts`.** Build mode is now exclusively controlled by the `--dev` CLI flag. +- **`isPrimaryPackageOutputTarget` removed from output targets.** Package.json validation now auto-detects based on configured outputs. +- **`validatePrimaryPackageOutputTarget` config option renamed to `validatePackageJson`.** +- **Export maps generation uses smart defaults.** Priority: `loader-bundle` > `standalone` for the root export. Types always come from the `types` output target. +- **`collection` field in package.json renamed to `collection`.** +- **Output file extensions modernized:** + - ESM files now use `.js` extension (was `.esm.js`) + - CJS files now use `.cjs` extension (was `.cjs.js`) + - Backwards compat: forwarding module `.esm.js` generated for existing CDN consumers +- **Global styles and assets modernized:** + - New `global-style` output target (first-class, auto-generated when `globalStyle` config exists) + - New `assets` output target (first-class, auto-generated when components have `assetsDirs`) + - Unified location: `dist/assets/` for both global styles and component assets + - `copyAssets` option removed from `loader-bundle` and `www` output targets + - `extras.addGlobalStyleToComponents` removed - use `inject` option on `global-style` output target instead: + - `inject: 'none'` - don't inject, load stylesheet externally + - `inject: 'client'` - inject into components on client only + - `inject: 'all'` - inject into components on both client and SSR + - Auto-generated `global-style` (from `globalStyle` config) defaults to `inject: 'client'` (preserves v4 behavior) + - Explicitly configured `global-style` outputs default to `inject: 'none'` +- **`standalone` output target: `externalRuntime` now defaults to `false`**. The runtime is bundled as a shared local chunk rather than kept as an external `@stencil/core/runtime/client` import. Set `externalRuntime: true` if you need multiple Stencil component libraries on the same page to share a single runtime instance (e.g., for `setNonce`/`setTagTransformer` to propagate across libraries). +- **`esmLoaderPath` config option renamed to `loaderPath`** in `loader-bundle` output target. +- **`hashFileNames` and `hashedFileNameLength` moved from top-level config to `loader-bundle` and `www` output targets.** Only these two targets serve bundles directly in the browser. Run `stencil migrate` to remove them from the top-level config, then add to your output targets if non-default values are needed. + +--- + +## New Features + +- **`global-style` output target now supports explicit `input`** - specify CSS source file directly on output target instead of using `globalStyle` config +- **`global-style` output target now supports `fileName`** - customize output filename +- **`global-style` output target now supports `inject`** - control whether styles are injected into component shadow DOMs (`'none'`, `'client'`, `'all'`) +- **Multiple `global-style` outputs supported** - build separate CSS bundles from different input files, each with independent `inject` settings +- **`www` can now use standalone loader** +- **`@Component` now supports `globalStyleUrl` and `globalStyle`** — co-locate document-level styles with the component. Styles are collected at build time and injected wherever `@import "stencil-globals"` appears in a global stylesheet. Works for all encapsulation types (shadow, scoped, none). No mode variants — CSS handles runtime variants via selectors or custom properties. Changes to `globalStyleUrl` files invalidate the global style build cache and trigger HMR correctly. +- **`@import "stencil-hydrate"` virtual placeholder** — add to any `global-style` input to inject static FOUC-prevention CSS at build time instead of relying on the dynamic ` + `); @@ -30,7 +33,9 @@ describe('hydrate style element', () => { expect(clientHydrated.root).toEqualHtml(` - + `); }); diff --git a/src/runtime/test/initialize-component.spec.tsx b/packages/core/src/runtime/_test_/initialize-component.spec.tsx similarity index 84% rename from src/runtime/test/initialize-component.spec.tsx rename to packages/core/src/runtime/_test_/initialize-component.spec.tsx index 52df81e69fe..20c0f6bd0e9 100644 --- a/src/runtime/test/initialize-component.spec.tsx +++ b/packages/core/src/runtime/_test_/initialize-component.spec.tsx @@ -1,6 +1,7 @@ -import { getHostRef } from '@platform'; import { Component } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; +import { getHostRef } from 'virtual:platform'; import { HOST_FLAGS } from '../../utils'; diff --git a/src/runtime/test/jsx.spec.tsx b/packages/core/src/runtime/_test_/jsx.spec.tsx similarity index 94% rename from src/runtime/test/jsx.spec.tsx rename to packages/core/src/runtime/_test_/jsx.spec.tsx index 555806e51b7..60291aa9395 100644 --- a/src/runtime/test/jsx.spec.tsx +++ b/packages/core/src/runtime/_test_/jsx.spec.tsx @@ -1,7 +1,6 @@ import { Component, Fragment, h, Host, Prop, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; - -import { FunctionalComponent } from '../../declarations'; +import { expect, describe, it } from '@stencil/vitest'; describe('jsx', () => { it('Fragment', async () => { @@ -65,7 +64,7 @@ describe('jsx', () => { first?: string; last?: string; } - const FunctionalCmp: FunctionalComponent = ({ first = 'Kim', last = 'Doe' }) => ( + const FunctionalCmp = ({ first = 'Kim', last = 'Doe' }: FunctionalCmpProps) => (
Hi, my name is {first} {last}.
@@ -74,7 +73,11 @@ describe('jsx', () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { - return ; + return ( + <> + + + ); } } diff --git a/packages/core/src/runtime/_test_/lifecycle-async.spec.tsx b/packages/core/src/runtime/_test_/lifecycle-async.spec.tsx new file mode 100644 index 00000000000..d7501edddaa --- /dev/null +++ b/packages/core/src/runtime/_test_/lifecycle-async.spec.tsx @@ -0,0 +1,133 @@ +import { Component, Prop, Watch } from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it, vi } from '@stencil/vitest'; + +describe('lifecycle async', () => { + it('wait for componentWillLoad', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + componentWillLoad() { + return new Promise((resolve) => { + setTimeout(resolve); + }); + } + + render() { + return 'Loaded'; + } + } + + const { root } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(root.textContent).toBe('Loaded'); + }); + + it('fire lifecycle methods', async () => { + let log = ''; + @Component({ tag: 'cmp-a' }) + class CmpA { + @Prop() prop = 0; + @Watch('prop') + propDidChange() { + log += ' propDidChange'; + } + + connectedCallback() { + log += ' connectedCallback'; + } + + disconnectedCallback() { + log += ' disconnectedCallback'; + } + + componentWillLoad() { + return new Promise((resolve) => { + setTimeout(() => { + log += ' componentWillLoad'; + resolve(); + }); + }); + } + + componentDidLoad() { + log += ' componentDidLoad'; + } + + componentWillUpdate() { + return new Promise((resolve) => { + setTimeout(() => { + log += ' componentWillUpdate'; + resolve(); + }); + }); + } + + componentDidUpdate() { + log += ' componentDidUpdate'; + } + + componentWillRender() { + return new Promise((resolve) => { + setTimeout(() => { + log += ' componentWillRender'; + resolve(); + }); + }); + } + + componentDidRender() { + log += ' componentDidRender'; + } + + render() { + log += ' render'; + return log.trim(); + } + } + + const { root, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(root.textContent).toBe('connectedCallback componentWillLoad componentWillRender render'); + expect(log.trim()).toEqual( + 'connectedCallback componentWillLoad componentWillRender render componentDidRender componentDidLoad', + ); + + log = ''; + root.prop = 1; + await waitForChanges(); + + expect(log.trim()).toBe( + 'propDidChange componentWillUpdate componentWillRender render componentDidRender componentDidUpdate', + ); + }); + + it('windows emits event', async () => { + const mockEvent = vi.fn(); + @Component({ tag: 'cmp-a' }) + class CmpA { + componentWillLoad() { + window.addEventListener('appload', (ev: Event) => mockEvent((ev as CustomEvent).detail)); + } + + render() { + return 'Done'; + } + } + await newSpecPage({ + components: [CmpA], + html: ``, + includeAnnotations: true, + }); + + expect(mockEvent).toHaveBeenCalledTimes(1); + expect(mockEvent).toHaveBeenCalledWith({ + namespace: 'app', + }); + }); +}); diff --git a/src/runtime/test/lifecycle-sync.spec.tsx b/packages/core/src/runtime/_test_/lifecycle-sync.spec.tsx similarity index 97% rename from src/runtime/test/lifecycle-sync.spec.tsx rename to packages/core/src/runtime/_test_/lifecycle-sync.spec.tsx index fc4749a5d76..9165d2089f1 100644 --- a/src/runtime/test/lifecycle-sync.spec.tsx +++ b/packages/core/src/runtime/_test_/lifecycle-sync.spec.tsx @@ -1,5 +1,6 @@ import { Component, Element, forceUpdate, h, Host, Method, Prop, Watch } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; describe('lifecycle sync', () => { it('should fire connected/disconnected when removed', async () => { @@ -68,7 +69,6 @@ describe('lifecycle sync', () => { }); expect(root).toEqualHtml(` -
@@ -206,9 +206,9 @@ describe('lifecycle sync', () => { render() { return ( - - - + + + diff --git a/src/runtime/test/listen.spec.tsx b/packages/core/src/runtime/_test_/listen.spec.tsx similarity index 86% rename from src/runtime/test/listen.spec.tsx rename to packages/core/src/runtime/_test_/listen.spec.tsx index 0be32514d15..6e457502d74 100644 --- a/src/runtime/test/listen.spec.tsx +++ b/packages/core/src/runtime/_test_/listen.spec.tsx @@ -1,5 +1,6 @@ import { Component, Event, EventEmitter, Listen, resolveVar, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it, vi } from '@stencil/vitest'; describe('listen', () => { it('listen to click on host, from elm.click()', async () => { @@ -23,21 +24,27 @@ describe('listen', () => { }); expect(root).toEqualHtml(` - 0 + + 0 + `); root.click(); await waitForChanges(); expect(root).toEqualHtml(` - 1 + + 1 + `); root.click(); await waitForChanges(); expect(root).toEqualHtml(` - 2 + + 2 + `); }); @@ -83,43 +90,57 @@ describe('listen', () => { const other = doc.querySelector('other') as any; expect(root).toEqualHtml(` - 0,0,0,0 + + 0,0,0,0 + `); root.click(); await waitForChanges(); expect(root).toEqualHtml(` - 1,1,1,1 + + 1,1,1,1 + `); parent.click(); await waitForChanges(); expect(root).toEqualHtml(` - 1,2,2,2 + + 1,2,2,2 + `); other.click(); await waitForChanges(); expect(root).toEqualHtml(` - 1,3,3,3 + + 1,3,3,3 + `); body.click(); await waitForChanges(); expect(root).toEqualHtml(` - 1,4,4,4 + + 1,4,4,4 + `); doc.dispatchEvent(new CustomEvent('click', { bubbles: true })); await waitForChanges(); expect(root).toEqualHtml(` - 1,4,5,5 + + 1,4,5,5 + `); win.dispatchEvent(new CustomEvent('click', { bubbles: true })); await waitForChanges(); expect(root).toEqualHtml(` - 1,4,5,6 + + 1,4,5,6 + `); }); @@ -197,9 +218,13 @@ describe('listen', () => { expect(log).toEqual( `connectedCallback event0 event1 event2 event3 event4 event5 componentWillLoad event6 componentDidLoad `, ); - expect(a).toEqualHtml(`1 7`); + expect(a).toEqualHtml(` + 1 7 + `); await waitForChanges(); - expect(a).toEqualHtml(`1 7`); + expect(a).toEqualHtml(` + 1 7 + `); }); it('disconnects target listeners when element is not connected to DOM', async () => { @@ -220,8 +245,8 @@ describe('listen', () => { components: [CmpA], }); - jest.spyOn(doc, 'addEventListener'); - jest.spyOn(doc, 'removeEventListener'); + vi.spyOn(doc, 'addEventListener'); + vi.spyOn(doc, 'removeEventListener'); doc.createElement('cmp-a'); await waitForChanges(); @@ -230,8 +255,8 @@ describe('listen', () => { expect(events).toEqual(0); // no event listeners have been added as the element is not connected to the DOM - expect(doc.addEventListener.mock.calls.length).toBe(0); - expect(doc.removeEventListener.mock.calls.length).toBe(0); + expect(doc.addEventListener).toHaveBeenCalledTimes(0); + expect(doc.removeEventListener).toHaveBeenCalledTimes(0); }); describe('resolveVar', () => { @@ -258,14 +283,18 @@ describe('listen', () => { }); expect(root).toEqualHtml(` - 0 + + 0 + `); root.dispatchEvent(new CustomEvent('myEvent', { bubbles: true })); await waitForChanges(); expect(root).toEqualHtml(` - 1 + + 1 + `); }); @@ -294,14 +323,18 @@ describe('listen', () => { }); expect(root).toEqualHtml(` - 0 + + 0 + `); root.dispatchEvent(new CustomEvent('myEvent', { bubbles: true })); await waitForChanges(); expect(root).toEqualHtml(` - 1 + + 1 + `); }); }); diff --git a/src/runtime/test/method.spec.tsx b/packages/core/src/runtime/_test_/method.spec.tsx similarity index 86% rename from src/runtime/test/method.spec.tsx rename to packages/core/src/runtime/_test_/method.spec.tsx index f2beb1bc0de..f7086d12297 100644 --- a/src/runtime/test/method.spec.tsx +++ b/packages/core/src/runtime/_test_/method.spec.tsx @@ -1,5 +1,6 @@ import { Component, Method, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; describe('method', () => { it('call method', async () => { @@ -14,7 +15,7 @@ describe('method', () => { @Method() promiseMethod(val: string) { - return new Promise((resolve) => { + return new Promise((resolve) => { this.someState = val; resolve(); }); @@ -31,7 +32,9 @@ describe('method', () => { }); expect(root).toEqualHtml(` - default + + default + `); await root.asyncMethod('async'); diff --git a/src/runtime/test/mixin.spec.tsx b/packages/core/src/runtime/_test_/mixin.spec.tsx similarity index 95% rename from src/runtime/test/mixin.spec.tsx rename to packages/core/src/runtime/_test_/mixin.spec.tsx index 7f1835caa27..8a2e6ad937b 100644 --- a/src/runtime/test/mixin.spec.tsx +++ b/packages/core/src/runtime/_test_/mixin.spec.tsx @@ -1,14 +1,11 @@ import { Component, Event, EventEmitter, h, MixedInCtor, Mixin, Prop, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; describe('mixin', () => { it('can call a constructor with args', async () => { const MyMixin = (Base: B) => { class Test extends Base { - constructor(...args: any[]) { - super(...args); - } - @State() test = 'testing!!'; } return Test; @@ -59,7 +56,9 @@ describe('mixin', () => { expect(root).toEqualHtml(` - ABC + + ABC + `); }); diff --git a/packages/core/src/runtime/_test_/nonce.spec.ts b/packages/core/src/runtime/_test_/nonce.spec.ts new file mode 100644 index 00000000000..d359d50d5c2 --- /dev/null +++ b/packages/core/src/runtime/_test_/nonce.spec.ts @@ -0,0 +1,16 @@ +import { expect, describe, it, beforeEach } from '@stencil/vitest'; +import { plt } from 'virtual:platform'; + +import { setNonce } from '../nonce'; + +describe('setNonce', () => { + beforeEach(() => { + plt.$nonce$ = undefined; + }); + + it('should assign the nonce value to the runtime platform', () => { + setNonce('nonce-1234'); + + expect(plt.$nonce$).toBe('nonce-1234'); + }); +}); diff --git a/src/runtime/test/normalize-watchers.spec.ts b/packages/core/src/runtime/_test_/normalize-watchers.spec.ts similarity index 97% rename from src/runtime/test/normalize-watchers.spec.ts rename to packages/core/src/runtime/_test_/normalize-watchers.spec.ts index 7391201d2fb..33af99f76ed 100644 --- a/src/runtime/test/normalize-watchers.spec.ts +++ b/packages/core/src/runtime/_test_/normalize-watchers.spec.ts @@ -1,3 +1,5 @@ +import { expect, describe, it } from '@stencil/vitest'; + import { normalizeWatchers } from '../normalize-watchers'; describe('normalizeWatchers', () => { diff --git a/src/runtime/test/parse-property-value.spec.ts b/packages/core/src/runtime/_test_/parse-property-value.spec.ts similarity index 76% rename from src/runtime/test/parse-property-value.spec.ts rename to packages/core/src/runtime/_test_/parse-property-value.spec.ts index 808d62ba43e..283f007329e 100644 --- a/src/runtime/test/parse-property-value.spec.ts +++ b/packages/core/src/runtime/_test_/parse-property-value.spec.ts @@ -1,3 +1,6 @@ +import { expect, describe, it, beforeEach, afterEach } from '@stencil/vitest'; +import { BUILD } from 'virtual:app-data'; + import { MEMBER_FLAGS } from '../../utils'; import { parsePropertyValue } from '../parse-property-value'; @@ -81,6 +84,66 @@ describe('parse-property-value', () => { }); }); + describe('form-associated boolean coercion', () => { + // For form-associated components, per HTML spec, the presence of any boolean + // attribute (regardless of value) should make the property true. + // This differs from legacy behavior where "false" string becomes boolean false. + + beforeEach(() => { + BUILD.formAssociated = true; + }); + + afterEach(() => { + BUILD.formAssociated = false; + }); + + it('coerces "false" to true for form-associated components (HTML spec behavior)', () => { + const result = parsePropertyValue('false', MEMBER_FLAGS.Boolean, true); + expect(result).toBe(true); + }); + + it('coerces "false" to false for non-form-associated components (legacy behavior)', () => { + const result = parsePropertyValue('false', MEMBER_FLAGS.Boolean, false); + expect(result).toBe(false); + }); + + it('coerces "true" to true for form-associated components', () => { + const result = parsePropertyValue('true', MEMBER_FLAGS.Boolean, true); + expect(result).toBe(true); + }); + + it('coerces "true" to true for non-form-associated components', () => { + const result = parsePropertyValue('true', MEMBER_FLAGS.Boolean, false); + expect(result).toBe(true); + }); + + it('coerces empty string to true for form-associated components', () => { + const result = parsePropertyValue('', MEMBER_FLAGS.Boolean, true); + expect(result).toBe(true); + }); + + it('coerces empty string to true for non-form-associated components', () => { + const result = parsePropertyValue('', MEMBER_FLAGS.Boolean, false); + expect(result).toBe(true); + }); + + it('preserves boolean false for form-associated components (non-string value)', () => { + const result = parsePropertyValue(false, MEMBER_FLAGS.Boolean, true); + expect(result).toBe(false); + }); + + it('preserves boolean true for form-associated components (non-string value)', () => { + const result = parsePropertyValue(true, MEMBER_FLAGS.Boolean, true); + expect(result).toBe(true); + }); + + it('coerces any non-empty string to true for form-associated components', () => { + expect(parsePropertyValue('disabled', MEMBER_FLAGS.Boolean, true)).toBe(true); + expect(parsePropertyValue('0', MEMBER_FLAGS.Boolean, true)).toBe(true); + expect(parsePropertyValue('no', MEMBER_FLAGS.Boolean, true)).toBe(true); + }); + }); + describe('number coercion', () => { it('coerces a number value to a number', () => { const result = parsePropertyValue(42, MEMBER_FLAGS.Number); diff --git a/src/runtime/test/prop-serialize.spec.tsx b/packages/core/src/runtime/_test_/prop-serialize.spec.tsx similarity index 96% rename from src/runtime/test/prop-serialize.spec.tsx rename to packages/core/src/runtime/_test_/prop-serialize.spec.tsx index 85ed0996acb..14bf4d2eb7e 100644 --- a/src/runtime/test/prop-serialize.spec.tsx +++ b/packages/core/src/runtime/_test_/prop-serialize.spec.tsx @@ -1,5 +1,6 @@ import { Component, Element, h, Prop, PropSerialize, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it, vi } from '@stencil/vitest'; import { withSilentWarn } from '../../testing/testing-utils'; @@ -38,8 +39,8 @@ describe('attribute serialization', () => { components: [CmpA], html: ``, }); - jest.spyOn(rootInstance, 'method1'); - jest.spyOn(rootInstance, 'method2'); + vi.spyOn(rootInstance, 'method1'); + vi.spyOn(rootInstance, 'method2'); expect(rootInstance.method1Called).toBe(1); expect(rootInstance.method2Called).toBe(1); @@ -117,7 +118,7 @@ describe('attribute serialization', () => { await waitForChanges(); expect(rootInstance.watchCalled).toBe(6); - jest.spyOn(rootInstance, 'method'); + vi.spyOn(rootInstance, 'method'); // trigger updates in element root.prop = 1000; @@ -157,8 +158,8 @@ describe('attribute serialization', () => { components: [CmpA], html: ``, }); - jest.spyOn(rootInstance, 'method1'); - jest.spyOn(rootInstance, 'method2'); + vi.spyOn(rootInstance, 'method1'); + vi.spyOn(rootInstance, 'method2'); // set same values, serializer should not be called ('cos the prop is reflected) root.prop1 = 1; @@ -202,7 +203,7 @@ describe('attribute serialization', () => { components: [CmpA], html: ``, }); - jest.spyOn(rootInstance, 'method'); + vi.spyOn(rootInstance, 'method'); expect(rootInstance.method).toHaveBeenCalledTimes(0); expect(root.hasAttribute('bool-prop')).toBe(false); diff --git a/src/runtime/test/prop-warnings.spec.tsx b/packages/core/src/runtime/_test_/prop-warnings.spec.tsx similarity index 83% rename from src/runtime/test/prop-warnings.spec.tsx rename to packages/core/src/runtime/_test_/prop-warnings.spec.tsx index d3cc8d433cf..7bf97151d8b 100644 --- a/src/runtime/test/prop-warnings.spec.tsx +++ b/packages/core/src/runtime/_test_/prop-warnings.spec.tsx @@ -1,9 +1,10 @@ import { Component, h, Method, Prop } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it, vi, afterEach, afterAll } from '@stencil/vitest'; @Component({ tag: 'shared-cmp', - shadow: true, + encapsulation: { type: 'shadow' }, }) class SharedCmp { @Prop() a = 'Boom!'; @@ -14,7 +15,7 @@ class SharedCmp { } describe('prop', () => { - const spy = jest.spyOn(console, 'warn').mockImplementation(); + const spy = vi.spyOn(console, 'warn').mockImplementation(vi.fn); afterEach(() => spy.mockReset()); afterAll(() => spy.mockRestore()); @@ -39,12 +40,16 @@ describe('prop', () => { html: ``, }); - expect(root).toEqualHtml('1'); + expect(root).toEqualHtml(` + 1 + `); await root.update(); await waitForChanges(); - expect(root).toEqualHtml('2'); + expect(root).toEqualHtml(` + 2 + `); expect(spy).toHaveBeenCalledTimes(1); expect(spy.mock.calls[0][0]).toMatch(/@Prop\(\) "[A-Za-z-]+" on <[A-Za-z-]+> is immutable/); }); @@ -69,12 +74,16 @@ describe('prop', () => { html: ``, }); - expect(root).toEqualHtml('1'); + expect(root).toEqualHtml(` + 1 + `); await root.update(); await waitForChanges(); - expect(root).toEqualHtml('2'); + expect(root).toEqualHtml(` + 2 + `); expect(spy).not.toHaveBeenCalled(); }); @@ -93,12 +102,16 @@ describe('prop', () => { html: ``, }); - expect(root).toEqualHtml('1'); + expect(root).toEqualHtml(` + 1 + `); root.a = 2; await waitForChanges(); - expect(root).toEqualHtml('2'); + expect(root).toEqualHtml(` + 2 + `); expect(spy).not.toHaveBeenCalled(); }); @@ -141,7 +154,9 @@ describe('prop', () => { html: ``, }); - expect(root).toEqualHtml('1'); + expect(root).toEqualHtml(` + 1 + `); root.a = undefined; await waitForChanges(); diff --git a/src/runtime/test/prop.spec.tsx b/packages/core/src/runtime/_test_/prop.spec.tsx similarity index 84% rename from src/runtime/test/prop.spec.tsx rename to packages/core/src/runtime/_test_/prop.spec.tsx index 6cfa2b0fa15..a53fc4e5d0b 100644 --- a/src/runtime/test/prop.spec.tsx +++ b/packages/core/src/runtime/_test_/prop.spec.tsx @@ -1,5 +1,6 @@ import { AttrDeserialize, Component, h, Prop } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; function Clamp(lowerBound: number, upperBound: number): any { const clamp = (value: number) => Math.max(lowerBound, Math.min(value, upperBound)); @@ -36,7 +37,9 @@ describe('prop', () => { expect(root).toEqualHtml(` - #005a00 + + #005a00 + `); }); @@ -113,7 +116,9 @@ describe('prop', () => { }); expect(root).toEqualHtml(` - false-true-string-88-accessor-5 + + false-true-string-88-accessor-5 + `); expect(root.textContent).toBe('false-true-string-88-accessor-5'); @@ -149,7 +154,9 @@ describe('prop', () => { }); expect(root).toEqualHtml(` - initial-first-initial-second-initial-third + + initial-first-initial-second-initial-third + `); expect(shouldUpdateCalls).toHaveLength(0); @@ -161,13 +168,27 @@ describe('prop', () => { // componentShouldUpdate should have been called for each prop expect(shouldUpdateCalls).toHaveLength(3); - expect(shouldUpdateCalls[0]).toEqual({ value: 'new-first', old: 'initial-first', prop: 'first' }); - expect(shouldUpdateCalls[1]).toEqual({ value: 'new-second', old: 'initial-second', prop: 'second' }); - expect(shouldUpdateCalls[2]).toEqual({ value: 'new-third', old: 'initial-third', prop: 'third' }); + expect(shouldUpdateCalls[0]).toEqual({ + value: 'new-first', + old: 'initial-first', + prop: 'first', + }); + expect(shouldUpdateCalls[1]).toEqual({ + value: 'new-second', + old: 'initial-second', + prop: 'second', + }); + expect(shouldUpdateCalls[2]).toEqual({ + value: 'new-third', + old: 'initial-third', + prop: 'third', + }); // All values should be rendered expect(root).toEqualHtml(` - new-first-new-second-new-third + + new-first-new-second-new-third + `); }); @@ -193,23 +214,31 @@ describe('prop', () => { }); expect(root).toEqualHtml(` - 1 + + 1 + `); root.num++; await waitForChanges(); expect(root).toEqualHtml(` - 2 + + 2 + `); root.num++; await waitForChanges(); expect(root).toEqualHtml(` - 2 + + 2 + `); root.num++; await waitForChanges(); expect(root).toEqualHtml(` - 4 + + 4 + `); }); @@ -235,23 +264,31 @@ describe('prop', () => { }); expect(root).toEqualHtml(` - 1 + + 1 + `); root.num = 2; await waitForChanges(); expect(root).toEqualHtml(` - 2 + + 2 + `); root.num = 3; await waitForChanges(); expect(root).toEqualHtml(` - 2 + + 2 + `); root.num = 4; await waitForChanges(); expect(root).toEqualHtml(` - 4 + + 4 + `); }); @@ -282,10 +319,14 @@ describe('prop', () => { }); expect(root).toEqualHtml(` - +
-
Hello World
-
Hello World
+
+ Hello World +
+
+ Hello World +
`); @@ -296,7 +337,7 @@ describe('prop', () => { class CmpA { @Prop() value: any; render() { - return ; + return ; } } @@ -307,15 +348,15 @@ describe('prop', () => { root.value = 0; await waitForChanges(); - expect(root.querySelector('#internal-input').value).toBe('0'); + expect(root.querySelector('#internal-input').value).toBe('0'); root.value = ''; await waitForChanges(); - expect(root.querySelector('#internal-input').value).toBe(''); + expect(root.querySelector('#internal-input').value).toBe(''); root.value = 0; await waitForChanges(); - expect(root.querySelector('#internal-input').value).toBe('0'); + expect(root.querySelector('#internal-input').value).toBe('0'); }); it('should maintain reactivity when prop is set to undefined on element before initialization', async () => { @@ -345,7 +386,9 @@ describe('prop', () => { const root = page.root; expect(root).toEqualHtml(` -
empty
+
+ empty +
`); @@ -356,7 +399,9 @@ describe('prop', () => { expect(root).toEqualHtml(` -
updated
+
+ updated +
`); expect(root.first).toBe('updated'); @@ -386,7 +431,9 @@ describe('prop', () => { // null is a valid value, should render 'empty' due to nullish coalescing expect(root).toEqualHtml(` -
empty
+
+ empty +
`); @@ -395,7 +442,9 @@ describe('prop', () => { expect(root).toEqualHtml(` -
test
+
+ test +
`); }); diff --git a/src/runtime/test/queue.spec.tsx b/packages/core/src/runtime/_test_/queue.spec.tsx similarity index 93% rename from src/runtime/test/queue.spec.tsx rename to packages/core/src/runtime/_test_/queue.spec.tsx index d1bf80c6e81..044f1a6f5dc 100644 --- a/src/runtime/test/queue.spec.tsx +++ b/packages/core/src/runtime/_test_/queue.spec.tsx @@ -1,5 +1,6 @@ import { Component, Method, readTask, writeTask } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; describe('queue', () => { it('should execute tasks in the right order', async () => { diff --git a/src/runtime/test/regression-json-string-non-parsing.spec.tsx b/packages/core/src/runtime/_test_/regression-json-string-non-parsing.spec.tsx similarity index 97% rename from src/runtime/test/regression-json-string-non-parsing.spec.tsx rename to packages/core/src/runtime/_test_/regression-json-string-non-parsing.spec.tsx index a95a9af2eb7..ecf371a3bf4 100644 --- a/src/runtime/test/regression-json-string-non-parsing.spec.tsx +++ b/packages/core/src/runtime/_test_/regression-json-string-non-parsing.spec.tsx @@ -1,5 +1,6 @@ import { Component, h, Prop } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; /** * Regression tests for: @@ -70,7 +71,7 @@ describe('regression: do not parse JSON strings into objects', () => { return (
- + {typeof this.value}:{this.value}
diff --git a/packages/core/src/runtime/_test_/render-text.spec.tsx b/packages/core/src/runtime/_test_/render-text.spec.tsx new file mode 100644 index 00000000000..42c9c10d3bf --- /dev/null +++ b/packages/core/src/runtime/_test_/render-text.spec.tsx @@ -0,0 +1,132 @@ +import { Component, Prop } from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; + +describe('render-text', () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return 'Hello World'; + } + } + + it('Hello World, html option', async () => { + const { body } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(body).toEqualHtml(` + + + Hello World + + + `); + }); + + @Component({ tag: 'cmp-a', encapsulation: { type: 'shadow' } }) + class CmpAShadow { + render() { + return 'Hello World'; + } + } + + it('Hello World, innerHTML, await waitForChanges, shadow component', async () => { + const { body, waitForChanges } = await newSpecPage({ + components: [CmpAShadow], + }); + + body.innerHTML = ``; + await waitForChanges(); + + expect(body).toEqualHtml(` + + + + Hello World + + + + `); + }); + + it('Hello World, innerHTML, await waitForChanges', async () => { + const { body, waitForChanges } = await newSpecPage({ + components: [CmpA], + }); + + body.innerHTML = ``; + await waitForChanges(); + + expect(body).toEqualHtml(` + + + Hello World + + + `); + }); + + it('Hello World, page.setContent, await waitForChanges', async () => { + const page = await newSpecPage({ + components: [CmpA], + }); + + await page.setContent(``); + + expect(page.body).toEqualHtml(` + + + Hello World + + + `); + expect(page.root).toEqualHtml(` + + Hello World + + `); + expect(page.rootInstance).not.toBeUndefined(); + expect(page.rootInstance).not.toBeNull(); + }); + + it('Hello World, re-render, waitForChanges', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + @Prop() excitement = ''; + render() { + return `Hello World${this.excitement}`; + } + } + + const { root, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(root).toEqualHtml(` + + Hello World + + `); + + root.excitement = `!`; + await waitForChanges(); + + expect(root).toEqualHtml(` + + Hello World! + + `); + + root.excitement = `!!`; + await waitForChanges(); + + expect(root).toEqualHtml(` + + Hello World!! + + `); + }); +}); diff --git a/packages/core/src/runtime/_test_/render-vdom.spec.tsx b/packages/core/src/runtime/_test_/render-vdom.spec.tsx new file mode 100644 index 00000000000..d0d5fbe3c39 --- /dev/null +++ b/packages/core/src/runtime/_test_/render-vdom.spec.tsx @@ -0,0 +1,1333 @@ +import { + Component, + Element, + forceUpdate, + getRenderingRef, + h, + Host, + Prop, + setErrorHandler, + State, +} from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; + +import { withSilentWarn } from '../../testing/testing-utils'; + +describe('render-vdom', () => { + describe('build conditionals', () => { + it('vdomText', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return
Hello VDOM
; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: true, + vdomRef: false, + vdomListener: false, + vdomFunctional: false, + vdomText: true, + }); + }); + + it('vdomText from identifier', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + const text = 'Hello VDOM'; + return
{text}
; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: true, + vdomRef: false, + vdomListener: false, + vdomFunctional: false, + vdomText: true, + }); + }); + + it('vdomText from call expression', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + const text = () => 'Hello VDOM'; + return
{text()}
; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: true, + vdomRef: false, + vdomListener: false, + vdomFunctional: false, + vdomText: true, + }); + }); + + it('vdomText from object access', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + const text = { text: 'Hello VDOM' }; + return
{text.text}
; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: true, + vdomRef: false, + vdomListener: false, + vdomFunctional: false, + vdomText: true, + }); + }); + + it('vdomClass', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return
; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: true, + vdomStyle: false, + vdomKey: true, + vdomRef: false, + vdomListener: false, + vdomFunctional: false, + vdomText: false, + }); + }); + + it('vdomStyle', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return
; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: true, + vdomKey: true, + vdomRef: false, + vdomListener: false, + vdomFunctional: false, + vdomText: false, + }); + }); + + it('vdomKey', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return
; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: true, + vdomRef: false, + vdomListener: false, + vdomFunctional: false, + vdomText: false, + }); + }); + + it('vdomRef', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return ( +
{ + return; + }} + >
+ ); + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: true, + vdomRef: true, + vdomListener: false, + vdomFunctional: false, + vdomText: false, + }); + }); + + it('vdomListener onClick', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return ( +
{ + return; + }} + >
+ ); + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: true, + vdomRef: false, + vdomListener: true, + vdomFunctional: false, + vdomText: false, + }); + }); + + it('vdomListener on-click', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return ( +
{ + return; + }} + >
+ ); + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: true, + vdomRef: false, + vdomListener: true, + vdomFunctional: false, + vdomText: false, + }); + }); + + it('vdomFunctional', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + const H = () => { + return null; + }; + return ; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: false, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: false, + vdomRef: false, + vdomListener: false, + vdomFunctional: true, + vdomText: false, + }); + }); + + it('vdomFunctional (2)', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + const Tunnel = { + Provider: () => { + return null; + }, + }; + return ; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: false, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: false, + vdomRef: false, + vdomListener: false, + vdomFunctional: true, + vdomText: false, + }); + }); + + it('fallback spread', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + props: any; + render() { + return
; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: true, + vdomClass: true, + vdomStyle: true, + vdomKey: true, + vdomRef: true, + vdomListener: true, + vdomFunctional: false, + vdomText: false, + }); + }); + + it('normal properties', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + prop: any; + + render() { + return ; + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: false, + vdomStyle: false, + vdomKey: true, + vdomRef: false, + vdomListener: false, + vdomFunctional: false, + vdomText: false, + }); + }); + + it('all but style', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + const Span = 'span'; + return ( + + + + ); + } + } + + const { build } = await newSpecPage({ components: [CmpA], strictBuild: true }); + expect(build).toMatchObject({ + vdomAttribute: true, + vdomXlink: false, + vdomClass: true, + vdomStyle: false, + vdomKey: true, + vdomRef: true, + vdomListener: true, + vdomFunctional: true, + vdomText: true, + }); + }); + }); + + it('rerender on ref mutation', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + private nuRender = 0; + @State() valid = false; + + render() { + this.nuRender++; + return ( +
(this.valid = true)}> + {this.valid ? 'true' : 'false'} - {this.nuRender} +
+ ); + } + } + + const { root } = await withSilentWarn(() => + newSpecPage({ + components: [CmpA], + html: ``, + }), + ); + + expect(root).toEqualHtml(` + +
+ true - 2 +
+
+ `); + }); + + it('not rerender on render() mutation', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + private nuRender = 0; + @State() valid = false; + render() { + this.valid = true; + this.nuRender++; + return ( +
+ {this.valid ? 'true' : 'false'} - {this.nuRender} +
+ ); + } + } + + const { root } = await withSilentWarn(() => + newSpecPage({ + components: [CmpA], + html: ``, + }), + ); + + expect(root).toEqualHtml(` + +
+ true - 1 +
+
+ `); + }); + + it('Hello VDOM, re-render, flush', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + @Prop() excitement = ''; + render() { + return
Hello VDOM{this.excitement}
; + } + } + + const { root, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(root).toEqualHtml(` + +
+ Hello VDOM +
+
+ `); + + root.excitement = `!`; + await waitForChanges(); + + expect(root).toEqualHtml(` + +
+ Hello VDOM! +
+
+ `); + + root.excitement = `!!`; + await waitForChanges(); + + expect(root).toEqualHtml(` + +
+ Hello VDOM!! +
+
+ `); + }); + + it('render crash should not remove the content', async () => { + let didError = false; + setErrorHandler((_err) => { + didError = true; + }); + @Component({ tag: 'cmp-a' }) + class CmpA { + @Prop() crash = false; + render() { + if (this.crash) { + throw new Error('YOLO'); + } + return
Hello
; + } + } + + const { root, waitForChanges } = await newSpecPage({ + components: [CmpA], + + html: ``, + }); + + expect(root).toEqualHtml(` + +
+ Hello +
+
+ `); + + expect(didError).toBe(false); + root.crash = true; + await waitForChanges(); + + expect(root).toEqualHtml(` + +
+ Hello +
+
+ `); + expect(didError).toBe(true); + }); + + it('Hello VDOM, html option', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return
Hello VDOM
; + } + } + + const { root } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(root).toEqualHtml(` + +
+ Hello VDOM +
+
+ `); + }); + + it(' test', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return ( + + + + ); + } + } + + const { root } = await newSpecPage({ + components: [CmpA], + includeAnnotations: true, + html: `Hello`, + }); + + expect(root).toEqualHtml(` + + + Hello + + + `); + }); + + it('Hello VDOM, body.innerHTML, await flush', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return
Hello VDOM
; + } + } + + const { body, waitForChanges } = await newSpecPage({ + components: [CmpA], + }); + + body.innerHTML = ``; + await waitForChanges(); + + expect(body).toEqualHtml(` + + +
+ Hello VDOM +
+
+ + `); + }); + + it('should add classes', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return ( +
+ Hello VDOM +
+ ); + } + } + + const { body } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(body).toEqualHtml(` + + +
+ Hello VDOM +
+
+ + `); + }); + + it('should error when reusing vnodes', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + @Prop() first = ''; + @Prop() middle = ''; + @Prop() last = ''; + + private getText(): string { + return `${this.first} ${this.middle} ${this.last}`; + } + + render() { + const name = {this.getText()}; + + return ( +
+
Hello, World! I'm {name}
+
I repeat, I'm {name}
+
One last time, I'm {name}
+
+ ); + } + } + + let error; + try { + await newSpecPage({ + components: [CmpA], + html: ``, + }); + } catch (e) { + error = e; + } + expect(error.message).toContain('JSX'); + }); + + it('should render nested arrays', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + @Prop() excitement = ''; + render() { + const jsx = [

H1

,

h2

, ['Outside',

h3

]]; + return ( +
+ Text0 + {jsx} +
+ ); + } + } + const { root } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + expect(root).toEqualHtml(` + +
+ Text0 +

+ H1 +

+

+ h2 +

+ Outside +

+ h3 +

+
+
+ `); + }); + + it('should not render booleans', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + @Prop() excitement = ''; + render() { + return ( +
+ {false} + hola + {true} +
+ ); + } + } + const { root } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + expect(root).toEqualHtml(` + +
+ hola +
+
+ `); + }); + + describe('getRenderingRef', () => { + it('returns instance', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + const ref = getRenderingRef(); + expect(ref).toBe(this); + return ; + } + } + const MyFunctionalCmp = (props: any) => { + expect(getRenderingRef()).toBe(props.cmp); + return

MyFunctionalCmp

; + }; + const { root } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + expect(root).toEqualHtml(` + +

+ MyFunctionalCmp +

+
+ `); + }); + + it('useState hook', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + @State() count = 0; + render() { + return ; + } + } + + const useState = (state: string) => { + const ref = getRenderingRef(); + return [ref[state], (value: any) => (ref[state] = value)]; + }; + + const MyFunctionalCmp = () => { + const [count, setCount] = useState('count'); + return

setCount(count + 1)}>{count}

; + }; + const { root, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + expect(root).toEqualHtml(` + +

+ 0 +

+
+ `); + root.querySelector('p').click(); + await waitForChanges(); + expect(root).toEqualHtml(` + +

+ 1 +

+
+ `); + }); + }); + + describe('forceUpdate', () => { + it('should trigger re-render', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + private count = 0; + render() { + return this.count++; + } + } + + const { root, rootInstance, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + await waitForChanges(); + expect(root.textContent).toEqual('0'); + + expect(forceUpdate(root)).toBe(true); + await waitForChanges(); + expect(root.textContent).toEqual('1'); + + expect(forceUpdate(rootInstance)).toBe(true); + await waitForChanges(); + expect(root.textContent).toEqual('2'); + + expect(forceUpdate(rootInstance)).toBe(true); + expect(forceUpdate(root)).toBe(true); + await waitForChanges(); + await waitForChanges(); + expect(root.textContent).toEqual('3'); + + root.remove(); + expect(forceUpdate(root)).toBe(false); + expect(forceUpdate(rootInstance)).toBe(false); + }); + }); + + describe('input', () => { + it('should render attributes', async () => { + @Component({ + tag: 'cmp-a', + }) + class CmpA { + render() { + return ( + + + + + + + + + + + ); + } + } + + const { root } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + expect(root).toEqualHtml(` + + + + + + + + + + + `); + }); + }); + + describe('svg', () => { + it('should not override classes', async () => { + @Component({ + tag: 'cmp-a', + styles: ':host{}', + encapsulation: { type: 'scoped' }, + }) + class CmpA { + @Prop() addClass = false; + render() { + return ; + } + } + + const { root, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + includeAnnotations: true, + }); + expect(root).toEqualHtml(` + + + + `); + + root.querySelector('svg').classList.add('manual'); + root.addClass = true; + await waitForChanges(); + + expect(root).toEqualHtml(` + + + + `); + }); + + it('should update attributes', async () => { + @Component({ + tag: 'svg-attr', + }) + class SvgAttr { + @Prop() isOpen = false; + + render() { + return ( +
+
+ {this.isOpen ? ( + + + + ) : ( + + + + )} +
+
+ ); + } + } + + const { root, waitForChanges } = await newSpecPage({ + components: [SvgAttr], + html: ``, + }); + + const rect = root.querySelector('rect'); + expect(rect.getAttribute('transform')).toBe(null); + + root.isOpen = true; + await waitForChanges(); + expect(rect.getAttribute('transform')).toBe('rotate(45 27 27)'); + + root.isOpen = false; + await waitForChanges(); + expect(rect.getAttribute('transform')).toBe(null); + }); + + it('should render foreignObject properly', async () => { + @Component({ + tag: 'cmp-a', + }) + class CmpA { + render() { + return ( + + +
hello
+ + + + +
Still outside svg
+
+
+ bye +
+ Hello + Bye +
+ ); + } + } + + const { root } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + for (const el of Array.from(root.querySelectorAll('.is-html'))) { + expect((el as HTMLElement).namespaceURI).toEqual('http://www.w3.org/1999/xhtml'); + } + for (const el of Array.from(root.querySelectorAll('.is-svg'))) { + expect((el as SVGElement).namespaceURI).toEqual('http://www.w3.org/2000/svg'); + } + + expect(root).toEqualHtml(` + + + +
+ hello +
+ + + + +
+ Still outside svg +
+
+
+ + bye + +
+ + Hello + + + Bye + +
+
`); + }); + }); + + describe('native elements', () => { + it('should render correctly', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return ; + } + } + + const { root } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(root).toEqualHtml(` + + + `); + }); + }); + + describe('ref property', () => { + it('should set on Host', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + selfRef: HTMLElement; + @Element() el: HTMLElement; + + render() { + return (this.selfRef = el)}>; + } + } + + const { root, rootInstance } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(rootInstance.el).toEqual(root); + expect(rootInstance.el).toEqual(rootInstance.selfRef); + }); + + it('should set and reset', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + divRef: HTMLElement; + @Prop() visible = true; + render() { + return this.visible &&
(this.divRef = el)}>Hello VDOM
; + } + } + + const { root, rootInstance, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(rootInstance.divRef).toEqual(root.querySelector('div')); + root.visible = false; + await waitForChanges(); + + expect(rootInstance.divRef).toEqual(null); + }); + + it('should set once', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + divRef: HTMLElement; + counter = 0; + setRef = () => { + this.counter++; + }; + + render() { + return
Hello VDOM
; + } + } + + const { root, rootInstance, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(rootInstance.counter).toEqual(1); + forceUpdate(root); + await waitForChanges(); + + expect(rootInstance.counter).toEqual(1); + }); + + it('should set once (2)', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + counter = 0; + setRef = (el: HTMLDivElement | null) => { + if (el !== null) { + this.counter++; + } + }; + @Prop() state = true; + + render() { + return this.state ?
Hello VDOM
:
Hello VDOM
; + } + } + + const { root, rootInstance, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(rootInstance.counter).toEqual(1); + + root.state = false; + await waitForChanges(); + expect(rootInstance.counter).toEqual(1); + + root.state = true; + await waitForChanges(); + expect(rootInstance.counter).toEqual(2); + }); + + it('should not call ref cb w/ null when children are reordered', async () => { + // this test is a regression test ensuring that the algorithm for matching + // up children across rerenders works correctly when a basic transposition is + // done (the elements at the ends of the children swap places). + @Component({ tag: 'cmp-a' }) + class CmpA { + divRef: HTMLElement; + @Prop() state = true; + + renderA() { + return ( +
(this.divRef = el)}> + A +
+ ); + } + + renderB() { + return
B
; + } + + render() { + return this.state + ? [this.renderB(),
middle
, this.renderA()] + : [this.renderA(),
middle
, this.renderB()]; + } + } + + const { root, rootInstance, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + // ref should be set correctly after the first render + expect(rootInstance.divRef).toEqual(root.querySelector('.a')); + root.state = false; + await waitForChanges(); + // We've changed the state and forced a re-render. This tests one of the + // ways in which children can be re-ordered that the `updateChildren` algo + // can handle without having `key` attrs set. + expect(rootInstance.divRef).toEqual(root.querySelector('.a')); + }); + + it('should not call ref cb w/ null when children w/ keys are reordered', async () => { + // this test is a regression test ensuring that the algorithm for matching + // up children across rerenders works correctly in a situation in which it + // needs to use the `key` attribute to disambiguate them. At present, if the + // `key` attribute is _not_ present in this case then this test will fail + // because without the `key` Stencil's child-identity heuristic falls over. + @Component({ tag: 'cmp-a' }) + class CmpA { + divRef: HTMLElement; + @Prop() state = true; + + renderA() { + return ( +
(this.divRef = el)}> + A +
+ ); + } + + renderB() { + return
B
; + } + + render() { + return this.state ? [this.renderB(), this.renderA()] : [this.renderA()]; + } + } + + const { root, rootInstance, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + // ref should be set correctly after the first render + expect(rootInstance.divRef).toEqual(root.querySelector('.a')); + root.state = false; + await waitForChanges(); + // We've changed the state and forced a re-render where the algorithm for + // reconciling children will have to use the `key` attribute to find the + // equivalent VNode on the re-render. So if that is all working correctly + // then the value of our `divRef` property should be set correctly after + // the rerender. + // + // The reordering that is conditionally done in the `render` method of the + // test component above is specifically the type of edge case that the + // parts of the `updateChildren` algorithm which _don't_ use the `key` attr + // have trouble with. + // + // This is essentially a regression test for the issue described in + // https://github.com/stenciljs/core/issues/3253 + expect(rootInstance.divRef).toEqual(root.querySelector('.a')); + }); + + it('should call ref callbacks in correct order when element parent changes', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + divRef: HTMLDivElement | null = null; + refCallHistory: Array = []; + @State() wrap: boolean = false; + + captureDiv = (el: HTMLDivElement | null) => { + this.refCallHistory.push(el); + this.divRef = el; + }; + + renderInner() { + return ( +
+ hello +
+ ); + } + + render() { + return ( + + {this.wrap ? ( +
+

article

+ {this.renderInner()} +
+ ) : ( + this.renderInner() + )} +
+ ); + } + } + + const { root, rootInstance, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + // After initial render, ref should point to the element + expect(rootInstance.divRef).not.toBeNull(); + const initialCallCount = rootInstance.refCallHistory.length; + expect(initialCallCount).toBeGreaterThan(0); + expect(rootInstance.refCallHistory[initialCallCount - 1]).toEqual( + root.querySelector('.inner'), + ); + + root.wrap = true; + await waitForChanges(); + expect(rootInstance.divRef).not.toBeNull(); + + root.wrap = false; + await waitForChanges(); + expect(rootInstance.divRef).not.toBeNull(); + + const finalRef = rootInstance.refCallHistory[rootInstance.refCallHistory.length - 1]; + expect(finalRef).not.toBeNull(); + expect(finalRef).toEqual(root.querySelector('.inner')); + }); + }); +}); diff --git a/src/runtime/test/scoped.spec.tsx b/packages/core/src/runtime/_test_/scoped.spec.tsx similarity index 93% rename from src/runtime/test/scoped.spec.tsx rename to packages/core/src/runtime/_test_/scoped.spec.tsx index 484c6ce012c..eff88b9e688 100644 --- a/src/runtime/test/scoped.spec.tsx +++ b/packages/core/src/runtime/_test_/scoped.spec.tsx @@ -1,12 +1,13 @@ import { Component, h, Host, Prop, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; describe('scoped', () => { it('should add scoped classes', async () => { @Component({ tag: 'cmp-a', styles: ':host { color: inherit }', - scoped: true, + encapsulation: { type: 'scoped' }, }) class CmpA { render() { @@ -21,7 +22,7 @@ describe('scoped', () => { @Component({ tag: 'cmp-b', styles: ':host { color: inherit }', - scoped: true, + encapsulation: { type: 'scoped' }, }) class CmpB { render() { @@ -39,9 +40,8 @@ describe('scoped', () => { }); expect(page.root).toEqualHtml(` - - - + +
Hola @@ -56,7 +56,7 @@ describe('scoped', () => { @Component({ tag: 'cmp-b', styles: ':host { color: inherit }', - scoped: true, + encapsulation: { type: 'scoped' }, }) class CmpB { @Prop() slot = true; @@ -65,11 +65,11 @@ describe('scoped', () => { return (
{this.slot ? ( -
+
) : ( -
+
)}
); @@ -82,21 +82,21 @@ describe('scoped', () => { }); expect(page.root).toEqualHtml(` - - +
-
hello
+
+ hello +
`); - page.root.slot = false; + (page.root as any).slot = false; await page.waitForChanges(); await page.waitForChanges(); expect(page.root).toEqualHtml(` - - +
@@ -108,8 +108,7 @@ describe('scoped', () => { describe('should keep scope for onSlotChange', () => { @Component({ tag: 'my-node-with-slot-changes', - shadow: false, - scoped: true, + encapsulation: { type: 'scoped' }, }) class MyNodeWithSlotChanges { @State() slotChangeCount: number = 0; diff --git a/packages/core/src/runtime/_test_/shadow.spec.tsx b/packages/core/src/runtime/_test_/shadow.spec.tsx new file mode 100644 index 00000000000..51bee5a4b37 --- /dev/null +++ b/packages/core/src/runtime/_test_/shadow.spec.tsx @@ -0,0 +1,104 @@ +import { Component, h } from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; + +@Component({ + tag: 'cmp-a', + styles: ':host { color: black }', + encapsulation: { type: 'shadow' }, +}) +class CmpA { + render() { + return ( +
+ + + + +
+ +
+
+ ); + } +} + +describe('shadow', () => { + it('render with shadow-dom enabled', async () => { + const page = await newSpecPage({ + components: [CmpA], + includeAnnotations: true, + html: ` + + End + Text + Start + `, + }); + + expect(page.root).toEqualHtml(` + + +
+ + + + +
+ +
+
+
+ + End + + Text + + Start + +
`); + + expect(page.root).toEqualLightHtml(` + + + End + + Text + + Start + + `); + }); + + it('test shadow root innerHTML', async () => { + @Component({ + tag: 'cmp-a', + encapsulation: { type: 'shadow' }, + }) + class CmpA { + render() { + return
Shadow Content
; + } + } + + const page = await newSpecPage({ + components: [CmpA], + html: ` + + Light Content + + `, + }); + + expect(page.root).toEqualHtml(` + + +
+ Shadow Content +
+
+ Light Content +
+ `); + }); +}); diff --git a/src/runtime/test/state.spec.tsx b/packages/core/src/runtime/_test_/state.spec.tsx similarity index 90% rename from src/runtime/test/state.spec.tsx rename to packages/core/src/runtime/_test_/state.spec.tsx index 4cd22e654d7..f4689fe592e 100644 --- a/src/runtime/test/state.spec.tsx +++ b/packages/core/src/runtime/_test_/state.spec.tsx @@ -1,5 +1,6 @@ import { Component, Method, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; function Clamp(lowerBound: number, upperBound: number): any { const clamp = (value: number) => Math.max(lowerBound, Math.min(value, upperBound)); @@ -32,7 +33,8 @@ describe('state', () => { @Method() async update() { - (this.boolFalse = true), (this.boolTrue = false); + this.boolFalse = true; + this.boolTrue = false; this.str = 'hello'; this.num = 99; this.clamped = 11; @@ -49,7 +51,9 @@ describe('state', () => { }); expect(root).toEqualHtml(` - false-true-string-88-0 + + false-true-string-88-0 + `); expect(root.textContent).toBe('false-true-string-88-0'); diff --git a/src/runtime/test/style.spec.tsx b/packages/core/src/runtime/_test_/style.spec.tsx similarity index 97% rename from src/runtime/test/style.spec.tsx rename to packages/core/src/runtime/_test_/style.spec.tsx index ed85d1f5bd8..94598dc0912 100644 --- a/src/runtime/test/style.spec.tsx +++ b/packages/core/src/runtime/_test_/style.spec.tsx @@ -1,5 +1,6 @@ import { Component, getMode, setMode } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; describe('style', () => { it('get style string', async () => { diff --git a/packages/core/src/runtime/_test_/svg-element.spec.tsx b/packages/core/src/runtime/_test_/svg-element.spec.tsx new file mode 100644 index 00000000000..736482b20e3 --- /dev/null +++ b/packages/core/src/runtime/_test_/svg-element.spec.tsx @@ -0,0 +1,139 @@ +// @vitest-environment stencil + +import { Component, h } from '@stencil/core'; +import { Prop } from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it, beforeEach } from '@stencil/vitest'; + +describe('SVG element', () => { + it('should render #text nodes', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + @Prop() lines: any[] = [1]; + + render() { + return ( + + {this.lines.map((a) => { + return [Hola {a}]; + })} + + ); + } + } + const { root, waitForChanges } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + expect(root).toEqualHtml(` + + + + Hola 1 + + + + `); + root.lines = [1, 2]; + await waitForChanges(); + expect(root).toEqualHtml(` + + + + Hola 1 + + + Hola 2 + + + + `); + + // Ensure all SVG elements have the SVG namespace + const namespaces = Array.from(root.querySelectorAll('text')).map((e: any) => e.namespaceURI); + + expect(namespaces).toEqual(['http://www.w3.org/2000/svg', 'http://www.w3.org/2000/svg']); + }); + + it('should render camelCase attributes', async () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + const A = 'a' as any; + return ( + + + + + ); + } + } + const { root } = await newSpecPage({ + components: [CmpA], + html: ``, + }); + expect(root).toEqualHtml(` + + + + + + + `); + }); + + describe('path', () => { + @Component({ tag: 'cmp-a' }) + class CmpA { + render() { + return ( +
+ Dude!! + + + +
+ ); + } + } + + let path: SVGGeometryElement; + beforeEach(async () => { + const page = await newSpecPage({ + components: [CmpA], + html: ``, + }); + path = page.root.querySelector('#my-svg-path'); + }); + + it('path namespace is SVG', () => { + expect(path.namespaceURI).toEqual('http://www.w3.org/2000/svg'); + }); + + it('allows read access to the ownerSVGElement property', () => { + expect(path.ownerSVGElement).toEqual(null); + }); + + it('allows read access to the viewportElement property', () => { + expect(path.viewportElement).toEqual(null); + }); + + it('allows access to the getTotalLength() method', () => { + expect(path.getTotalLength()).toEqual(0); + }); + + it('allows access to the isPointInFill() method', () => { + expect(path.isPointInFill()).toEqual(false); + }); + + it('allows access to the isPointInStroke() method', () => { + expect(path.isPointInStroke()).toEqual(false); + }); + }); +}); diff --git a/src/runtime/test/update-component.spec.tsx b/packages/core/src/runtime/_test_/update-component.spec.tsx similarity index 89% rename from src/runtime/test/update-component.spec.tsx rename to packages/core/src/runtime/_test_/update-component.spec.tsx index 8fdc0a4591e..40afce50e04 100644 --- a/src/runtime/test/update-component.spec.tsx +++ b/packages/core/src/runtime/_test_/update-component.spec.tsx @@ -1,5 +1,6 @@ import { Component, h, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it, vi } from '@stencil/vitest'; describe('update-component', () => { describe('scheduleUpdate - initial load with queueMicrotask', () => { @@ -15,7 +16,7 @@ describe('update-component', () => { } it('should use queueMicrotask for initial load dispatch', async () => { - const queueMicrotaskSpy = jest.spyOn(global, 'queueMicrotask'); + const queueMicrotaskSpy = vi.spyOn(global, 'queueMicrotask'); const page = await newSpecPage({ components: [TestCmp], @@ -30,7 +31,7 @@ describe('update-component', () => { it('should not interfere with following render dispatch events', async () => { let componentWillRender = 0; - const queueMicrotaskSpy = jest.spyOn(global, 'queueMicrotask'); + const queueMicrotaskSpy = vi.spyOn(global, 'queueMicrotask'); @Component({ tag: 'update-test-cmp', diff --git a/packages/core/src/runtime/_test_/vdom-relocation.spec.tsx b/packages/core/src/runtime/_test_/vdom-relocation.spec.tsx new file mode 100644 index 00000000000..1f1ad578bbc --- /dev/null +++ b/packages/core/src/runtime/_test_/vdom-relocation.spec.tsx @@ -0,0 +1,86 @@ +import { Component, h, Listen, State } from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; + +describe('vdom-relocation', () => { + it('vdom-relocation', async () => { + @Component({ + tag: 'my-root', + }) + class Root { + @State() data = [1, 2, 3]; + @Listen('click') + onclick() { + this.data = [...this.data, this.data.length + 1]; + } + + render() { + return ( + + {this.data.map((a) => ( +
{a}
+ ))} +
+ ); + } + } + + @Component({ + tag: 'my-child', + }) + class Child { + render() { + return ( +
+ +
+ ); + } + } + + const { root, waitForChanges } = await newSpecPage({ + components: [Root, Child], + html: ``, + }); + + expect(root).toEqualHtml(` + + +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+
+
`); + + root.click(); + await waitForChanges(); + + expect(root).toEqualHtml(` + + +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+ 4 +
+
+
+
`); + }); +}); diff --git a/src/runtime/test/watch.spec.tsx b/packages/core/src/runtime/_test_/watch.spec.tsx similarity index 87% rename from src/runtime/test/watch.spec.tsx rename to packages/core/src/runtime/_test_/watch.spec.tsx index 24e450220f8..1c9388f73fa 100644 --- a/src/runtime/test/watch.spec.tsx +++ b/packages/core/src/runtime/_test_/watch.spec.tsx @@ -1,5 +1,6 @@ import { Component, Method, Prop, State, Watch } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it, vi } from '@stencil/vitest'; import { withSilentWarn } from '../../testing/testing-utils'; @@ -36,8 +37,8 @@ describe('watch', () => { components: [CmpA], html: ``, }); - jest.spyOn(rootInstance, 'method1'); - jest.spyOn(rootInstance, 'method2'); + vi.spyOn(rootInstance, 'method1'); + vi.spyOn(rootInstance, 'method2'); // set same values, watch should not be called root.prop1 = 1; @@ -64,6 +65,7 @@ describe('watch', () => { @Prop({ mutable: true }) prop = 10; @Prop({ mutable: true }) value = 10; + // @ts-ignore @State({ mutable: true }) someState = 'default'; @Watch('prop') @@ -76,23 +78,23 @@ describe('watch', () => { componentWillLoad() { expect(this.watchCalled).toBe(0); this.prop = 1; - expect(this.watchCalled).toBe(1); + expect(this.watchCalled).toBe(0); this.value = 1; - expect(this.watchCalled).toBe(2); + expect(this.watchCalled).toBe(0); this.someState = 'hello'; - expect(this.watchCalled).toBe(3); + expect(this.watchCalled).toBe(0); } componentDidLoad() { - expect(this.watchCalled).toBe(3); + expect(this.watchCalled).toBe(0); this.prop = 1; this.value = 1; this.someState = 'hello'; - expect(this.watchCalled).toBe(3); + expect(this.watchCalled).toBe(0); this.prop = 20; this.value = 30; this.someState = 'bye'; - expect(this.watchCalled).toBe(6); + expect(this.watchCalled).toBe(0); } } @@ -103,18 +105,20 @@ describe('watch', () => { }), ); - expect(rootInstance.watchCalled).toBe(6); - jest.spyOn(rootInstance, 'method'); + expect(rootInstance.watchCalled).toBe(0); + vi.spyOn(rootInstance, 'method'); // trigger updates in element root.prop = 1000; expect(rootInstance.method).toHaveBeenLastCalledWith(1000, 20, 'prop'); + expect(rootInstance.watchCalled).toBe(1); root.value = 1300; expect(rootInstance.method).toHaveBeenLastCalledWith(1300, 30, 'value'); + expect(rootInstance.watchCalled).toBe(2); }); - it('should Watch from lifecycles', async () => { + it('should *not* watch from lifecycle as per documentation', async () => { @Component({ tag: 'cmp-a' }) class CmpA { renderCount = 0; @@ -134,22 +138,22 @@ describe('watch', () => { connectedCallback() { expect(this.watchCalled).toBe(0); this.state = 1; - expect(this.watchCalled).toBe(1); + expect(this.watchCalled).toBe(0); this.state = 1; - expect(this.watchCalled).toBe(1); + expect(this.watchCalled).toBe(0); this.state = 2; - expect(this.watchCalled).toBe(2); + expect(this.watchCalled).toBe(0); } componentWillLoad() { - expect(this.watchCalled).toBe(2); + expect(this.watchCalled).toBe(0); this.state = 3; - expect(this.watchCalled).toBe(3); + expect(this.watchCalled).toBe(0); } componentDidLoad() { this.state = 4; - expect(this.watchCalled).toBe(4); + expect(this.watchCalled).toBe(0); } render() { @@ -165,14 +169,20 @@ describe('watch', () => { }), ); - expect(root).toEqualHtml(`2 4 4`); + expect(root).toEqualHtml(` + 2 4 0 + `); await waitForChanges(); await waitForChanges(); - expect(root).toEqualHtml(`2 4 4`); + expect(root).toEqualHtml(` + 2 4 0 + `); await root.pushState(); await waitForChanges(); - expect(root).toEqualHtml(`3 5 5`); + expect(root).toEqualHtml(` + 3 5 1 + `); }); it('correctly calls watch when @Prop uses `set()', async () => { @@ -205,7 +215,7 @@ describe('watch', () => { components: [CmpA], html: ``, }); - jest.spyOn(rootInstance, 'method1'); + vi.spyOn(rootInstance, 'method1'); // set same values, watch should not be called root.prop1 = 1; diff --git a/packages/core/src/runtime/asset-path.ts b/packages/core/src/runtime/asset-path.ts new file mode 100644 index 00000000000..84cb0a3202b --- /dev/null +++ b/packages/core/src/runtime/asset-path.ts @@ -0,0 +1,9 @@ +import { plt, win } from 'virtual:platform'; + +export const getAssetPath = (path: string) => { + const base = plt.$resourcesUrl$ || new URL('.', import.meta.url).href; + const assetUrl = new URL(path, base); + return assetUrl.origin !== win.location.origin ? assetUrl.href : assetUrl.pathname; +}; + +export const setAssetPath = (path: string) => (plt.$resourcesUrl$ = path); diff --git a/packages/core/src/runtime/bootstrap-loader.ts b/packages/core/src/runtime/bootstrap-loader.ts new file mode 100644 index 00000000000..98e3b7402cb --- /dev/null +++ b/packages/core/src/runtime/bootstrap-loader.ts @@ -0,0 +1,305 @@ +import { BUILD } from 'virtual:app-data'; +import { getHostRef, plt, registerHost, transformTag, win } from 'virtual:platform'; + +import { CMP_FLAGS } from '../utils/constants'; +import { queryNonceMetaTagContent } from '../utils/query-nonce-meta-tag-content'; +import { createShadowRoot } from '../utils/shadow-root'; +import { connectedCallback } from './connected-callback'; +import { disconnectedCallback } from './disconnected-callback'; +import { + applyLightDomPatches, + patchChildSlotNodes, + patchCloneNode, + patchInsertBefore, + patchSlotAppend, + patchSlotAppendChild, + patchSlotInsertAdjacentHTML, + patchSlotPrepend, + patchSlotRemoveChild, + patchTextContent, +} from './dom-extras'; +import { hmrStart } from './hmr-component'; +import { normalizeWatchers } from './normalize-watchers'; +import { createTime, installDevTools } from './profile'; +import { proxyComponent } from './proxy-component'; +import { HYDRATED_CSS, PLATFORM_FLAGS, PROXY_FLAGS } from './runtime-constants'; +import { hydrateScopedToShadow } from './styles'; +import { appDidLoad } from './update-component'; +import { addHostEventListeners } from '.'; +import type * as d from '../declarations'; +export { setNonce } from 'virtual:platform'; + +export const bootstrapLazy = ( + lazyBundles: d.LazyBundlesRuntimeData, + options: d.CustomElementsDefineOptions = {}, +) => { + if (BUILD.profile && performance.mark) { + performance.mark('st:app:start'); + } + installDevTools(); + + if (!win.document) { + console.warn('Stencil: No document found. Skipping'); + return; + } + + const endBootstrap = createTime('bootstrapLazy'); + const cmpTags: string[] = []; + const exclude = options.exclude || []; + const customElements = win.customElements; + const head = win.document.head; + const metaCharset = /*@__PURE__*/ head.querySelector('meta[charset]'); + const dataStyles = /*@__PURE__*/ win.document.createElement('style'); + const deferredConnectedCallbacks: { connectedCallback: () => void }[] = []; + let appLoadFallback: any; + let isBootstrapping = true; + + Object.assign(plt, options); + if (BUILD.asyncQueue && options.syncQueue) { + plt.$flags$ |= PLATFORM_FLAGS.queueSync; + } + if (BUILD.hydrateClientSide) { + // If the app is already hydrated there is not point to disable the + // async queue. This will improve the first input delay + plt.$flags$ |= PLATFORM_FLAGS.appLoaded; + } + + if (BUILD.hydrateClientSide && BUILD.shadowDom) { + hydrateScopedToShadow(); + } + + lazyBundles.map((lazyBundle) => { + lazyBundle[1].map((compactMeta) => { + const cmpMeta: d.ComponentRuntimeMeta = { + $flags$: compactMeta[0], + $tagName$: compactMeta[1], + $members$: compactMeta[2], + $listeners$: compactMeta[3], + }; + + if (BUILD.member) { + cmpMeta.$members$ = compactMeta[2]; + } + if (BUILD.hostListener) { + cmpMeta.$listeners$ = compactMeta[3]; + } + if (BUILD.reflect) { + cmpMeta.$attrsToReflect$ = []; + } + if (BUILD.propChangeCallback) { + // Watchers need normalization because the compiler format changed in + // 4.39.x (string[] → { [method]: flags }[]). Libraries compiled with + // an older Stencil may still emit the legacy format. Serializers and + // deserializers were introduced after that change, so their format is + // always current and only needs a nullish fallback. + cmpMeta.$watchers$ = normalizeWatchers(compactMeta[4]); + cmpMeta.$serializers$ = compactMeta[5] ?? {}; + cmpMeta.$deserializers$ = compactMeta[6] ?? {}; + } + const tagName = transformTag(cmpMeta.$tagName$); + const HostElement = class extends HTMLElement { + ['s-p']: Promise[]; + ['s-rc']: (() => void)[]; + // has registered event listeners for this instance + hreListeners = false; + + // StencilLazyHost + constructor(self: HTMLElement) { + // @ts-ignore + super(self); + self = this; + + registerHost(self, cmpMeta); + if ( + BUILD.shadowDom && + cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation && + !(cmpMeta.$flags$ & CMP_FLAGS.shadowNeedsScopedCss) + ) { + // this component is using shadow dom + // add the read-only property "shadowRoot" to the host element + // adding the shadow root build conditionals to minimize runtime + + if (!self.shadowRoot) { + // we don't want to call `attachShadow` if there's already a shadow root + // attached to the component + createShadowRoot.call(self, cmpMeta); + } else if (BUILD.isDev && self.shadowRoot.mode !== 'open') { + throw new Error( + `Unable to re-use existing shadow root for ${cmpMeta.$tagName$}! Mode is set to ${self.shadowRoot.mode} but DSD shadow roots must use open mode.`, + ); + } + } + } + + connectedCallback() { + const hostRef = getHostRef(this); + if (!hostRef) { + return; + } + + /** + * The `connectedCallback` lifecycle event can potentially be fired multiple times + * if the element is removed from the DOM and re-inserted. This is not a common use case, + * but it can happen in some scenarios. To prevent registering the same event listeners + * multiple times, we will only register them once. + */ + if (!this.hreListeners) { + this.hreListeners = true; + addHostEventListeners(this, hostRef, cmpMeta.$listeners$); + } + + if (appLoadFallback) { + clearTimeout(appLoadFallback); + appLoadFallback = null; + } + if (isBootstrapping) { + // connectedCallback will be processed once all components have been registered + deferredConnectedCallbacks.push(this); + } else { + plt.jmp(() => connectedCallback(this)); + } + } + + disconnectedCallback() { + plt.jmp(() => disconnectedCallback(this)); + + /** + * Clear up references within the `$vnode$` object to the DOM + * node that was removed. This is necessary to ensure that these + * references used as keys in the `hostRef` object can be properly + * garbage collected. + * + * Also remove the reference from `deferredConnectedCallbacks` array + * otherwise removed instances won't get garbage collected. + */ + plt.raf(() => { + const hostRef = getHostRef(this); + if (!hostRef) { + return; + } + const i = deferredConnectedCallbacks.findIndex((host) => host === this); + if (i > -1) { + deferredConnectedCallbacks.splice(i, 1); + } + if (hostRef?.$vnode$?.$elm$ instanceof Node && !hostRef.$vnode$.$elm$.isConnected) { + delete hostRef.$vnode$.$elm$; + } + }); + } + + componentOnReady() { + return getHostRef(this)?.$onReadyPromise$; + } + }; + + // patchCloneNode applies to all non-shadow components, not just those with slots + if ( + !(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && + (BUILD.slotCloneNode || (BUILD.patchClone && cmpMeta.$flags$ & CMP_FLAGS.patchClone)) + ) { + patchCloneNode(HostElement.prototype); + } + + if ( + !(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && + cmpMeta.$flags$ & CMP_FLAGS.hasSlot + ) { + // 'all' path: global lightDomPatches:true or per-component patchAll flag + if (BUILD.lightDomPatches || (BUILD.patchAll && cmpMeta.$flags$ & CMP_FLAGS.patchAll)) { + applyLightDomPatches(HostElement.prototype); + } else { + // Individual patches via global BUILD flags OR per-component flags + if ( + BUILD.slotChildNodes || + (BUILD.patchChildren && cmpMeta.$flags$ & CMP_FLAGS.patchChildren) + ) { + patchChildSlotNodes(HostElement.prototype); + } + if ( + BUILD.slotDomMutations || + (BUILD.patchInsert && cmpMeta.$flags$ & CMP_FLAGS.patchInsert) + ) { + patchSlotAppendChild(HostElement.prototype); + patchSlotAppend(HostElement.prototype); + patchSlotPrepend(HostElement.prototype); + patchSlotInsertAdjacentHTML(HostElement.prototype); + patchInsertBefore(HostElement.prototype); + patchSlotRemoveChild(HostElement.prototype); + } + if (BUILD.slotTextContent) { + patchTextContent(HostElement.prototype); + } + } + } + + // if the component is formAssociated we need to set that on the host + // element so that it will be ready for `attachInternals` to be called on + // it later on + if (BUILD.formAssociated && cmpMeta.$flags$ & CMP_FLAGS.formAssociated) { + (HostElement as any).formAssociated = true; + } + + if (BUILD.hotModuleReplacement) { + // if we're in an HMR dev build then we need to set up the callback + // which will carry out the work of actually replacing the module for + // this particular component + ((HostElement as any).prototype as d.HostElement)['s-hmr'] = function ( + hmrVersionId: string, + ) { + hmrStart(this, cmpMeta, hmrVersionId); + }; + } + + cmpMeta.$lazyBundleId$ = lazyBundle[0]; + + if (!exclude.includes(tagName) && !customElements.get(tagName)) { + cmpTags.push(tagName); + customElements.define( + tagName, + proxyComponent(HostElement as any, cmpMeta, PROXY_FLAGS.isElementConstructor) as any, + ); + } + }); + }); + + // Only bother generating CSS if we have components + if (cmpTags.length > 0) { + // Add hydration styles + if ( + BUILD.invisiblePrehydration && + !BUILD.staticHydrationStyles && + (BUILD.hydratedClass || BUILD.hydratedAttribute) + ) { + dataStyles.textContent += cmpTags.sort() + HYDRATED_CSS; + } + + // If we have styles, add them to the DOM + if (dataStyles.innerHTML.length) { + dataStyles.setAttribute('data-styles', ''); + + // Apply CSP nonce to the style tag if it exists + const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document); + if (nonce != null) { + dataStyles.setAttribute('nonce', nonce); + } + + // Insert the styles into the document head + // NOTE: this _needs_ to happen last so we can ensure the nonce (and other attributes) are applied + head.insertBefore(dataStyles, metaCharset ? metaCharset.nextSibling : head.firstChild); + } + } + + // Process deferred connectedCallbacks now all components have been registered + isBootstrapping = false; + if (deferredConnectedCallbacks.length) { + deferredConnectedCallbacks.map((host) => host.connectedCallback()); + } else { + if (BUILD.profile) { + plt.jmp(() => (appLoadFallback = setTimeout(appDidLoad, 30, 'timeout'))); + } else { + plt.jmp(() => (appLoadFallback = setTimeout(appDidLoad, 30))); + } + } + // Fallback appLoad event + endBootstrap(); +}; diff --git a/packages/core/src/runtime/bootstrap-standalone.ts b/packages/core/src/runtime/bootstrap-standalone.ts new file mode 100644 index 00000000000..25a4fba0fcb --- /dev/null +++ b/packages/core/src/runtime/bootstrap-standalone.ts @@ -0,0 +1,215 @@ +import { BUILD } from 'virtual:app-data'; +import { + addHostEventListeners, + consoleError, + forceUpdate, + getHostRef, + registerHost, + styles, + transformTag, +} from 'virtual:platform'; +import type * as d from '@stencil/core'; + +import { CMP_FLAGS } from '../utils/constants'; +import { createShadowRoot } from '../utils/shadow-root'; +import { connectedCallback } from './connected-callback'; +import { disconnectedCallback } from './disconnected-callback'; +import { + applyLightDomPatches, + patchChildSlotNodes, + patchCloneNode, + patchInsertBefore, + patchSlotAppend, + patchSlotAppendChild, + patchSlotInsertAdjacentHTML, + patchSlotPrepend, + patchSlotRemoveChild, + patchTextContent, +} from './dom-extras'; +import { hmrStart } from './hmr-component'; +import { computeMode } from './mode'; +import { normalizeWatchers } from './normalize-watchers'; +import { proxyComponent } from './proxy-component'; +import { PROXY_FLAGS } from './runtime-constants'; +import { attachStyles, getScopeId, hydrateScopedToShadow, registerStyle } from './styles'; + +export const defineCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMetaCompact) => { + customElements.define( + transformTag(compactMeta[1]), + proxyCustomElement(Cstr, compactMeta) as CustomElementConstructor, + ); +}; + +export const proxyCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMetaCompact) => { + // Set the app start mark on first component (for consistent profiling with lazy build) + if ( + BUILD.profile && + performance.mark && + performance.getEntriesByName('st:app:start', 'mark').length === 0 + ) { + performance.mark('st:app:start'); + } + + const cmpMeta: d.ComponentRuntimeMeta = { + $flags$: compactMeta[0], + $tagName$: compactMeta[1], + }; + try { + if (BUILD.member) { + cmpMeta.$members$ = compactMeta[2]; + } + if (BUILD.hostListener) { + cmpMeta.$listeners$ = compactMeta[3]; + } + if (BUILD.propChangeCallback) { + cmpMeta.$watchers$ = normalizeWatchers(Cstr.$watchers$); + cmpMeta.$deserializers$ = Cstr.$deserializers$; + cmpMeta.$serializers$ = Cstr.$serializers$; + } + if (BUILD.reflect) { + cmpMeta.$attrsToReflect$ = []; + } + + if (BUILD.hotModuleReplacement) { + // if we're in an HMR dev build then we need to set up the callback + // which will carry out the work of actually replacing the module for + // this particular component + (Cstr.prototype as d.HostElement)['s-hmr'] = function (hmrVersionId: string) { + hmrStart(this, cmpMeta, hmrVersionId); + }; + } + + // patchCloneNode applies to all non-shadow components, not just those with slots + if ( + !(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && + (BUILD.slotCloneNode || (BUILD.patchClone && cmpMeta.$flags$ & CMP_FLAGS.patchClone)) + ) { + patchCloneNode(Cstr.prototype); + } + + if ( + !(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && + cmpMeta.$flags$ & CMP_FLAGS.hasSlot + ) { + // 'all' path: global lightDomPatches:true or per-component patchAll flag + if (BUILD.lightDomPatches || (BUILD.patchAll && cmpMeta.$flags$ & CMP_FLAGS.patchAll)) { + applyLightDomPatches(Cstr.prototype); + } else { + // Individual patches via global BUILD flags OR per-component flags + if ( + BUILD.slotChildNodes || + (BUILD.patchChildren && cmpMeta.$flags$ & CMP_FLAGS.patchChildren) + ) { + patchChildSlotNodes(Cstr.prototype); + } + if ( + BUILD.slotDomMutations || + (BUILD.patchInsert && cmpMeta.$flags$ & CMP_FLAGS.patchInsert) + ) { + patchSlotAppendChild(Cstr.prototype); + patchSlotAppend(Cstr.prototype); + patchSlotPrepend(Cstr.prototype); + patchSlotInsertAdjacentHTML(Cstr.prototype); + patchInsertBefore(Cstr.prototype); + patchSlotRemoveChild(Cstr.prototype); + } + if (BUILD.slotTextContent) { + patchTextContent(Cstr.prototype); + } + } + } + + if (BUILD.hydrateClientSide && BUILD.shadowDom) { + hydrateScopedToShadow(); + } + + const originalConnectedCallback = Cstr.prototype.connectedCallback; + const originalDisconnectedCallback = Cstr.prototype.disconnectedCallback; + Object.assign(Cstr.prototype, { + __hasHostListenerAttached: false, + __registerHost(this: d.HostElement) { + registerHost(this, cmpMeta); + }, + componentOnReady(this: d.HostElement) { + return getHostRef(this)?.$onReadyPromise$; + }, + connectedCallback(this: d.HostElement & { __hasHostListenerAttached: boolean }) { + if (!this.__hasHostListenerAttached) { + const hostRef = getHostRef(this); + if (!hostRef) { + return; + } + addHostEventListeners(this, hostRef, cmpMeta.$listeners$); + this.__hasHostListenerAttached = true; + } + + connectedCallback(this); + if (originalConnectedCallback) { + originalConnectedCallback.call(this); + } + }, + disconnectedCallback(this: d.HostElement) { + disconnectedCallback(this); + if (originalDisconnectedCallback) { + originalDisconnectedCallback.call(this); + } + }, + __attachShadow(this: d.HostElement) { + const isClosed = BUILD.shadowModeClosed && !!(cmpMeta.$flags$ & CMP_FLAGS.shadowModeClosed); + + // For closed shadow roots, this.shadowRoot will be null. + // Check our stored reference instead. + let existingRoot: ShadowRoot | null = this.shadowRoot; + if (BUILD.shadowModeClosed && isClosed) { + existingRoot = (this as any).__shadowRoot ?? null; + } + + if (!existingRoot) { + createShadowRoot.call(this, cmpMeta); + } else if ( + BUILD.isDev && + BUILD.shadowModeClosed && + isClosed && + existingRoot.mode !== 'closed' + ) { + throw new Error( + `Unable to re-use existing shadow root for ${cmpMeta.$tagName$}! Mode is set to ${existingRoot.mode} but expected closed.`, + ); + } + }, + }); + Object.defineProperty(Cstr, 'is', { + value: cmpMeta.$tagName$, + configurable: true, + }); + + return proxyComponent(Cstr, cmpMeta, PROXY_FLAGS.isElementConstructor | PROXY_FLAGS.proxyState); + } catch (e) { + consoleError(e); + return Cstr; + } +}; + +export const forceModeUpdate = (elm: d.RenderNode) => { + if (BUILD.style && BUILD.mode && !BUILD.lazyLoad) { + const mode = computeMode(elm); + const hostRef = getHostRef(elm); + + if (hostRef && hostRef.$modeName$ !== mode) { + const cmpMeta = hostRef.$cmpMeta$; + const oldScopeId = elm['s-sc']; + const scopeId = getScopeId(cmpMeta, mode); + const style = (elm.constructor as any).style[mode]; + const flags = cmpMeta.$flags$; + if (style) { + if (!styles.has(scopeId)) { + registerStyle(scopeId, style, !!(flags & CMP_FLAGS.shadowDomEncapsulation)); + } + hostRef.$modeName$ = mode; + elm.classList.remove(oldScopeId + '-h', oldScopeId + '-s'); + attachStyles(hostRef); + forceUpdate(elm); + } + } + } +}; diff --git a/src/runtime/client-hydrate.ts b/packages/core/src/runtime/client-hydrate.ts similarity index 87% rename from src/runtime/client-hydrate.ts rename to packages/core/src/runtime/client-hydrate.ts index f407cd283e7..d67dbcf6f67 100644 --- a/src/runtime/client-hydrate.ts +++ b/packages/core/src/runtime/client-hydrate.ts @@ -1,9 +1,10 @@ -import { BUILD } from '@app-data'; -import { getHostRef, plt, transformTag, win } from '@platform'; +import { BUILD } from 'virtual:app-data'; +import { getHostRef, plt, transformTag, win } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; import { CMP_FLAGS } from '../utils/constants'; import { internalCall, patchSlottedNode } from './dom-extras'; +import { getShadowRoot } from './element'; import { createTime } from './profile'; import { COMMENT_NODE_ID, @@ -40,7 +41,8 @@ export const initializeClientHydrate = ( hostRef: d.HostRef, ) => { const endHydrate = createTime('hydrateClient', tagName); - const shadowRoot = hostElm.shadowRoot; + // Use getShadowRoot to handle both open and closed shadow roots + const shadowRoot = getShadowRoot(hostElm); // children placed by SSR within this component but don't necessarily belong to it. // We need to keep tabs on them so we can move them to the right place later const childRenderNodes: RenderNodeData[] = []; @@ -116,11 +118,11 @@ export const initializeClientHydrate = ( // let's find and add its styles to the shadowRoot, so we don't get a visual flicker const cmpMeta = getHostRef(childRenderNode.$elm$); if (cmpMeta) { - const scopeId = getScopeId( + const childScopeId = getScopeId( cmpMeta.$cmpMeta$, BUILD.mode ? childRenderNode.$elm$.getAttribute('s-mode') : undefined, ); - const styleSheet = win.document.querySelector(`style[sty-id="${scopeId}"]`); + const styleSheet = win.document.querySelector(`style[sty-id="${childScopeId}"]`); if (styleSheet) { shadowRootNodes.unshift(styleSheet.cloneNode(true) as d.RenderNode); @@ -129,7 +131,8 @@ export const initializeClientHydrate = ( } if (childRenderNode.$tag$ === 'slot') { - childRenderNode.$name$ = childRenderNode.$elm$['s-sn'] || (childRenderNode.$elm$ as any)['name'] || null; + childRenderNode.$name$ = + childRenderNode.$elm$['s-sn'] || (childRenderNode.$elm$ as any)['name'] || null; if (childRenderNode.$children$) { childRenderNode.$flags$ |= VNODE_FLAGS.isSlotFallback; @@ -147,14 +150,15 @@ export const initializeClientHydrate = ( } if (orgLocationNode && orgLocationNode.isConnected) { + const orgParentNode = orgLocationNode.parentNode; if (orgLocationNode.parentElement.shadowRoot && orgLocationNode['s-en'] === '') { // if this node is within a shadowDOM, with an original location home // we're safe to move it now - orgLocationNode.parentNode.insertBefore(node, orgLocationNode.nextSibling); + orgParentNode.insertBefore(node, orgLocationNode.nextSibling); } // Remove original location / slot reference comment now. // we'll handle it via `addSlotRelocateNode` later - orgLocationNode.parentNode.removeChild(orgLocationNode); + orgParentNode.removeChild(orgLocationNode); if (!shadowRoot) { // Add the Original Order of this node. @@ -190,61 +194,68 @@ export const initializeClientHydrate = ( for (snGroupIdx; snGroupIdx < snGroupLen; snGroupIdx++) { slottedItem = slotGroup[snGroupIdx]; - if (!hosts[slottedItem.hostId as any]) { + const hid = slottedItem.hostId as any; + if (!hosts[hid]) { // Cache this host for other grouped slotted nodes - hosts[slottedItem.hostId as any] = plt.$orgLocNodes$.get(slottedItem.hostId); + hosts[hid] = plt.$orgLocNodes$.get(slottedItem.hostId); } // This *shouldn't* happen as we collect all the custom elements first in `initializeDocumentHydrate` - if (!hosts[slottedItem.hostId as any]) continue; + if (!hosts[hid]) continue; - const hostEle = hosts[slottedItem.hostId as any]; + const hostEle = hosts[hid]; + const siNode = slottedItem.node; + const siSlot = slottedItem.slot; - if (hostEle.shadowRoot && slottedItem.node.parentElement !== hostEle) { + if (hostEle.shadowRoot && siNode.parentElement !== hostEle) { // shadowDOM. This slotted node got left behind. // Move the item to the element root for native slotting // insert node after the previous node in the slotGroup - hostEle.insertBefore(slottedItem.node, slotGroup[snGroupIdx - 1]?.node?.nextSibling); + hostEle.insertBefore(siNode, slotGroup[snGroupIdx - 1]?.node?.nextSibling); } // This node is either slotted in a non-shadow host, OR *that* host is nested in a non-shadow host if (!hostEle.shadowRoot || !shadowRoot) { // Try to set an appropriate Content-position Reference (CR) node for this host element - if (!slottedItem.slot['s-cr']) { + if (!siSlot['s-cr']) { // Is a CR already set on the host? - slottedItem.slot['s-cr'] = hostEle['s-cr']; + siSlot['s-cr'] = hostEle['s-cr']; - if (!slottedItem.slot['s-cr'] && hostEle.shadowRoot) { + if (!siSlot['s-cr'] && hostEle.shadowRoot) { // Host has shadowDOM - just use the host itself as the CR for native slotting - slottedItem.slot['s-cr'] = hostEle; + siSlot['s-cr'] = hostEle; } else { // If all else fails - just set the CR as the first child // (9/10 if node['s-cr'] hasn't been set, the node will be at the element root) - slottedItem.slot['s-cr'] = ((hostEle as any).__childNodes || hostEle.childNodes)[0]; + siSlot['s-cr'] = ((hostEle as any).__childNodes || hostEle.childNodes)[0]; } } // Create our 'Original Location' node - addSlotRelocateNode(slottedItem.node, slottedItem.slot, false, slottedItem.node['s-oo'] || currentPos); + addSlotRelocateNode(siNode, siSlot, false, siNode['s-oo'] || currentPos); if ( - slottedItem.node.parentElement?.shadowRoot && - slottedItem.node['getAttribute'] && - slottedItem.node.getAttribute('slot') + siNode.parentElement?.shadowRoot && + siNode['getAttribute'] && + siNode.getAttribute('slot') ) { // Remove the `slot` attribute from the slotted node: // if it's projected from a scoped component into a shadowRoot it's slot attribute will cause it to be hidden. // scoped components use the `s-sn` attribute to identify slotted nodes - slottedItem.node.removeAttribute('slot'); + siNode.removeAttribute('slot'); } - if (BUILD.experimentalSlotFixes) { + if ( + BUILD.lightDomPatches || + BUILD.slotChildNodes || + (BUILD.patchAll && hostRef.$cmpMeta$.$flags$ & CMP_FLAGS.patchAll) + ) { // patch this node for accessors like `nextSibling` (et al) - patchSlottedNode(slottedItem.node); + patchSlottedNode(siNode); } } // Empty text nodes are never accounted on the server (they don't get a comment node, or a positional id) // So let's manually increment their position counter for them, keeping them in the correct order in the slot - currentPos = (slottedItem.node['s-oo'] || currentPos) + 1; + currentPos = (siNode['s-oo'] || currentPos) + 1; } } @@ -277,8 +288,15 @@ export const initializeClientHydrate = ( Array.from(hostElm.childNodes).forEach((node) => { // don't remove slotted or original location nodes - if (typeof (node as d.RenderNode)['s-en'] !== 'string' && typeof (node as d.RenderNode)['s-sn'] !== 'string') { - if (node.nodeType === NODE_TYPE.ElementNode && (node as HTMLElement).slot && (node as HTMLElement).hidden) { + if ( + typeof (node as d.RenderNode)['s-en'] !== 'string' && + typeof (node as d.RenderNode)['s-sn'] !== 'string' + ) { + if ( + node.nodeType === NODE_TYPE.ElementNode && + (node as HTMLElement).slot && + (node as HTMLElement).hidden + ) { // this is a slotted node that doesn't have a home ... yet. // we can safely leave it be, native behavior will mean it's hidden (node as HTMLElement).removeAttribute('hidden'); @@ -340,7 +358,6 @@ const clientHydrate = ( if (childIdSplt[0] === hostId || childIdSplt[0] === '0') { childVNode = createSimpleVNode({ - $flags$: 0, $hostId$: childIdSplt[0], $nodeId$: childIdSplt[1], $depth$: childIdSplt[2], @@ -451,12 +468,6 @@ const clientHydrate = ( $depth$: childIdSplt[3], $index$: childIdSplt[4] || '0', $elm$: node, - $attrs$: null, - $children$: null, - $key$: null, - $name$: null, - $tag$: null, - $text$: null, }); if (childNodeType === TEXT_NODE_ID) { @@ -545,7 +556,10 @@ const clientHydrate = ( * @param node The node to search. * @param orgLocNodes A map of the original location annotations and the current node being searched. */ -export const initializeDocumentHydrate = (node: d.RenderNode, orgLocNodes: d.PlatformRuntime['$orgLocNodes$']) => { +const initializeDocumentHydrate = ( + node: d.RenderNode, + orgLocNodes: d.PlatformRuntime['$orgLocNodes$'], +) => { if (node.nodeType === NODE_TYPE.ElementNode) { // Add all the loaded component IDs in this document; required to find nodes later when deciding where slotted nodes should live const componentId = node[HYDRATE_ID] || node.getAttribute(HYDRATE_ID); @@ -581,23 +595,8 @@ export const initializeDocumentHydrate = (node: d.RenderNode, orgLocNodes: d.Pla * @param vnode - a vnode partial which will be augmented * @returns an complete vnode */ -const createSimpleVNode = (vnode: Partial): RenderNodeData => { - const defaultVNode: RenderNodeData = { - $flags$: 0, - $hostId$: null, - $nodeId$: null, - $depth$: null, - $index$: '0', - $elm$: null, - $attrs$: null, - $children$: null, - $key$: null, - $name$: null, - $tag$: null, - $text$: null, - }; - return { ...defaultVNode, ...vnode }; -}; +const createSimpleVNode = (vnode: Partial): RenderNodeData => + ({ $flags$: 0, $index$: '0', ...vnode }) as RenderNodeData; function addSlot( slotName: string, @@ -616,13 +615,17 @@ function addSlot( // Find this slots' current host parent (as dictated by the VDOM tree). // Important because where it is now in the constructed SSR markup might be different to where to *should* be - const parentNodeId = parentVNode?.$elm$ ? parentVNode.$elm$['s-id'] || parentVNode.$elm$.getAttribute('s-id') : ''; + const parentNodeId = parentVNode?.$elm$ + ? parentVNode.$elm$['s-id'] || parentVNode.$elm$.getAttribute('s-id') + : ''; if (BUILD.shadowDom && shadowRootNodes && win.document) { /* SHADOW */ // Browser supports shadowRoot and this is a shadow dom component; create an actual slot element - const slot = (childVNode.$elm$ = win.document.createElement(childVNode.$tag$ as string) as d.RenderNode); + const slot = (childVNode.$elm$ = win.document.createElement( + childVNode.$tag$ as string, + ) as d.RenderNode); if (childVNode.$name$) { // Add the slot name attribute @@ -632,7 +635,10 @@ function addSlot( if (parentVNode.$elm$.shadowRoot && parentNodeId && parentNodeId !== childVNode.$hostId$) { // Shadow component's slot is placed inside a nested component's shadowDOM; it doesn't belong to this host - it was forwarded by the SSR markup. // Insert it in the root of this host; it's lightDOM. It doesn't really matter where in the host root; the component will take care of it. - internalCall(parentVNode.$elm$, 'insertBefore')(slot, internalCall(parentVNode.$elm$, 'children')[0]); + internalCall(parentVNode.$elm$, 'insertBefore')( + slot, + internalCall(parentVNode.$elm$, 'children')[0], + ); } else { // Insert the new slot element before the slot comment internalCall(internalCall(node, 'parentNode') as d.RenderNode, 'insertBefore')(slot, node); @@ -651,10 +657,17 @@ function addSlot( // Test to see if this non-shadow component's mock 'slot' is placed inside a nested component's shadowDOM. If so, it doesn't belong here; // it was forwarded by the SSR markup. So we'll insert it into the root of this host; it's lightDOM with accompanying 'slotted' nodes - const shouldMove = parentNodeId && parentNodeId !== childVNode.$hostId$ && parentVNode.$elm$.shadowRoot; + const shouldMove = + parentNodeId && parentNodeId !== childVNode.$hostId$ && parentVNode.$elm$.shadowRoot; // attempt to find any mock slotted nodes which we'll move later - addSlottedNodes(slottedNodes, slotId, slotName, node, shouldMove ? parentNodeId : childVNode.$hostId$); + addSlottedNodes( + slottedNodes, + slotId, + slotName, + node, + shouldMove ? parentNodeId : childVNode.$hostId$, + ); patchSlotNode(node); if (shouldMove) { @@ -690,28 +703,30 @@ const addSlottedNodes = ( hostId: string, ) => { let slottedNode = slotNode.nextSibling as d.RenderNode; - slottedNodes[slotNodeId as any] = slottedNodes[slotNodeId as any] || []; + const group: SlottedNodes = (slottedNodes[slotNodeId as any] ||= []); // stop if we find another slot node (as subsequent nodes will belong to that slot) if (!slottedNode || slottedNode.nodeValue?.startsWith(SLOT_NODE_ID + '.')) return; // Loop through the next siblings of the slot node, looking for nodes that match this slot's name + // slottedNode is guaranteed truthy here (checked above) and at each while re-entry (checked by while condition) do { + const sa = slottedNode['getAttribute'] && slottedNode.getAttribute('slot'); if ( - slottedNode && - (((slottedNode['getAttribute'] && slottedNode.getAttribute('slot')) || slottedNode['s-sn']) === slotName || - (slotName === '' && - !slottedNode['s-sn'] && - (!slottedNode['getAttribute'] || !slottedNode.getAttribute('slot')) && - (slottedNode.nodeType === NODE_TYPE.CommentNode || slottedNode.nodeType === NODE_TYPE.TextNode))) + (sa || slottedNode['s-sn']) === slotName || + (slotName === '' && + !slottedNode['s-sn'] && + !sa && + (slottedNode.nodeType === NODE_TYPE.CommentNode || + slottedNode.nodeType === NODE_TYPE.TextNode)) ) { // Looking for nodes that match this slot's name, // OR are text / comment nodes and the slot is a default slot (no name) - text / comments cannot be direct descendants of *named* slots. // Also ignore slot fallback nodes - they're not part of the lightDOM slottedNode['s-sn'] = slotName; - slottedNodes[slotNodeId as any].push({ slot: slotNode, node: slottedNode, hostId }); + group.push({ slot: slotNode, node: slottedNode, hostId }); } - slottedNode = slottedNode?.nextSibling as d.RenderNode; + slottedNode = slottedNode.nextSibling as d.RenderNode; // continue *unless* we find another slot node (as subsequent nodes will belong to that slot) } while (slottedNode && !slottedNode.nodeValue?.startsWith(SLOT_NODE_ID + '.')); }; @@ -724,7 +739,10 @@ const addSlottedNodes = ( * @param type - the type of node to find * @returns the first corresponding node of the type */ -const findCorrespondingNode = (node: Node, type: NODE_TYPE.CommentNode | NODE_TYPE.TextNode) => { +const findCorrespondingNode = ( + node: Node, + type: typeof NODE_TYPE.CommentNode | typeof NODE_TYPE.TextNode, +) => { let sibling = node; do { sibling = sibling.nextSibling; diff --git a/packages/core/src/runtime/connected-callback.ts b/packages/core/src/runtime/connected-callback.ts new file mode 100644 index 00000000000..ac88905e11c --- /dev/null +++ b/packages/core/src/runtime/connected-callback.ts @@ -0,0 +1,175 @@ +import { BUILD } from 'virtual:app-data'; +import { addHostEventListeners, getHostRef, nextTick, plt, win } from 'virtual:platform'; +import type * as d from '@stencil/core'; + +import { CMP_FLAGS, HOST_FLAGS, MEMBER_FLAGS } from '../utils/constants'; +import { initializeClientHydrate } from './client-hydrate'; +import { getShadowRoot } from './element'; +import { fireConnectedCallback, initializeComponent } from './initialize-component'; +import { createTime } from './profile'; +import { HYDRATE_ID, NODE_TYPE, PLATFORM_FLAGS } from './runtime-constants'; +import { addStyle, getScopeId } from './styles'; +import { attachToAncestor } from './update-component'; +import { insertBefore } from './vdom/vdom-render'; + +export const connectedCallback = (elm: d.HostElement) => { + if ((plt.$flags$ & PLATFORM_FLAGS.isTmpDisconnected) === 0) { + const hostRef = getHostRef(elm); + if (!hostRef) { + return; + } + + const cmpMeta = hostRef.$cmpMeta$; + const endConnected = createTime('connectedCallback', cmpMeta.$tagName$); + + if (!(hostRef.$flags$ & HOST_FLAGS.hasConnected)) { + // first time this component has connected + hostRef.$flags$ |= HOST_FLAGS.hasConnected; + + let hostId: string; + if (BUILD.hydrateClientSide) { + hostId = elm.getAttribute(HYDRATE_ID); + if (hostId) { + if (BUILD.shadowDom && cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) { + // Use getShadowRoot to handle both open and closed shadow roots + const shadowRoot = getShadowRoot(elm); + const scopeId = BUILD.mode + ? addStyle(shadowRoot, cmpMeta, elm.getAttribute('s-mode')) + : addStyle(shadowRoot, cmpMeta); + elm.classList.remove(scopeId + '-h', scopeId + '-s'); + } else if (BUILD.scoped && cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation) { + // set the scope id on the element now. Useful when hydrating, + // to more quickly set the initial scoped classes for scoped css + const scopeId = getScopeId( + cmpMeta, + BUILD.mode ? elm.getAttribute('s-mode') : undefined, + ); + elm['s-sc'] = scopeId; + } + initializeClientHydrate(elm, cmpMeta.$tagName$, hostId, hostRef); + } + } + + if (BUILD.slotRelocation && !hostId) { + // initUpdate + // if the slot polyfill is required we'll need to put some nodes + // in here to act as original content anchors as we move nodes around + // host element has been connected to the DOM + if ( + BUILD.hydrateServerSide || + ((BUILD.slot || BUILD.shadowDom) && cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation) + ) { + setContentReference(elm); + } + } + + if (BUILD.asyncLoading) { + // find the first ancestor component (if there is one) and register + // this component as one of the actively loading child components for its ancestor + let ancestorComponent = elm; + + while ( + (ancestorComponent = + (ancestorComponent.parentNode as any) || (ancestorComponent.host as any)) + ) { + // climb up the ancestors looking for the first + // component that hasn't finished its lifecycle update yet + if ( + (BUILD.hydrateClientSide && + ancestorComponent.nodeType === NODE_TYPE.ElementNode && + ancestorComponent.hasAttribute('s-id') && + ancestorComponent['s-p']) || + ancestorComponent['s-p'] + ) { + // we found this components first ancestor component + // keep a reference to this component's ancestor component + attachToAncestor(hostRef, (hostRef.$ancestorComponent$ = ancestorComponent)); + break; + } + } + } + + // Lazy properties + // https://developers.google.com/web/fundamentals/web-components/best-practices#lazy-properties + if (BUILD.prop && !BUILD.hydrateServerSide && cmpMeta.$members$) { + Object.entries(cmpMeta.$members$).map(([memberName, [memberFlags]]) => { + if ( + memberFlags & MEMBER_FLAGS.Prop && + Object.prototype.hasOwnProperty.call(elm, memberName) + ) { + // Skip accessor properties created by reWireGetterSetter for ES2022 class field support. + // ES2022 rewiring creates instance accessors that delegate to the prototype, and these + // should not be treated as lazy data properties that need to be re-set. + if (cmpMeta.$flags$ & CMP_FLAGS.hasModernPropertyDecls) { + const desc = Object.getOwnPropertyDescriptor(elm, memberName); + if (desc && (desc.get || desc.set)) { + return; + } + } + const value = (elm as any)[memberName]; + delete (elm as any)[memberName]; + (elm as any)[memberName] = value; + } + }); + } + + // Pending props - apply props that were set on the element before it was upgraded. + // This handles the case where parent components render child custom elements before + // the child's module is loaded (e.g., lazy-loading with 'standalone'). + // Props were queued in setAccessor and are now applied through the proper setters. + // This only applies to 'standalone' (!lazyLoad), not 'loader-bundle' which handles this differently. + if (!BUILD.lazyLoad && BUILD.prop && !BUILD.hydrateServerSide) { + const pendingProps: Map | undefined = (elm as any)['s-pp']; + if (pendingProps) { + delete (elm as any)['s-pp']; + pendingProps.forEach((value, propName) => { + (elm as any)[propName] = value; + }); + } + } + + if (BUILD.initializeNextTick) { + // connectedCallback, taskQueue, initialLoad + // angular sets attribute AFTER connectCallback + // https://github.com/angular/angular/issues/18909 + // https://github.com/angular/angular/issues/19940 + nextTick(() => initializeComponent(elm, hostRef, cmpMeta)); + } else { + initializeComponent(elm, hostRef, cmpMeta); + } + } else { + // not the first time this has connected + + // reattach any event listeners to the host + // since they would have been removed when disconnected + addHostEventListeners(elm, hostRef, cmpMeta.$listeners$); + + // fire off connectedCallback() on component instance + if (hostRef?.$lazyInstance$) { + fireConnectedCallback(hostRef.$lazyInstance$, elm); + } else if (hostRef?.$onReadyPromise$) { + hostRef.$onReadyPromise$.then(() => fireConnectedCallback(hostRef.$lazyInstance$, elm)); + } + } + + endConnected(); + } +}; + +const setContentReference = (elm: d.HostElement) => { + if (!win.document) { + return; + } + + // only required when we're NOT using native shadow dom (slot) + // or this browser doesn't support native shadow dom + // and this host element was NOT created with SSR + // let's pick out the inner content for slot projection + // create a node to represent where the original + // content was first placed, which is useful later on + const contentRefElm = (elm['s-cr'] = win.document.createComment( + BUILD.isDebug ? `content-ref (host=${elm.localName})` : '', + ) as any); + contentRefElm['s-cn'] = true; + insertBefore(elm, contentRefElm, elm.firstChild as d.RenderNode); +}; diff --git a/src/runtime/disconnected-callback.ts b/packages/core/src/runtime/disconnected-callback.ts similarity index 91% rename from src/runtime/disconnected-callback.ts rename to packages/core/src/runtime/disconnected-callback.ts index 87e3a26b451..51222931167 100644 --- a/src/runtime/disconnected-callback.ts +++ b/packages/core/src/runtime/disconnected-callback.ts @@ -1,7 +1,7 @@ -import { BUILD } from '@app-data'; -import { getHostRef, plt } from '@platform'; +import { BUILD } from 'virtual:app-data'; +import { getHostRef, plt } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; import { PLATFORM_FLAGS } from './runtime-constants'; import { rootAppliedStyles } from './styles'; import { safeCall } from './update-component'; diff --git a/src/runtime/dom-extras.ts b/packages/core/src/runtime/dom-extras.ts similarity index 84% rename from src/runtime/dom-extras.ts rename to packages/core/src/runtime/dom-extras.ts index 9902aff24ff..4bd5c76fecb 100644 --- a/src/runtime/dom-extras.ts +++ b/packages/core/src/runtime/dom-extras.ts @@ -1,7 +1,6 @@ -import { BUILD } from '@app-data'; -import { supportsShadow } from '@platform'; +import { BUILD } from 'virtual:app-data'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; import { addSlotRelocateNode, dispatchSlotChangeEvent, @@ -15,14 +14,11 @@ import { /// HOST ELEMENTS /// -export const patchPseudoShadowDom = (hostElementPrototype: HTMLElement) => { - patchCloneNode(hostElementPrototype); +export const applyLightDomPatches = (hostElementPrototype: HTMLElement) => { patchSlotAppendChild(hostElementPrototype); patchSlotAppend(hostElementPrototype); patchSlotPrepend(hostElementPrototype); - patchSlotInsertAdjacentElement(hostElementPrototype); patchSlotInsertAdjacentHTML(hostElementPrototype); - patchSlotInsertAdjacentText(hostElementPrototype); patchInsertBefore(hostElementPrototype); patchTextContent(hostElementPrototype); patchChildSlotNodes(hostElementPrototype); @@ -40,8 +36,9 @@ export const patchCloneNode = (HostElementPrototype: any) => { HostElementPrototype.cloneNode = function (deep?: boolean) { const srcNode = this; - const isShadowDom = BUILD.shadowDom ? srcNode.shadowRoot && supportsShadow : false; + const isShadowDom = BUILD.shadowDom ? !!srcNode.shadowRoot : false; const clonedNode = orgCloneNode.call(srcNode, isShadowDom ? deep : false) as Node; + if (BUILD.slot && !isShadowDom && deep) { let i = 0; let slotted, nonStencilNode; @@ -66,9 +63,11 @@ export const patchCloneNode = (HostElementPrototype: any) => { for (; i < childNodes.length; i++) { slotted = (childNodes[i] as any)['s-nr']; - nonStencilNode = stencilPrivates.every((privateField) => !(childNodes[i] as any)[privateField]); + nonStencilNode = stencilPrivates.every( + (privateField) => !(childNodes[i] as any)[privateField], + ); if (slotted) { - if (BUILD.appendChildSlotFix && (clonedNode as any).__appendChild) { + if ((clonedNode as any).__appendChild) { (clonedNode as any).__appendChild(slotted.cloneNode(true)); } else { clonedNode.appendChild(slotted.cloneNode(true)); @@ -104,7 +103,10 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { const appendAfter = slotChildNodes[slotChildNodes.length - 1]; const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; - const insertedNode: d.RenderNode = internalCall(parent, 'insertBefore')(newChild, appendAfter.nextSibling); + const insertedNode: d.RenderNode = internalCall(parent, 'insertBefore')( + newChild, + appendAfter.nextSibling, + ); dispatchSlotChangeEvent(slotNode); // Check if there is fallback content that should be hidden @@ -123,7 +125,7 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { * * @param ElementPrototype The Stencil component to be patched */ -const patchSlotRemoveChild = (ElementPrototype: any) => { +export const patchSlotRemoveChild = (ElementPrototype: any) => { if (ElementPrototype.__removeChild) return; ElementPrototype.__removeChild = ElementPrototype.removeChild; @@ -152,7 +154,10 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { if ((HostElementPrototype as any).__prepend) return; (HostElementPrototype as any).__prepend = HostElementPrototype.prepend; - HostElementPrototype.prepend = function (this: d.HostElement, ...newChildren: (d.RenderNode | string)[]) { + HostElementPrototype.prepend = function ( + this: d.HostElement, + ...newChildren: (d.RenderNode | string)[] + ) { newChildren.forEach((newChild: d.RenderNode | string) => { if (typeof newChild === 'string') { newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode; @@ -166,7 +171,10 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { const appendAfter = slotChildNodes[0]; const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; - const toReturn = internalCall(parent, 'insertBefore')(newChild, internalCall(appendAfter, 'nextSibling')); + const toReturn = internalCall(parent, 'insertBefore')( + newChild, + internalCall(appendAfter, 'nextSibling'), + ); dispatchSlotChangeEvent(slotNode); return toReturn; } @@ -189,7 +197,10 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { export const patchSlotAppend = (HostElementPrototype: HTMLElement) => { if ((HostElementPrototype as any).__append) return; (HostElementPrototype as any).__append = HostElementPrototype.append; - HostElementPrototype.append = function (this: d.HostElement, ...newChildren: (d.RenderNode | string)[]) { + HostElementPrototype.append = function ( + this: d.HostElement, + ...newChildren: (d.RenderNode | string)[] + ) { newChildren.forEach((newChild: d.RenderNode | string) => { if (typeof newChild === 'string') { newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode; @@ -210,7 +221,11 @@ export const patchSlotInsertAdjacentHTML = (HostElementPrototype: HTMLElement) = if ((HostElementPrototype as any).__insertAdjacentHTML) return; const originalInsertAdjacentHtml = HostElementPrototype.insertAdjacentHTML; - HostElementPrototype.insertAdjacentHTML = function (this: d.HostElement, position: InsertPosition, text: string) { + HostElementPrototype.insertAdjacentHTML = function ( + this: d.HostElement, + position: InsertPosition, + text: string, + ) { if (position !== 'afterbegin' && position !== 'beforeend') { return originalInsertAdjacentHtml.call(this, position, text); } @@ -230,19 +245,6 @@ export const patchSlotInsertAdjacentHTML = (HostElementPrototype: HTMLElement) = }; }; -/** - * Patches the `insertAdjacentText` method for a slotted node inside a scoped component. Specifically, - * we only need to patch the behavior for the specific `beforeend` and `afterbegin` positions so the text node - * gets inserted into the DOM in the correct location. - * - * @param HostElementPrototype the `Element` to be patched - */ -export const patchSlotInsertAdjacentText = (HostElementPrototype: HTMLElement) => { - HostElementPrototype.insertAdjacentText = function (this: d.HostElement, position: InsertPosition, text: string) { - this.insertAdjacentHTML(position, text); - }; -}; - /** * Patches the `insertBefore` of a non-shadow component. * @@ -254,7 +256,7 @@ export const patchSlotInsertAdjacentText = (HostElementPrototype: HTMLElement) = * * @param HostElementPrototype the custom element prototype to patch */ -const patchInsertBefore = (HostElementPrototype: HTMLElement) => { +export const patchInsertBefore = (HostElementPrototype: HTMLElement) => { if ((HostElementPrototype as any).__insertBefore) return; const eleProto: d.RenderNode = HostElementPrototype; if (eleProto.__insertBefore) return; @@ -267,7 +269,9 @@ const patchInsertBefore = (HostElementPrototype: HTMLElement) => { currentChild: d.RenderNode | null, ) { const { slotName, slotNode } = findSlotFromSlottedNode(newChild, this); - const slottedNodes = this.__childNodes ? this.childNodes : getSlottedChildNodes(this.childNodes); + const slottedNodes = this.__childNodes + ? this.childNodes + : getSlottedChildNodes(this.childNodes); if (slotNode) { let found = false; @@ -317,36 +321,6 @@ const patchInsertBefore = (HostElementPrototype: HTMLElement) => { }; }; -/** - * Patches the `insertAdjacentElement` method for a slotted node inside a scoped component. Specifically, - * we only need to patch the behavior for the specific `beforeend` and `afterbegin` positions so the element - * gets inserted into the DOM in the correct location. - * - * @param HostElementPrototype the `Element` to be patched - */ -export const patchSlotInsertAdjacentElement = (HostElementPrototype: HTMLElement) => { - if ((HostElementPrototype as any).__insertAdjacentElement) return; - const originalInsertAdjacentElement = HostElementPrototype.insertAdjacentElement; - - HostElementPrototype.insertAdjacentElement = function ( - this: d.HostElement, - position: InsertPosition, - element: d.RenderNode, - ): Element { - if (position !== 'afterbegin' && position !== 'beforeend') { - return originalInsertAdjacentElement.call(this, position, element); - } - if (position === 'afterbegin') { - this.prepend(element); - return element; - } else if (position === 'beforeend') { - this.append(element); - return element; - } - return element; - }; -}; - /** * Patches the `textContent` of an unnamed slotted node inside a scoped component * @@ -358,12 +332,16 @@ export const patchTextContent = (hostElementPrototype: HTMLElement): void => { Object.defineProperty(hostElementPrototype, 'textContent', { get: function () { let text = ''; - const childNodes = this.__childNodes ? this.childNodes : getSlottedChildNodes(this.childNodes); + const childNodes = this.__childNodes + ? this.childNodes + : getSlottedChildNodes(this.childNodes); childNodes.forEach((node: d.RenderNode) => (text += node.textContent || '')); return text; }, set: function (value) { - const childNodes = this.__childNodes ? this.childNodes : getSlottedChildNodes(this.childNodes); + const childNodes = this.__childNodes + ? this.childNodes + : getSlottedChildNodes(this.childNodes); childNodes.forEach((node: d.RenderNode) => { if (node['s-ol']) node['s-ol'].remove(); node.remove(); @@ -411,6 +389,11 @@ export const patchChildSlotNodes = (elm: HTMLElement) => { Object.defineProperty(elm, 'childNodes', { get() { const result = new FakeNodeList(); + // If not yet initialized by Stencil (e.g. a cloneNode result), return raw children + if (!(this as any)['s-cr']) { + result.push(...Array.from(this.__childNodes)); + return result; + } result.push(...getSlottedChildNodes(this.__childNodes)); return result; }, @@ -602,7 +585,10 @@ function patchHostOriginalAccessor( * * @returns the original accessor or method of the node */ -export function internalCall(node: T, method: P): T[P] { +export function internalCall( + node: T, + method: P, +): T[P] { if ('__' + method in node) { const toReturn = node[('__' + method) as keyof d.RenderNode] as T[P]; if (typeof toReturn !== 'function') return toReturn; diff --git a/packages/core/src/runtime/element.ts b/packages/core/src/runtime/element.ts new file mode 100644 index 00000000000..1468a4d417f --- /dev/null +++ b/packages/core/src/runtime/element.ts @@ -0,0 +1,26 @@ +import { BUILD } from 'virtual:app-data'; +import { getHostRef } from 'virtual:platform'; +import type * as d from '@stencil/core'; + +export const getElement = (ref: any) => + BUILD.lazyLoad ? getHostRef(ref)?.$hostElement$ : (ref as d.HostElement); + +/** + * Get the shadow root for a Stencil component's host element. + * This works for both open and closed shadow DOM modes. + * + * For closed shadow DOM, `element.shadowRoot` returns `null` by design, + * but Stencil stores the reference internally so components can still + * access their own shadow root. + * + * @param element The host element (from @Element() decorator) + * @returns The shadow root, or null if no shadow root exists + */ +export const getShadowRoot = (element: HTMLElement): ShadowRoot | null => { + // For closed shadow DOM, Stencil stores the shadow root as __shadowRoot + // since element.shadowRoot returns null by spec for closed mode + if (BUILD.shadowModeClosed && (element as any).__shadowRoot) { + return (element as any).__shadowRoot; + } + return element.shadowRoot; +}; diff --git a/src/runtime/event-emitter.ts b/packages/core/src/runtime/event-emitter.ts similarity index 77% rename from src/runtime/event-emitter.ts rename to packages/core/src/runtime/event-emitter.ts index de3821770c1..6c4a307a1a0 100644 --- a/src/runtime/event-emitter.ts +++ b/packages/core/src/runtime/event-emitter.ts @@ -1,7 +1,7 @@ -import { BUILD } from '@app-data'; -import { consoleDevWarn, plt } from '@platform'; +import { BUILD } from 'virtual:app-data'; +import { consoleDevWarn, plt } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; import { EVENT_FLAGS } from '../utils/constants'; import { getElement } from './element'; @@ -10,7 +10,9 @@ export const createEvent = (ref: d.RuntimeRef, name: string, flags: number) => { return { emit: (detail: any) => { if (BUILD.isDev && !elm.isConnected) { - consoleDevWarn(`The "${name}" event was emitted, but the dispatcher node is no longer connected to the dom.`); + consoleDevWarn( + `The "${name}" event was emitted, but the dispatcher node is no longer connected to the dom.`, + ); } return emitEvent(elm, name, { bubbles: !!(flags & EVENT_FLAGS.Bubbles), diff --git a/src/runtime/fragment.ts b/packages/core/src/runtime/fragment.ts similarity index 100% rename from src/runtime/fragment.ts rename to packages/core/src/runtime/fragment.ts diff --git a/packages/core/src/runtime/hmr-component.ts b/packages/core/src/runtime/hmr-component.ts new file mode 100644 index 00000000000..6b0c6090d76 --- /dev/null +++ b/packages/core/src/runtime/hmr-component.ts @@ -0,0 +1,122 @@ +import * as d from '@stencil/core'; +import { BUILD } from 'virtual:app-data'; +import { addHostEventListeners, forceUpdate, getHostRef } from 'virtual:platform'; + +import { HOST_FLAGS } from '../utils/constants'; +import { initializeComponent } from './initialize-component'; + +/** + * Kick off hot-module-replacement for a component. In order to replace the + * component in-place we: + * + * 1. get a reference to the {@link d.HostRef} for the element + * 2. reset the element's runtime flags + * 3. re-run the initialization logic for the element (via + * {@link initializeComponent}) + * + * For standalone (non-lazy) builds, we instead re-import the component module + * and patch the prototype of the registered constructor in-place, then + * force a re-render of all existing instances in the DOM. + * + * @param hostElement the host element for the component which we want to start + * doing HMR + * @param cmpMeta runtime metadata for the component + * @param hmrVersionId the current HMR version ID + */ +export const hmrStart = ( + hostElement: d.HostElement, + cmpMeta: d.ComponentRuntimeMeta, + hmrVersionId: string, +) => { + if (BUILD.lazyLoad) { + // Lazy-loaded build: reset flags and re-initialize (existing behavior) + const hostRef = getHostRef(hostElement); + if (!hostRef) { + return; + } + + // reset state flags to only have been connected + hostRef.$flags$ = HOST_FLAGS.hasConnected; + + // detach any event listeners that may have been added + if (BUILD.hostListener && hostRef.$rmListeners$) { + hostRef.$rmListeners$.map((rmListener) => rmListener()); + hostRef.$rmListeners$ = undefined; + } + + // re-initialize the component + initializeComponent(hostElement, hostRef, cmpMeta, hmrVersionId); + } else { + // Standalone build: re-import the module and patch the constructor prototype, + // then re-connect all existing instances in the DOM. + hmrStandalone(hostElement, cmpMeta, hmrVersionId); + } +}; + +const hmrStandalone = async ( + hostElement: d.HostElement, + cmpMeta: d.ComponentRuntimeMeta, + hmrVersionId: string, +) => { + const modulePath: string | undefined = (hostElement.constructor as any).__stencil_module__; + console.log(`[Stencil HMR] hmrStandalone <${cmpMeta.$tagName$}> modulePath:`, modulePath); + if (!modulePath) { + console.warn( + `[Stencil HMR] No __stencil_module__ on <${cmpMeta.$tagName$}> constructor — was this built with devMode?`, + ); + return; + } + + try { + // Re-import with cache-bust query string so the browser fetches the updated file + const newModule = await import( + /* @vite-ignore */ + `${modulePath}?s-hmr=${hmrVersionId}` + ); + + // Find the updated class — prefer a named export whose `.is` tag matches, + // fall back to the default export + const NewClass: any = + Object.values(newModule).find( + (v: any) => typeof v === 'function' && v.is === cmpMeta.$tagName$, + ) ?? newModule.default; + + if (!NewClass) { + return; + } + + // Patch the registered constructor prototype in-place so all existing + // instances pick up the new render/lifecycle methods. + // Object.assign is intentionally NOT used here — class methods are + // non-enumerable and would be silently skipped. + const ctor = customElements.get(cmpMeta.$tagName$) as any; + + if (ctor) { + for (const key of Object.getOwnPropertyNames(NewClass.prototype)) { + if (key === 'constructor') continue; + Object.defineProperty( + ctor.prototype, + key, + Object.getOwnPropertyDescriptor(NewClass.prototype, key)!, + ); + } + } + + // Force a re-render on all live instances + const instances = document.querySelectorAll(cmpMeta.$tagName$); + instances.forEach((el) => { + if (BUILD.hostListener) { + const hostRef = getHostRef(el as any); + if (hostRef?.$rmListeners$) { + hostRef.$rmListeners$.map((rmListener) => rmListener()); + hostRef.$rmListeners$ = undefined; + // Re-attach listeners with new handler references + addHostEventListeners(el as any, hostRef, cmpMeta.$listeners$); + } + } + forceUpdate(el); + }); + } catch (e) { + console.error(`[Stencil HMR] Failed to reload <${cmpMeta.$tagName$}>`, e); + } +}; diff --git a/packages/core/src/runtime/host-listener.ts b/packages/core/src/runtime/host-listener.ts new file mode 100644 index 00000000000..6c452bba3a5 --- /dev/null +++ b/packages/core/src/runtime/host-listener.ts @@ -0,0 +1,70 @@ +import { BUILD } from 'virtual:app-data'; +import { consoleError, plt, supportsListenerOptions, win } from 'virtual:platform'; +import type * as d from '@stencil/core'; + +import { HOST_FLAGS, LISTENER_FLAGS } from '../utils/constants'; + +export const addHostEventListeners = ( + elm: d.HostElement, + hostRef: d.HostRef, + listeners?: d.ComponentRuntimeHostListener[], +) => { + if (BUILD.hostListener && listeners && win.document) { + // this is called immediately within the element's constructor + // initialize our event listeners on the host element + // we do this now so that we can listen to events that may + // have fired even before the instance is ready + + listeners.map(([flags, name, method]) => { + const target = BUILD.hostListenerTarget + ? getHostListenerTarget(win.document, elm, flags) + : elm; + const handler = hostListenerProxy(hostRef, method); + const opts = hostListenerOpts(flags); + plt.ael(target, name, handler, opts); + (hostRef.$rmListeners$ = hostRef.$rmListeners$ || []).push(() => + plt.rel(target, name, handler, opts), + ); + }); + } +}; + +const hostListenerProxy = (hostRef: d.HostRef, methodName: string) => (ev: Event) => { + try { + if (BUILD.lazyLoad) { + if (hostRef.$flags$ & HOST_FLAGS.isListenReady) { + // instance is ready, let's call it's member method for this event + hostRef.$lazyInstance$?.[methodName](ev); + } else { + (hostRef.$queuedListeners$ = hostRef.$queuedListeners$ || []).push([methodName, ev]); + } + } else { + (hostRef.$hostElement$ as any)[methodName](ev); + } + } catch (e) { + consoleError(e, hostRef.$hostElement$); + } +}; + +const getHostListenerTarget = (doc: Document, elm: Element, flags: number): EventTarget => { + if (BUILD.hostListenerTargetDocument && flags & LISTENER_FLAGS.TargetDocument) { + return doc; + } + if (BUILD.hostListenerTargetWindow && flags & LISTENER_FLAGS.TargetWindow) { + return win; + } + if (BUILD.hostListenerTargetBody && flags & LISTENER_FLAGS.TargetBody) { + return doc.body; + } + + return elm; +}; + +// prettier-ignore +const hostListenerOpts = (flags: number) => + supportsListenerOptions + ? ({ + passive: (flags & LISTENER_FLAGS.Passive) !== 0, + capture: (flags & LISTENER_FLAGS.Capture) !== 0, + }) + : (flags & LISTENER_FLAGS.Capture) !== 0; diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts new file mode 100644 index 00000000000..3f5c3d7569b --- /dev/null +++ b/packages/core/src/runtime/index.ts @@ -0,0 +1,26 @@ +export { getAssetPath, setAssetPath } from './asset-path'; +export type { HTMLStencilElement, JSXBase } from '../declarations/stencil-public-runtime'; +export { defineCustomElement, forceModeUpdate, proxyCustomElement } from './bootstrap-standalone'; +export { bootstrapLazy } from './bootstrap-loader'; +export { connectedCallback } from './connected-callback'; +export { disconnectedCallback } from './disconnected-callback'; +export { getElement, getShadowRoot } from './element'; +export { createEvent } from './event-emitter'; +export { Fragment } from './fragment'; +export { addHostEventListeners } from './host-listener'; +export { Mixin } from './mixin'; +export { getMode, setMode } from './mode'; +export { setNonce } from './nonce'; +export { normalizeWatchers } from './normalize-watchers'; +export { parsePropertyValue } from './parse-property-value'; +export { setPlatformOptions } from './platform-options'; +export { proxyComponent } from './proxy-component'; +export { render } from './render'; +export { HYDRATED_STYLE_ID } from './runtime-constants'; +export { getValue, setValue } from './set-value'; +export { setTagTransformer, transformTag } from './tag-transform'; +export { forceUpdate, getRenderingRef, postUpdateComponent } from './update-component'; +export { h, Host } from './vdom/h'; +export { jsx, jsxs, jsxDEV } from './vdom/jsx-runtime'; +export { insertVdomAnnotations } from './vdom/vdom-annotations'; +export { renderVdom } from './vdom/vdom-render'; diff --git a/src/runtime/initialize-component.ts b/packages/core/src/runtime/initialize-component.ts similarity index 90% rename from src/runtime/initialize-component.ts rename to packages/core/src/runtime/initialize-component.ts index 68220944d70..ffb86330abf 100644 --- a/src/runtime/initialize-component.ts +++ b/packages/core/src/runtime/initialize-component.ts @@ -1,7 +1,7 @@ -import { BUILD } from '@app-data'; -import { consoleError, loadModule, needsScopedSSR, styles } from '@platform'; +import { BUILD } from 'virtual:app-data'; +import { consoleError, loadModule, needsScopedSSR, styles } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; import { CMP_FLAGS, HOST_FLAGS } from '../utils/constants'; import { expandPartSelectors, scopeCss } from '../utils/shadow-css'; import { computeMode } from './mode'; @@ -53,7 +53,9 @@ export const initializeComponent = async ( Cstr = CstrImport as d.ComponentConstructor | undefined; } if (!Cstr) { - throw new Error(`Constructor for "${cmpMeta.$tagName$}#${hostRef.$modeName$}" was not found`); + throw new Error( + `Constructor for "${cmpMeta.$tagName$}#${hostRef.$modeName$}" was not found`, + ); } if (BUILD.member && !Cstr.isProxied) { // we've never proxied this Constructor before @@ -88,14 +90,14 @@ export const initializeComponent = async ( if (BUILD.member) { hostRef.$flags$ &= ~HOST_FLAGS.isConstructingInstance; } - if (BUILD.propChangeCallback) { - hostRef.$flags$ |= HOST_FLAGS.isWatchReady; - } + // Note: isWatchReady is now set in postUpdateComponent after componentDidLoad, + // per lifecycle docs that @Watch should only fire on subsequent prop changes. endNewInstance(); // For components that relocate slots, defer connectedCallback until after first render // so that slotted content is available - const needsDeferredCallback = BUILD.slotRelocation && cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation; + const needsDeferredCallback = + BUILD.slotRelocation && cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation; if (!needsDeferredCallback) { fireConnectedCallback(hostRef.$lazyInstance$, elm); } else { @@ -117,12 +119,9 @@ export const initializeComponent = async ( * * ``` */ - const cmpTag = elm.localName; - - // wait for the CustomElementRegistry to mark the component as ready before setting `isWatchReady`. Otherwise, - // watchers may fire prematurely if `customElements.get()`/`customElements.whenDefined()` resolves _before_ - // Stencil has completed instantiating the component. - customElements.whenDefined(cmpTag).then(() => (hostRef.$flags$ |= HOST_FLAGS.isWatchReady)); + // Note: cmpTag was previously used for whenDefined().then() to set isWatchReady, + // but isWatchReady is now set in postUpdateComponent after componentDidLoad, + // per lifecycle docs that @Watch should only fire on subsequent prop changes. } if (BUILD.style && Cstr && Cstr.style) { diff --git a/src/runtime/mixin.ts b/packages/core/src/runtime/mixin.ts similarity index 86% rename from src/runtime/mixin.ts rename to packages/core/src/runtime/mixin.ts index 01d40987c58..6fd33ec90a0 100644 --- a/src/runtime/mixin.ts +++ b/packages/core/src/runtime/mixin.ts @@ -1,4 +1,4 @@ -import { BUILD } from '@app-data'; +import { BUILD } from 'virtual:app-data'; type Ctor = new (...args: any[]) => T; diff --git a/packages/core/src/runtime/mode.ts b/packages/core/src/runtime/mode.ts new file mode 100644 index 00000000000..9059587956b --- /dev/null +++ b/packages/core/src/runtime/mode.ts @@ -0,0 +1,10 @@ +import { getHostRef, modeResolutionChain } from 'virtual:platform'; +import type * as d from '@stencil/core'; + +// Private +export const computeMode = (elm: d.HostElement) => + modeResolutionChain.map((h) => h(elm)).find((m) => !!m); + +// Public +export const setMode = (handler: d.ResolutionHandler) => modeResolutionChain.push(handler); +export const getMode = (ref: d.RuntimeRef) => getHostRef(ref)?.$modeName$; diff --git a/src/runtime/nonce.ts b/packages/core/src/runtime/nonce.ts similarity index 90% rename from src/runtime/nonce.ts rename to packages/core/src/runtime/nonce.ts index 752a9a85e60..21935612577 100644 --- a/src/runtime/nonce.ts +++ b/packages/core/src/runtime/nonce.ts @@ -1,4 +1,4 @@ -import { plt } from '@platform'; +import { plt } from 'virtual:platform'; /** * Assigns the given value to the nonce property on the runtime platform object. diff --git a/src/runtime/normalize-watchers.ts b/packages/core/src/runtime/normalize-watchers.ts similarity index 97% rename from src/runtime/normalize-watchers.ts rename to packages/core/src/runtime/normalize-watchers.ts index ac38eb90a01..8c94239e672 100644 --- a/src/runtime/normalize-watchers.ts +++ b/packages/core/src/runtime/normalize-watchers.ts @@ -1,4 +1,4 @@ -import type * as d from '../declarations'; +import type * as d from '@stencil/core'; /** * Normalizes watcher metadata to the current `{ [methodName]: flags }[]` format. diff --git a/src/runtime/parse-property-value.ts b/packages/core/src/runtime/parse-property-value.ts similarity index 77% rename from src/runtime/parse-property-value.ts rename to packages/core/src/runtime/parse-property-value.ts index 0ba0ed65d97..4a53d782fd6 100644 --- a/src/runtime/parse-property-value.ts +++ b/packages/core/src/runtime/parse-property-value.ts @@ -1,8 +1,7 @@ -import { BUILD } from '@app-data'; +import { BUILD } from 'virtual:app-data'; -import { MEMBER_FLAGS, SERIALIZED_PREFIX } from '../utils/constants'; +import { MEMBER_FLAGS } from '../utils/constants'; import { isComplexType } from '../utils/helpers'; -import { deserializeProperty } from '../utils/serialize'; /** * Parse a new property value for a given property type. @@ -28,20 +27,11 @@ import { deserializeProperty } from '../utils/serialize'; * @param isFormAssociated whether the component is form-associated (optional) * @returns the parsed/coerced value */ -export const parsePropertyValue = (propValue: unknown, propType: number, isFormAssociated?: boolean): any => { - /** - * Allow hydrate parameters that contain a complex non-serialized values. - * This is SSR-specific and should only run during hydration. - */ - if ( - (BUILD.hydrateClientSide || BUILD.hydrateServerSide) && - typeof propValue === 'string' && - propValue.startsWith(SERIALIZED_PREFIX) - ) { - propValue = deserializeProperty(propValue); - return propValue; - } - +export const parsePropertyValue = ( + propValue: unknown, + propType: number, + isFormAssociated?: boolean, +): any => { if (propValue != null && !isComplexType(propValue)) { /** * ensure this value is of the correct prop type @@ -65,7 +55,11 @@ export const parsePropertyValue = (propValue: unknown, propType: number, isFormA * force it to be a number */ if (BUILD.propNumber && propType & MEMBER_FLAGS.Number) { - return typeof propValue === 'string' ? parseFloat(propValue) : typeof propValue === 'number' ? propValue : NaN; + return typeof propValue === 'string' + ? parseFloat(propValue) + : typeof propValue === 'number' + ? propValue + : NaN; } /** diff --git a/src/runtime/platform-options.ts b/packages/core/src/runtime/platform-options.ts similarity index 93% rename from src/runtime/platform-options.ts rename to packages/core/src/runtime/platform-options.ts index 42e550464cd..8faf032f3f4 100644 --- a/src/runtime/platform-options.ts +++ b/packages/core/src/runtime/platform-options.ts @@ -1,4 +1,4 @@ -import { plt } from '@platform'; +import { plt } from 'virtual:platform'; interface SetPlatformOptions { raf?: (c: FrameRequestCallback) => number; diff --git a/src/runtime/profile.ts b/packages/core/src/runtime/profile.ts similarity index 96% rename from src/runtime/profile.ts rename to packages/core/src/runtime/profile.ts index 9b0058edcde..f50403f1c6b 100644 --- a/src/runtime/profile.ts +++ b/packages/core/src/runtime/profile.ts @@ -1,5 +1,5 @@ -import { BUILD } from '@app-data'; -import { getHostRef, win } from '@platform'; +import { BUILD } from 'virtual:app-data'; +import { getHostRef, win } from 'virtual:platform'; import { HOST_FLAGS } from '../utils/constants'; diff --git a/src/runtime/proxy-component.ts b/packages/core/src/runtime/proxy-component.ts similarity index 90% rename from src/runtime/proxy-component.ts rename to packages/core/src/runtime/proxy-component.ts index dd4de8f8009..bcba2fc83b7 100644 --- a/src/runtime/proxy-component.ts +++ b/packages/core/src/runtime/proxy-component.ts @@ -1,7 +1,7 @@ -import { BUILD } from '@app-data'; -import { consoleDevWarn, getHostRef, parsePropertyValue, plt } from '@platform'; +import { BUILD } from 'virtual:app-data'; +import { consoleDevWarn, getHostRef, parsePropertyValue, plt } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; import { CMP_FLAGS, HOST_FLAGS, MEMBER_FLAGS, WATCH_FLAGS } from '../utils/constants'; import { getPropertyDescriptor } from '../utils/get-prop-descriptor'; import { normalizeWatchers } from './normalize-watchers'; @@ -18,9 +18,9 @@ import { getValue, setValue } from './set-value'; * * On a traditional component, this is wired up to the element only. * - * @param Cstr the constructor for a component that we need to process - * @param cmpMeta metadata collected previously about the component - * @param flags a number used to store a series of bit flags + * @param Cstr - the constructor for a component that we need to process + * @param cmpMeta - metadata collected previously about the component + * @param flags - a number used to store a series of bit flags * @returns a reference to the same constructor passed in (but now mutated) */ export const proxyComponent = ( @@ -32,7 +32,6 @@ export const proxyComponent = ( if (BUILD.isTesting) { if (prototype.__stencilAugmented) { - // @ts-expect-error - we don't want to re-augment the prototype. This happens during spec tests. return; } prototype.__stencilAugmented = true; @@ -40,9 +39,13 @@ export const proxyComponent = ( /** * proxy form associated custom element lifecycle callbacks - * @ref https://web.dev/articles/more-capable-form-controls#lifecycle_callbacks + * @see https://web.dev/articles/more-capable-form-controls#lifecycle_callbacks */ - if (BUILD.formAssociated && cmpMeta.$flags$ & CMP_FLAGS.formAssociated && flags & PROXY_FLAGS.isElementConstructor) { + if ( + BUILD.formAssociated && + cmpMeta.$flags$ & CMP_FLAGS.formAssociated && + flags & PROXY_FLAGS.isElementConstructor + ) { FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS.forEach((cbName) => { const originalFormAssociatedCallback = prototype[cbName]; Object.defineProperty(prototype, cbName, { @@ -52,12 +55,16 @@ export const proxyComponent = ( if (!instance) { hostRef?.$onReadyPromise$?.then((asyncInstance: d.ComponentInterface) => { const cb = asyncInstance[cbName]; - typeof cb === 'function' && cb.call(asyncInstance, ...args); + if (typeof cb === 'function') { + cb.call(asyncInstance, ...args); + } }); } else { // Use the method on `instance` if `lazyLoad` is set, otherwise call the original method to avoid an infinite loop. const cb = BUILD.lazyLoad ? instance[cbName] : originalFormAssociatedCallback; - typeof cb === 'function' && cb.call(instance, ...args); + if (typeof cb === 'function') { + cb.call(instance, ...args); + } } }, }); @@ -88,7 +95,8 @@ export const proxyComponent = ( ) { // preserve any getters / setters that already exist on the prototype; // we'll call them via our new accessors. On a lazy component, this would only be called on the class instance. - const { get: origGetter, set: origSetter } = getPropertyDescriptor(prototype, memberName) || {}; + const { get: origGetter, set: origSetter } = + getPropertyDescriptor(prototype, memberName) || {}; if (origGetter) cmpMeta.$members$[memberName][0] |= MEMBER_FLAGS.Getter; if (origSetter) cmpMeta.$members$[memberName][0] |= MEMBER_FLAGS.Setter; @@ -263,7 +271,11 @@ export const proxyComponent = ( if (BUILD.observeAttribute && (!BUILD.lazyLoad || flags & PROXY_FLAGS.isElementConstructor)) { const attrNameToPropName = new Map(); - prototype.attributeChangedCallback = function (attrName: string, oldValue: string, newValue: string) { + prototype.attributeChangedCallback = function ( + attrName: string, + oldValue: string, + newValue: string, + ) { plt.jmp(() => { const propName = attrNameToPropName.get(attrName); const hostRef = getHostRef(this); @@ -308,10 +320,6 @@ export const proxyComponent = ( // by connectedCallback as this attributeChangedCallback will not fire. // // https://developers.google.com/web/fundamentals/web-components/best-practices#lazy-properties - // - // TODO(STENCIL-16) we should think about whether or not we actually want to be reflecting the attributes to - // properties here given that this goes against best practices outlined here - // https://developers.google.com/web/fundamentals/web-components/best-practices#avoid-reentrancy if (this.hasOwnProperty(propName) && BUILD.lazyLoad) { newValue = this[propName]; delete this[propName]; @@ -354,13 +362,18 @@ export const proxyComponent = ( } else if (propName == null) { // At this point we should know this is not a "member", so we can treat it like watching an attribute // on a vanilla web component - const flags = hostRef?.$flags$; + const hostFlags = hostRef?.$flags$; // We only want to trigger the callback(s) if: // 1. The instance is ready // 2. The watchers are ready // 3. The value has changed - if (hostRef && flags && !(flags & HOST_FLAGS.isConstructingInstance) && newValue !== oldValue) { + if ( + hostRef && + hostFlags && + !(hostFlags & HOST_FLAGS.isConstructingInstance) && + newValue !== oldValue + ) { const elm = BUILD.lazyLoad ? hostRef.$hostElement$ : this; const instance = BUILD.lazyLoad ? hostRef.$lazyInstance$ : (elm as any); const entry = cmpMeta.$watchers$?.[attrName]; @@ -368,9 +381,9 @@ export const proxyComponent = ( const [[watchMethodName, watcherFlags]] = Object.entries(watcher); if ( instance[watchMethodName] != null && - (flags & HOST_FLAGS.isWatchReady || watcherFlags & WATCH_FLAGS.Immediate) + (hostFlags & HOST_FLAGS.isWatchReady || watcherFlags & WATCH_FLAGS.Immediate) ) { - instance[watchMethodName].call(instance, newValue, oldValue, attrName); + instance[watchMethodName](newValue, oldValue, attrName); } }); } @@ -386,18 +399,23 @@ export const proxyComponent = ( // spurious. Without this guard the setter fires reentrant mid-render when // taskQueue:'immediate' is used, corrupting the vdom patch and crashing with // "Cannot read properties of null (reading 'nodeType')". - const isSpuriousBooleanRemoval = isBooleanTarget && newValue === null && this[propName] === undefined; + const isSpuriousBooleanRemoval = + isBooleanTarget && newValue === null && this[propName] === undefined; // special handling of boolean attributes. Null (removal) means false. // everything else means true (including an empty string if (isBooleanTarget) { - (newValue as any) = newValue === null || newValue === 'false' ? false : true; + (newValue as any) = !(newValue === null || newValue === 'false'); } // test whether this property either has no 'getter' or if it does, does it also have a 'setter' // before attempting to write back to component props const propDesc = Object.getOwnPropertyDescriptor(prototype, propName); - if (!isSpuriousBooleanRemoval && newValue != this[propName] && (!propDesc.get || !!propDesc.set)) { + if ( + !isSpuriousBooleanRemoval && + newValue != this[propName] && + (!propDesc.get || !!propDesc.set) + ) { this[propName] = newValue; } }); diff --git a/packages/core/src/runtime/readme.md b/packages/core/src/runtime/readme.md new file mode 100644 index 00000000000..cf445dc4476 --- /dev/null +++ b/packages/core/src/runtime/readme.md @@ -0,0 +1,149 @@ +# runtime + +Platform-agnostic core runtime for Stencil components. + +## Overview + +This directory contains the core logic that powers Stencil components at runtime: + +- **Reactivity** - `@Prop` and `@State` change detection +- **Virtual DOM** - Diffing and patching (`vdom/`) +- **Lifecycle** - Component initialization, updates, and teardown +- **Rendering** - JSX to DOM transformation + +## Architecture + +The runtime is platform-agnostic - it defines _what_ needs to happen but not _how_. Platform-specific behavior is provided by: + +- `client/` - Browser implementation (uses real DOM, `requestAnimationFrame`, etc.) +- `server/` - SSR implementation (uses mock-doc, synchronous rendering) + +Both platforms implement the `@platform` interface, allowing the same runtime code to work in both environments. + +## Key Files + +| File | Purpose | +| ------------------------- | ------------------------------------------- | +| `connected-callback.ts` | Component connection and ancestor detection | +| `initialize-component.ts` | First-time component setup | +| `update-component.ts` | Re-rendering and lifecycle dispatch | +| `set-value.ts` | Reactive property updates | +| `proxy-component.ts` | Property/attribute reflection | +| `vdom/` | Virtual DOM implementation | + +--- + +## Lifecycle Order Of Operations + +Component lifecycle events fire `componentWillLoad` from top to bottom, then fire `componentDidLoad` from bottom to top. It should take into account each component can finish lazy-loaded requests in any random order. Additionally, any `componentWillLoad` can return a promise that all child components should wait on until it's resolved, while still keeping the correct firing order. + +``` + + + + + + +cmp-a - componentWillLoad +cmp-b - componentWillLoad +cmp-c - componentWillLoad +cmp-c - componentDidLoad +cmp-b - componentDidLoad +cmp-a - componentDidLoad +``` + +## Hydrated CSS Visibility + +By default, components are assigned `visibility: hidden` using their tag name as the css selector. Therefore, before the components and their descendants have finished hydrating, each component is hidden by default. This is done to prevent janky flickering as components hydrate asynchronously. As each component fully loads the `hydrated` css class is then added. + +The `hydrated` css class that's added to the component assigns `visibility: inherit` style to the element. If any parent component is still hydrating then this component will not show until the top most component has added the `hydrated` css class. + +## Lifecycle Process + +- **Connect**: Synchronously within `connectedCallback`, each component looks for an ancestor component and adds itself as a child component if an ancestor is found. + - Climb up the parent elements with a while loop. + + - Stop at the first element that has an `s-init` function. + + - If the ancestor component we found hasn't ran its lifecycle update yet, then add this component to the ancestor's `s-al` set. The `s-al` is a set of child components that are actively loading. + + - If no ancestor component is found then continue without the component setting an ancestor component. + +- **Initialize Component**: Initialize the component for the first time within `initializeComponent`. + - If the component has already initialized loading then do nothing. Data to know if the component has started to initialize is in the host ref data, which ensures it doesn't try to initialize more than once. + + - Async request the lazy-loaded component constructor and await the response. + + - After the component implementation constructor request has been received, create a new instance of the component with the lazy-loaded constructor. + + - The constructor will directly wire the host element and lazy-loaded component instance together with the host ref data. + + - If the component has an ancestor component, but the ancestor hasn't ran its lifecycle update yet, then this component should not be initialized at this moment and shouldn't fire its `componentWillLoad` yet. Instead, this component should be added to the ancestor component's array of render callbacks `s-rc`, which would call `initializeComponent` again after its ready. Once the ancestor component has ran its lifecycle update, it'll then call all of its child render callbacks so that the `componentWillLoad` lifecycle events are in the correct order. + + - If there is no ancestor component, or the ancestor component has already rendered, then fire off the first update. + + - When ready, `updateComponent` will be added as an async write task and ran asynchronously. + +- **First Update**: The first component update and render from within `updateComponent`. + - Set the lifecycle ready value `s-lr` to `false` signifying that the lifecycle update is not ready for this component. + + - Fire off `componentWillLoad` lifecycle. + + - Fire off `componentWillRender` lifecycle. + + - Add scoped css data and classes for scoped encapsulation or shadow dom encapsulation without shadow dom browser support. + + - Attach shadow root for shadow dom components. + + - Attach styles to shadow root or document depending on encapsulation. + + - First render. + + - Set the lifecycle ready value `s-lr` to `true` signifying that the lifecycle update has happened and the component is now ready for child component lifecycles. + + - Fire off all of this component's child render callbacks within `s-rc`. Each of the child render callbacks will fire off their own initialize component process. + + - All component descendants should fire `componentWillLoad` lifecycle in the correct order, top to bottom. + + - Fire `postUpdateComponent`. The bottom most component will not have any child render callbacks, so at this point the `componentDidLoad` lifecycle events should start firing from bottom to top. + + - Fire off `componentDidLoad` lifecycle. + + - Fire off `componentDidRender` lifecycle. + + - Add `hydrated` css class signifying the component has finished loading. At this point this component has finished updating. + + - If the component has an ancestor component, then remove this component from its set of actively loading children in `s-al`. + + - After removing this component from the ancestor component's `s-al` set, if the set is now empty then fire the ancestor component's `s-init`. + + - Firing `s-init` on the ancestor component allows the ancestor to complete its first update and fire its own `componentDidLoad` lifecycle event, allowing for `componentDidLoad` lifecycles to fire bottom to top. + + - Fire all `componentOnReady` resolves. + +- **Subsequent Updates**: All subsequent component updates and re-renders from within `updateComponent`. + - Somehow `setValue` is triggered, either through a `Prop` or `State` update, or calling `forceUpdate()` on a component. If there is a change or a forced update, then `setValue` will add `updateComponent` to an async write task. + + - Fire `updateComponent` from async task queue. + + - Fire off `componentWillUpdate` lifecycle. + + - Fire off `componentWillRender` lifecycle. + + - Patch render. + + - Fire `postUpdateComponent`. + + - Fire off `componentDidUpdate` lifecycle. + + - Fire off `componentDidRender` lifecycle. + +## Property Descriptions + +`s-al`: A component's `Set` of child components that are actively loading. + +`s-init`: A function to be called by child components to finish initializing the component. + +`s-lr`: The component's lifecycle ready status. `true` if the component has finished its lifecycle update, falsy if it is actively updating and has not fired off either `componentWillLoad` or `componentWillUpdate`. + +`s-rc`: A component's array of child component render callbacks. After a component renders, it should then fire off all of its child component render callbacks. \ No newline at end of file diff --git a/packages/core/src/runtime/render.ts b/packages/core/src/runtime/render.ts new file mode 100644 index 00000000000..4ff23215791 --- /dev/null +++ b/packages/core/src/runtime/render.ts @@ -0,0 +1,54 @@ +import type * as d from '@stencil/core'; + +import { renderVdom } from './vdom/vdom-render'; + +/** + * A WeakMap to persist HostRef objects across multiple render() calls to the + * same container. This enables VNode diffing on re-renders — without it, each + * call creates a fresh HostRef with no previous VNode, causing renderVdom to + * replace the entire DOM subtree instead of patching only what changed. + */ +const hostRefCache = new WeakMap(); + +/** + * Method to render a virtual DOM tree to a container element. + * + * Supports efficient re-renders: calling `render()` again on the same container + * will diff the new VNode tree against the previous one and only update what changed, + * preserving existing DOM elements and their state. + * + * @example + * ```tsx + * import { render } from '@stencil/core'; + * + * const vnode = ( + *
+ *

Hello, world!

+ *
+ * ); + * render(vnode, document.body); + * ``` + * + * @param vnode - The virtual DOM tree to render + * @param container - The container element to render the virtual DOM tree to + */ +export function render(vnode: d.VNode, container: Element) { + let ref = hostRefCache.get(container); + + if (!ref) { + const cmpMeta: d.ComponentRuntimeMeta = { + $flags$: 0, + $tagName$: container.tagName, + }; + + ref = { + $flags$: 0, + $cmpMeta$: cmpMeta, + $hostElement$: container as d.HostElement, + }; + + hostRefCache.set(container, ref); + } + + renderVdom(ref, vnode); +} diff --git a/packages/core/src/runtime/runtime-constants.ts b/packages/core/src/runtime/runtime-constants.ts new file mode 100644 index 00000000000..ea331af8a3e --- /dev/null +++ b/packages/core/src/runtime/runtime-constants.ts @@ -0,0 +1,90 @@ +/** + * Bit flags for recording various properties of VDom nodes + */ +export const VNODE_FLAGS = { + /** + * Whether or not a vdom node is a slot reference + */ + isSlotReference: 1 << 0, + + /** + * Whether or not a slot element has fallback content + */ + isSlotFallback: 1 << 1, + + /** + * Whether or not an element is a host element + */ + isHost: 1 << 2, +} as const; + +export const PROXY_FLAGS = { + isElementConstructor: 1 << 0, + proxyState: 1 << 1, +} as const; + +// PLATFORM_FLAGS base values +const PF_appLoaded = 1 << 1; +const PF_queueSync = 1 << 2; + +export const PLATFORM_FLAGS = { + /** + * designates a node in the DOM as being actively moved by the runtime + */ + isTmpDisconnected: 1 << 0, + appLoaded: PF_appLoaded, + queueSync: PF_queueSync, + + queueMask: PF_appLoaded | PF_queueSync, +} as const; + +/** + * A (subset) of node types which are relevant for the Stencil runtime. These + * values are based on the values which can possibly be returned by the + * `.nodeType` property of a DOM node. See here for details: + * + * {@link https://dom.spec.whatwg.org/#ref-for-dom-node-nodetype%E2%91%A0} + */ +export const NODE_TYPE = { + ElementNode: 1, + TextNode: 3, + CommentNode: 8, + DocumentNode: 9, + DocumentTypeNode: 10, + DocumentFragment: 11, +} as const; + +export const CONTENT_REF_ID = 'r'; +export const ORG_LOCATION_ID = 'o'; +export const SLOT_NODE_ID = 's'; +export const TEXT_NODE_ID = 't'; +export const COMMENT_NODE_ID = 'c'; + +export const HYDRATE_ID = 's-id'; +export const HYDRATED_STYLE_ID = 'sty-id'; +export const HYDRATE_CHILD_ID = 'c-id'; +export const HYDRATED_CSS = '{visibility:hidden}.hydrated{visibility:inherit}'; + +export const STENCIL_DOC_DATA = '_stencilDocData'; +export const DEFAULT_DOC_DATA = { + hostIds: 0, + rootLevelIds: 0, + staticComponents: new Set(), +}; + +/** + * Constant for styles to be globally applied to `slot-fb` elements for pseudo-slot behavior. + * + * Two cascading rules must be used instead of a `:not()` selector due to Stencil browser + * support as of Stencil v4. + */ +export const SLOT_FB_CSS = 'slot-fb{display:contents}slot-fb[hidden]{display:none}'; + +export const XLINK_NS = 'http://www.w3.org/1999/xlink'; + +export const FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS = [ + 'formAssociatedCallback', + 'formResetCallback', + 'formDisabledCallback', + 'formStateRestoreCallback', +] as const; diff --git a/src/runtime/set-value.ts b/packages/core/src/runtime/set-value.ts similarity index 76% rename from src/runtime/set-value.ts rename to packages/core/src/runtime/set-value.ts index b33c536d7a6..ccdd2be5f6b 100644 --- a/src/runtime/set-value.ts +++ b/packages/core/src/runtime/set-value.ts @@ -1,40 +1,34 @@ -import { BUILD } from '@app-data'; -import { consoleDevWarn, consoleError, getHostRef } from '@platform'; +import { BUILD } from 'virtual:app-data'; +import { consoleDevWarn, consoleError, getHostRef } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; import { CMP_FLAGS, HOST_FLAGS, WATCH_FLAGS } from '../utils/constants'; import { parsePropertyValue } from './parse-property-value'; import { scheduleUpdate } from './update-component'; -export const getValue = (ref: d.RuntimeRef, propName: string) => getHostRef(ref).$instanceValues$.get(propName); +export const getValue = (ref: d.RuntimeRef, propName: string) => + getHostRef(ref).$instanceValues$.get(propName); -export const setValue = (ref: d.RuntimeRef, propName: string, newVal: any, cmpMeta: d.ComponentRuntimeMeta) => { +export const setValue = ( + ref: d.RuntimeRef, + propName: string, + newVal: any, + cmpMeta: d.ComponentRuntimeMeta, +) => { // check our new property value against our internal value const hostRef = getHostRef(ref); if (!hostRef) { + // Todo(STENCIL-1308): remove once a solution for this was identified and implemented + if (BUILD.lazyLoad) { + throw new Error( + BUILD.isDev + ? `Couldn't find host element for "${cmpMeta.$tagName$}". This usually happens when integrating a 3rd party Stencil component with another Stencil runtime. See https://github.com/stenciljs/core/issues/5457` + : `Host element not found for "${cmpMeta.$tagName$}"`, + ); + } return; } - /** - * If the host element is not found, let's fail with a better error message and provide - * details on why this may happen. In certain cases, e.g. see https://github.com/stenciljs/core/issues/5457, - * users might import a component through e.g. a loader script, which causes confusions in runtime - * as there are multiple runtimes being loaded and/or different components used with different - * loading strategies, e.g. lazy vs implicitly loaded. - * - * Todo(STENCIL-1308): remove, once a solution for this was identified and implemented - */ - if (BUILD.lazyLoad && !hostRef) { - throw new Error( - `Couldn't find host element for "${cmpMeta.$tagName$}" as it is ` + - 'unknown to this Stencil runtime. This usually happens when integrating ' + - 'a 3rd party Stencil component with another Stencil component or application. ' + - 'Please reach out to the maintainers of the 3rd party Stencil component or report ' + - 'this on the Stencil Discord server (https://chat.stenciljs.com) or comment ' + - 'on this similar [GitHub issue](https://github.com/stenciljs/core/issues/5457).', - ); - } - if ( BUILD.serializer && hostRef.$serializerValues$.has(propName) && @@ -59,7 +53,10 @@ export const setValue = (ref: d.RuntimeRef, propName: string, newVal: any, cmpMe // explicitly check for NaN on both sides, as `NaN === NaN` is always false const areBothNaN = Number.isNaN(oldVal) && Number.isNaN(newVal); const didValueChange = newVal !== oldVal && !areBothNaN; - if ((!BUILD.lazyLoad || !(flags & HOST_FLAGS.isConstructingInstance) || oldVal === undefined) && didValueChange) { + if ( + (!BUILD.lazyLoad || !(flags & HOST_FLAGS.isConstructingInstance) || oldVal === undefined) && + didValueChange + ) { // gadzooks! the property's value has changed!! // set our new value! hostRef.$instanceValues$.set(propName, newVal); diff --git a/src/runtime/slot-polyfill-utils.ts b/packages/core/src/runtime/slot-polyfill-utils.ts similarity index 89% rename from src/runtime/slot-polyfill-utils.ts rename to packages/core/src/runtime/slot-polyfill-utils.ts index 76f2d793443..8d359476e87 100644 --- a/src/runtime/slot-polyfill-utils.ts +++ b/packages/core/src/runtime/slot-polyfill-utils.ts @@ -1,6 +1,6 @@ -import { BUILD } from '@app-data'; +import { BUILD } from 'virtual:app-data'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; import { internalCall } from './dom-extras'; import { NODE_TYPE } from './runtime-constants'; @@ -40,7 +40,10 @@ export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { let i = 0; for (i = 0; i < childNodes.length; i++) { const childNode = childNodes[i] as d.RenderNode; - if (childNode.nodeType === NODE_TYPE.ElementNode && internalCall(childNode, 'childNodes').length) { + if ( + childNode.nodeType === NODE_TYPE.ElementNode && + internalCall(childNode, 'childNodes').length + ) { // keep drilling down updateFallbackSlotVisibility(childNode); } @@ -53,7 +56,7 @@ export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { * corresponding slot location node which points to the slotted node (via `['s-nr']`). * * This is only required until all patches are unified / switched on all the time (then we can rely on `childNodes`) - * either under 'experimentalSlotFixes' or on by default + * either under 'lightDomPatches' or on by default * @param childNodes all 'internal' child nodes of the component * @returns An array of slotted reference nodes. */ @@ -75,7 +78,11 @@ export const getSlottedChildNodes = (childNodes: NodeListOf): d.Patch * @param slotName the name of the slot to match on. * @returns a reference to the slot node that matches the provided name, `null` otherwise */ -export function getHostSlotNodes(childNodes: NodeListOf, hostName?: string, slotName?: string) { +export function getHostSlotNodes( + childNodes: NodeListOf, + hostName?: string, + slotName?: string, +) { let i = 0; let slottedNodes: d.RenderNode[] = []; let childNode: d.RenderNode; @@ -108,7 +115,8 @@ export const getSlotChildSiblings = (slot: d.RenderNode, slotName: string, inclu let node = slot; while ((node = node.nextSibling as any)) { - if (getSlotName(node) === slotName && (includeSlot || !node['s-sr'])) childNodes.push(node as any); + if (getSlotName(node) === slotName && (includeSlot || !node['s-sr'])) + childNodes.push(node as any); } return childNodes; }; @@ -167,7 +175,9 @@ export const addSlotRelocateNode = ( if (!slotNode['s-cr'] || !slotNode['s-cr'].parentNode) return; const parent = slotNode['s-cr'].parentNode as any; - const appendMethod = prepend ? internalCall(parent, 'prepend') : internalCall(parent, 'appendChild'); + const appendMethod = prepend + ? internalCall(parent, 'prepend') + : internalCall(parent, 'appendChild'); if (BUILD.hydrateClientSide && typeof position !== 'undefined') { slottedNodeLocation['s-oo'] = position; @@ -210,16 +220,20 @@ export function patchSlotNode(node: d.RenderNode) { const slotName = this['s-sn']; if (opts?.flatten) { - console.error(` - Flattening is not supported for Stencil non-shadow slots. - You can use \`.childNodes\` to nested slot fallback content. - If you have a particular use case, please open an issue on the Stencil repo. - `); + if (BUILD.isDev) { + console.error( + 'Flattening is not supported for Stencil non-shadow slots. You can use `.childNodes` for nested slot fallback content.', + ); + } else { + console.error('Flattening not supported for Stencil non-shadow slots'); + } } const parent = this['s-cr'].parentElement as d.RenderNode; // get all light dom nodes - const slottedNodes = parent.__childNodes ? parent.childNodes : getSlottedChildNodes(parent.childNodes); + const slottedNodes = parent.__childNodes + ? parent.childNodes + : getSlottedChildNodes(parent.childNodes); (slottedNodes as d.RenderNode[]).forEach((n) => { // find all the nodes assigned to slots we care about @@ -244,7 +258,10 @@ export function patchSlotNode(node: d.RenderNode) { * @param elm the slot node to dispatch the event from */ export function dispatchSlotChangeEvent(elm: d.RenderNode) { - elm.dispatchEvent(new CustomEvent('slotchange', { bubbles: false, cancelable: false, composed: false })); + (elm as any).name = elm['s-sn'] || ''; + elm.dispatchEvent( + new CustomEvent('slotchange', { bubbles: false, cancelable: false, composed: false }), + ); } /** diff --git a/packages/core/src/runtime/styles.ts b/packages/core/src/runtime/styles.ts new file mode 100644 index 00000000000..eb286b505dd --- /dev/null +++ b/packages/core/src/runtime/styles.ts @@ -0,0 +1,432 @@ +import { BUILD } from 'virtual:app-data'; +import { + plt, + styles, + supportsConstructableStylesheets, + supportsMutableAdoptedStyleSheets, + win, + writeTask, +} from 'virtual:platform'; +import type * as d from '@stencil/core'; + +import { CMP_FLAGS } from '../utils/constants'; +import { queryNonceMetaTagContent } from '../utils/query-nonce-meta-tag-content'; +import { getShadowRoot } from './element'; +import { createTime } from './profile'; +import { HYDRATED_STYLE_ID, NODE_TYPE, SLOT_FB_CSS } from './runtime-constants'; + +export const rootAppliedStyles: d.RootAppliedStyleMap = /*@__PURE__*/ new WeakMap(); + +/** + * Get or initialize the set of applied style scope IDs for a container element. + * + * @param container the container element to track styles for + * @returns the set of applied scope IDs + */ +const getAppliedStyles = (container: Element): Set => { + let applied = rootAppliedStyles.get(container); + if (!applied) { + applied = new Set(); + rootAppliedStyles.set(container, applied); + } + return applied; +}; + +/** + * Safely adopt a stylesheet into a container's adoptedStyleSheets. + * Handles both mutable and immutable adoptedStyleSheets arrays. + * + * @param container the shadow root or document to adopt styles into + * @param sheet the CSSStyleSheet to adopt + * @param prepend if true, add to beginning; if false, add to end + */ +const adoptStylesheet = ( + container: ShadowRoot | Document, + sheet: CSSStyleSheet, + prepend: boolean = false, +) => { + if (supportsMutableAdoptedStyleSheets) { + if (prepend) { + container.adoptedStyleSheets.unshift(sheet); + } else { + container.adoptedStyleSheets.push(sheet); + } + } else { + if (prepend) { + container.adoptedStyleSheets = [sheet, ...container.adoptedStyleSheets]; + } else { + container.adoptedStyleSheets = [...container.adoptedStyleSheets, sheet]; + } + } +}; + +/** + * Create a CSSStyleSheet for the correct window context. + * Constructable stylesheets can't be shared between windows, + * so we need to create one for the current window. + * + * @param container the container node (used to determine the window context) + * @param cssText the CSS text to populate the stylesheet with + * @returns a new CSSStyleSheet for the correct window + */ +const createStylesheetForWindow = (container: Node, cssText: string): CSSStyleSheet => { + const currentWindow = ((container as Document).defaultView ?? + (container as Element).ownerDocument?.defaultView ?? + win) as Window & typeof globalThis; + const sheet = new currentWindow.CSSStyleSheet(); + sheet.replaceSync(cssText); + return sheet; +}; + +/** + * Get the style for a component, appending slot fallback CSS if needed. + * Returns a new value without mutating the cached style. + * + * @param style - the style string or CSSStyleSheet to process + * @returns the style (string or CSSStyleSheet) with slot CSS appended if needed, or undefined + */ +const getStyleWithSlotCss = ( + style: string | CSSStyleSheet | undefined, +): string | CSSStyleSheet | undefined => { + // Component needs slot fallback CSS + if (!style) { + return SLOT_FB_CSS; + } + if (typeof style === 'string') { + return style + SLOT_FB_CSS; + } + + return style; +}; + +/** + * Register the styles for a component by creating a stylesheet and then + * registering it under the component's scope ID in a `WeakMap` for later use. + * + * If constructable stylesheet are not supported or `allowCS` is set to + * `false` then the styles will be registered as a string instead. + * + * @param scopeId the scope ID for the component of interest + * @param cssText styles for the component of interest + * @param allowCS whether or not to use a constructable stylesheet + */ +export const registerStyle = (scopeId: string, cssText: string, allowCS: boolean) => { + if (supportsConstructableStylesheets && allowCS) { + const sheet = (styles.get(scopeId) as CSSStyleSheet) ?? new CSSStyleSheet(); + sheet.replaceSync(cssText); + styles.set(scopeId, sheet); + } else { + styles.set(scopeId, cssText); + } +}; + +/** + * Attach the styles for a given component to the DOM + * + * If the element uses shadow or is already attached to the DOM then we can + * create a stylesheet inside of its associated document fragment, otherwise + * we'll stick the stylesheet into the document head. + * + * @param styleContainerNode the node within which a style element for the + * component of interest should be added + * @param cmpMeta runtime metadata for the component of interest + * @param mode an optional current mode + * @returns the scope ID for the component of interest + */ +export const addStyle = ( + styleContainerNode: any, + cmpMeta: d.ComponentRuntimeMeta, + mode?: string, +) => { + const scopeId = getScopeId(cmpMeta, mode); + + if (!win.document) return scopeId; + + let style = styles.get(scopeId); + + if (cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation) { + style = getStyleWithSlotCss(style); + } + + // Determine the style container: + // - Keep shadow roots (DocumentFragment) as-is + // - For closed shadow DOM during SSR, the host element is passed directly - keep it + // - Otherwise, fallback to the document (for when element is not connected) + const isClosedShadowSSR = + BUILD.hydrateServerSide && + BUILD.shadowModeClosed && + cmpMeta.$flags$ & CMP_FLAGS.shadowNeedsScopedCss && + cmpMeta.$flags$ & CMP_FLAGS.shadowModeClosed; + + if (styleContainerNode.nodeType !== NODE_TYPE.DocumentFragment && !isClosedShadowSSR) { + styleContainerNode = win.document; + } + + if (style) { + if (typeof style === 'string') { + styleContainerNode = styleContainerNode.head || (styleContainerNode as HTMLElement); + const appliedStyles = getAppliedStyles(styleContainerNode); + let styleElm: HTMLStyleElement; + + // Check if style element already exists (for HMR updates) + // For shadow DOM components, directly update their dedicated style element + // For scoped components, check if they have their own HMR-created style element + const existingStyleElm: HTMLStyleElement = + (BUILD.hydrateClientSide || BUILD.hotModuleReplacement) && + styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`); + + if (existingStyleElm) { + // Update existing style element (for hydration or HMR) + existingStyleElm.textContent = style; + } else if (!appliedStyles.has(scopeId)) { + styleElm = win.document.createElement('style'); + styleElm.textContent = style; + + // Apply CSP nonce to the style tag if it exists + const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document); + if (nonce != null) { + styleElm.setAttribute('nonce', nonce); + } + + if ( + (BUILD.hydrateServerSide && + (cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation || + cmpMeta.$flags$ & CMP_FLAGS.shadowNeedsScopedCss || + cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation)) || + BUILD.hotModuleReplacement + ) { + styleElm.setAttribute(HYDRATED_STYLE_ID, scopeId); + } + + // Mark elements where slot-fb CSS was appended so the HMR updater + // knows to re-append it when the style text is replaced + if (BUILD.hotModuleReplacement && cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation) { + styleElm.setAttribute('data-slot-fb', ''); + } + + /** + * attach styles at the end of the head tag if we render scoped components + */ + if (!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation)) { + if (styleContainerNode.nodeName === 'HEAD') { + /** + * if the page contains preconnect links, we want to insert the styles + * after the last preconnect link to ensure the styles are preloaded + */ + const preconnectLinks = styleContainerNode.querySelectorAll('link[rel=preconnect]'); + const referenceNode = + preconnectLinks.length > 0 + ? preconnectLinks[preconnectLinks.length - 1].nextSibling + : styleContainerNode.querySelector('style'); + (styleContainerNode as HTMLElement).insertBefore( + styleElm, + referenceNode?.parentNode === styleContainerNode ? referenceNode : null, + ); + } else if ('host' in styleContainerNode) { + if (supportsConstructableStylesheets) { + // Scoped component in shadow root: create stylesheet and prepend to adoptedStyleSheets + const stylesheet = createStylesheetForWindow(styleContainerNode, style); + adoptStylesheet(styleContainerNode, stylesheet, true); + } else { + /** + * If a scoped component is used within a shadow root and constructable stylesheets are + * not supported, we want to insert the styles at the beginning of the shadow root node. + * + * However, if there is already a style node in the shadow root, we just append + * the styles to the existing node. + * + * Note: order of how styles are applied is important. The new style node + * should be inserted before the existing style node. + * + * During HMR, create separate style elements for scoped components so they can be + * updated independently without affecting other components' styles. + */ + const existingStyleContainer: HTMLStyleElement = + styleContainerNode.querySelector('style'); + if (existingStyleContainer && !BUILD.hotModuleReplacement) { + existingStyleContainer.textContent = style + existingStyleContainer.textContent; + } else { + (styleContainerNode as HTMLElement).prepend(styleElm); + } + } + } else { + styleContainerNode.append(styleElm); + } + } + + /** + * attach styles at the beginning of a shadow root node if we render shadow components + */ + if (cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) { + // For closed shadow DOM SSR (styles inlined into host element), prepend to be first child + // For regular shadow DOM, append to the shadow root + if (isClosedShadowSSR) { + (styleContainerNode as HTMLElement).prepend(styleElm); + } else { + styleContainerNode.insertBefore(styleElm, null); + } + } + + if (appliedStyles) { + appliedStyles.add(scopeId); + } + } + } else if (BUILD.constructableCSS) { + const appliedStyles = getAppliedStyles(styleContainerNode); + if (!appliedStyles.has(scopeId)) { + // Ensure stylesheet is for the correct window context + const currentWindow = (styleContainerNode.defaultView ?? + styleContainerNode.ownerDocument.defaultView) as Window & typeof globalThis; + let stylesheet: CSSStyleSheet; + if (style.constructor === currentWindow.CSSStyleSheet) { + stylesheet = style; + } else { + // Copy rules to a new stylesheet for this window + stylesheet = new currentWindow.CSSStyleSheet(); + for (let i = 0; i < style.cssRules.length; i++) { + stylesheet.insertRule(style.cssRules[i].cssText, i); + } + } + adoptStylesheet(styleContainerNode, stylesheet); + appliedStyles.add(scopeId); + + // Remove SSR style element from shadow root now that adoptedStyleSheets is in use + // Only remove from shadow roots, not from document head (for scoped components) + if (BUILD.hydrateClientSide && 'host' in styleContainerNode) { + const ssrStyleElm = styleContainerNode.querySelector( + `[${HYDRATED_STYLE_ID}="${scopeId}"]`, + ); + if (ssrStyleElm) { + writeTask(() => ssrStyleElm.remove()); + } + } + } + } + } + + return scopeId; +}; + +/** + * Add styles for a given component to the DOM, optionally handling 'scoped' + * encapsulation by adding an appropriate class name to the host element. + * + * @param hostRef the host reference for the component of interest + */ +export const attachStyles = (hostRef: d.HostRef) => { + const cmpMeta = hostRef.$cmpMeta$; + const elm = hostRef.$hostElement$; + const flags = cmpMeta.$flags$; + const endAttachStyles = createTime('attachStyles', cmpMeta.$tagName$); + + // Determine the style container: + // - For shadow DOM components with a shadow root, use the shadow root + // - For closed shadow DOM during SSR (shadowNeedsScopedCss + shadowModeClosed), use the host element + // so styles are inlined with the component for proper serialization + // - For regular scoped components, use the document root + let styleContainerNode: ShadowRoot | HTMLElement; + const shadowRoot = BUILD.shadowDom ? getShadowRoot(elm) : null; + + if (shadowRoot) { + styleContainerNode = shadowRoot; + } else if ( + BUILD.hydrateServerSide && + BUILD.shadowModeClosed && + flags & CMP_FLAGS.shadowNeedsScopedCss && + flags & CMP_FLAGS.shadowModeClosed + ) { + // Closed shadow DOM with scoped CSS during SSR: inline styles into the host element + styleContainerNode = elm; + } else { + styleContainerNode = elm.getRootNode() as ShadowRoot; + } + + const scopeId = addStyle(styleContainerNode, cmpMeta, hostRef.$modeName$); + + if ( + (BUILD.shadowDom || BUILD.scoped) && + BUILD.cssAnnotations && + flags & CMP_FLAGS.needsScopedEncapsulation + ) { + // only required when we're NOT using native shadow dom (slot) + // or this browser doesn't support native shadow dom + // and this host element was NOT created with SSR + // let's pick out the inner content for slot projection + // create a node to represent where the original + // content was first placed, which is useful later on + // DOM WRITE!! + elm['s-sc'] = scopeId; + elm.classList.add(scopeId + '-h'); + } + endAttachStyles(); +}; + +/** + * Get the scope ID for a given component + * + * @param cmp runtime metadata for the component of interest + * @param mode the current mode (optional) + * @returns a scope ID for the component of interest + */ +export const getScopeId = (cmp: d.ComponentRuntimeMeta, mode?: string) => + 'sc-' + + (BUILD.mode && mode && cmp.$flags$ & CMP_FLAGS.hasMode + ? cmp.$tagName$ + '-' + mode + : cmp.$tagName$); + +/** + * Convert a 'scoped' CSS string to one appropriate for use in the shadow DOM. + * + * Given a 'scoped' CSS string that looks like this: + * + * ``` + * /*!@div*\/div.class-name { display: flex }; + * ``` + * + * Convert it to a 'shadow' appropriate string, like so: + * + * ``` + * /*!@div*\/div.class-name { display: flex } + * ─┬─ ────────┬──────── + * │ │ + * │ ┌─────────────────┘ + * ▼ ▼ + * div{ display: flex } + * ``` + * + * Note that forward-slashes in the above are escaped so they don't end the + * comment. + * + * @param css a CSS string to convert + * @returns the converted string + */ +const convertScopedToShadow = (css: string) => css.replace(/\/\*!@([^/]+)\*\/[^{]+\{/g, '$1{'); + +/** + * Hydrate styles after SSR for components *not* using DSD. Convert 'scoped' styles to 'shadow' + * and add them to a constructable stylesheet. + * + * @returns void + */ +export const hydrateScopedToShadow = () => { + if (!win.document) { + return; + } + + const styleElements = win.document.querySelectorAll(`[${HYDRATED_STYLE_ID}]`); + let i = 0; + for (; i < styleElements.length; i++) { + registerStyle( + styleElements[i].getAttribute(HYDRATED_STYLE_ID), + convertScopedToShadow(styleElements[i].innerHTML), + true, + ); + } +}; + +declare global { + export interface CSSStyleSheet { + replaceSync(cssText: string): void; + replace(cssText: string): Promise; + } +} diff --git a/packages/core/src/runtime/tag-transform.ts b/packages/core/src/runtime/tag-transform.ts new file mode 100644 index 00000000000..3c8d82a92e5 --- /dev/null +++ b/packages/core/src/runtime/tag-transform.ts @@ -0,0 +1,21 @@ +import type * as d from '@stencil/core'; + +let tagTransformer: d.TagTransformer | undefined = undefined; + +/** + * Transforms a tag name using the current tag transformer + * @param tag - the tag to transform e.g. `my-tag` + * @returns the transformed tag e.g. `new-my-tag` + */ +export function transformTag(tag: T): T { + if (!tagTransformer) return tag; + return tagTransformer(tag) as T; +} + +/** + * Sets the tag transformer to be used when rendering custom elements + * @param transformer the transformer function to use. Must return a string + */ +export function setTagTransformer(transformer: d.TagTransformer) { + tagTransformer = transformer; +} diff --git a/src/runtime/update-component.ts b/packages/core/src/runtime/update-component.ts similarity index 83% rename from src/runtime/update-component.ts rename to packages/core/src/runtime/update-component.ts index 3718cfecb03..99003ce5281 100644 --- a/src/runtime/update-component.ts +++ b/packages/core/src/runtime/update-component.ts @@ -1,7 +1,7 @@ -import { BUILD, NAMESPACE } from '@app-data'; -import { Build, consoleError, getHostRef, nextTick, plt, win, writeTask } from '@platform'; +import { BUILD, NAMESPACE } from 'virtual:app-data'; +import { Build, consoleError, getHostRef, nextTick, plt, win, writeTask } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; import { CMP_FLAGS, HOST_FLAGS } from '../utils/constants'; import { emitEvent } from './event-emitter'; import { createTime } from './profile'; @@ -10,7 +10,12 @@ import { attachStyles } from './styles'; import { renderVdom } from './vdom/vdom-render'; export const attachToAncestor = (hostRef: d.HostRef, ancestorComponent?: d.HostElement) => { - if (BUILD.asyncLoading && ancestorComponent && !hostRef.$onRenderResolve$ && ancestorComponent['s-p']) { + if ( + BUILD.asyncLoading && + ancestorComponent && + !hostRef.$onRenderResolve$ && + ancestorComponent['s-p'] + ) { const index = ancestorComponent['s-p'].push( new Promise( (r) => @@ -23,7 +28,10 @@ export const attachToAncestor = (hostRef: d.HostRef, ancestorComponent?: d.HostE } }; -export const scheduleUpdate = (hostRef: d.HostRef, isInitialLoad: boolean): Promise | void => { +export const scheduleUpdate = ( + hostRef: d.HostRef, + isInitialLoad: boolean, +): Promise | void => { if (BUILD.taskQueue && BUILD.updatable) { hostRef.$flags$ |= HOST_FLAGS.isQueuedForUpdate; } @@ -75,11 +83,12 @@ const dispatchHooks = (hostRef: d.HostRef, isInitialLoad: boolean): Promise with invalid Stencil runtime! ` + - 'Make sure this imported component is compiled with a `externalRuntime: true` flag. ' + - 'For more information, please refer to https://stenciljs.com/docs/custom-elements#externalruntime', - ); + if (BUILD.isDev) { + throw new Error( + `Can't render <${elm.tagName.toLowerCase()} /> — compiled without externalRuntime: true. See https://stenciljs.com/docs/custom-elements#externalruntime`, + ); + } + return; } // We're going to use this variable together with `enqueue` to implement a @@ -107,7 +116,9 @@ const dispatchHooks = (hostRef: d.HostRef, isInitialLoad: boolean): Promise safeCall(instance, methodName, event, elm)); + hostRef.$queuedListeners$.map(([methodName, event]) => + safeCall(instance, methodName, event, elm), + ); hostRef.$queuedListeners$ = undefined; } } @@ -117,7 +128,9 @@ const dispatchHooks = (hostRef: d.HostRef, isInitialLoad: boolean): Promise cb(elm)); } } - emitLifecycleEvent(elm, 'componentWillLoad'); + if (BUILD.lifecycleDOMEvents) { + emitLifecycleEvent(elm, 'componentWillLoad'); + } // If `componentWillLoad` returns a `Promise` then we want to wait on // whatever's going on in that `Promise` before we launch into // rendering the component, doing other lifecycle stuff, etc. So @@ -125,7 +138,9 @@ const dispatchHooks = (hostRef: d.HostRef, isInitialLoad: boolean): Promise safeCall(instance, 'componentWillRender', undefined, elm)); + if (BUILD.lifecycleDOMEvents) { + emitLifecycleEvent(elm, 'componentWillRender'); + } + maybePromise = enqueue(maybePromise, () => + safeCall(instance, 'componentWillRender', undefined, elm), + ); endSchedule(); @@ -159,13 +178,19 @@ const dispatchHooks = (hostRef: d.HostRef, isInitialLoad: boolean): Promise | undefined, fn: () => Promise): Promise | undefined => - isPromisey(maybePromise) - ? maybePromise.then(fn).catch((err) => { - console.error(err); - fn(); - }) - : fn(); +const enqueue = ( + maybePromise: Promise | undefined, + fn: () => Promise | void, +): Promise | undefined => { + if (isPromisey(maybePromise)) { + return maybePromise.then(fn).catch((err) => { + console.error(err); + fn(); + }); + } + const result = fn(); + return isPromisey(result) ? result : undefined; +}; /** * Check that a value is a `Promise`. To check, we first see if the value is an @@ -179,7 +204,9 @@ const enqueue = (maybePromise: Promise | undefined, fn: () => Promise | unknown): maybePromise is Promise => maybePromise instanceof Promise || - (maybePromise && (maybePromise as any).then && typeof (maybePromise as Promise).then === 'function'); + (maybePromise && + (maybePromise as any).then && + typeof (maybePromise as Promise).then === 'function'); /** * Update a component given reference to its host elements and so on. @@ -190,12 +217,13 @@ const isPromisey = (maybePromise: Promise | unknown): maybePromise is Prom * rendered * @param isInitialLoad whether or not this function is being called as part of * the first render cycle + * @returns a `Promise` if the update is asynchronous, otherwise `void` */ -const updateComponent = async ( +const updateComponent = ( hostRef: d.HostRef, instance: d.HostElement | d.ComponentInterface, isInitialLoad: boolean, -) => { +): Promise | void => { const elm = hostRef.$hostElement$ as d.RenderNode; const endUpdate = createTime('update', hostRef.$cmpMeta$.$tagName$); const rc = elm['s-rc']; @@ -210,11 +238,23 @@ const updateComponent = async ( } if (BUILD.hydrateServerSide) { - await callRender(hostRef, instance, elm, isInitialLoad); - } else { - callRender(hostRef, instance, elm, isInitialLoad); + return (callRender(hostRef, instance, elm, isInitialLoad) as Promise).then(() => { + afterRender(hostRef, elm, rc, isInitialLoad, endRender, endUpdate); + }); } + callRender(hostRef, instance, elm, isInitialLoad); + afterRender(hostRef, elm, rc, isInitialLoad, endRender, endUpdate); +}; + +const afterRender = ( + hostRef: d.HostRef, + elm: d.RenderNode, + rc: (() => void)[] | undefined, + isInitialLoad: boolean, + endRender: () => void, + endUpdate: () => void, +) => { if (BUILD.isDev) { hostRef.$renderCount$ = hostRef.$renderCount$ === undefined ? 1 : hostRef.$renderCount$ + 1; hostRef.$flags$ &= ~HOST_FLAGS.devOnRender; @@ -278,14 +318,19 @@ let renderingRef: any = null; * @param isInitialLoad whether or not this function is being called as part of * @returns an empty promise */ -const callRender = (hostRef: d.HostRef, instance: any, elm: HTMLElement, isInitialLoad: boolean) => { +const callRender = ( + hostRef: d.HostRef, + instance: any, + elm: HTMLElement, + isInitialLoad: boolean, +) => { // in order for bundlers to correctly tree-shake the BUILD object // we need to ensure BUILD is not deoptimized within a try/catch - // https://rollupjs.org/guide/en/#treeshake tryCatchDeoptimization - const allRenderFn = BUILD.allRenderFn ? true : false; - const lazyLoad = BUILD.lazyLoad ? true : false; - const taskQueue = BUILD.taskQueue ? true : false; - const updatable = BUILD.updatable ? true : false; + // https://rolldownjs.org/guide/en/#treeshake tryCatchDeoptimization + const allRenderFn = !!BUILD.allRenderFn; + const lazyLoad = !!BUILD.lazyLoad; + const taskQueue = !!BUILD.taskQueue; + const updatable = !!BUILD.updatable; try { renderingRef = instance; @@ -308,7 +353,9 @@ const callRender = (hostRef: d.HostRef, instance: any, elm: HTMLElement, isIniti // or we need to update the css class/attrs on the host element // DOM WRITE! if (BUILD.hydrateServerSide) { - return Promise.resolve(instance).then((value) => renderVdom(hostRef, value, isInitialLoad)); + return Promise.resolve(instance).then((value) => + renderVdom(hostRef, value, isInitialLoad), + ); } else { renderVdom(hostRef, instance, isInitialLoad); } @@ -344,7 +391,9 @@ export const postUpdateComponent = (hostRef: d.HostRef) => { if (BUILD.isDev) { hostRef.$flags$ &= ~HOST_FLAGS.devOnRender; } - emitLifecycleEvent(elm, 'componentDidRender'); + if (BUILD.lifecycleDOMEvents) { + emitLifecycleEvent(elm, 'componentDidRender'); + } if (!(hostRef.$flags$ & HOST_FLAGS.hasLoadedComponent)) { hostRef.$flags$ |= HOST_FLAGS.hasLoadedComponent; @@ -362,7 +411,17 @@ export const postUpdateComponent = (hostRef: d.HostRef) => { hostRef.$flags$ &= ~HOST_FLAGS.devOnDidLoad; } - emitLifecycleEvent(elm, 'componentDidLoad'); + if (BUILD.lifecycleDOMEvents) { + emitLifecycleEvent(elm, 'componentDidLoad'); + } + + // Set isWatchReady after componentDidLoad so watchers don't fire on initial prop values. + // Per lifecycle docs, @Watch should only fire on subsequent prop changes, not initial load. + // Watchers with 'immediate' flag will still fire (checked separately in setValue). + if (BUILD.propChangeCallback) { + hostRef.$flags$ |= HOST_FLAGS.isWatchReady; + } + endPostUpdate(); if (BUILD.asyncLoading) { @@ -383,7 +442,9 @@ export const postUpdateComponent = (hostRef: d.HostRef) => { if (BUILD.isDev) { hostRef.$flags$ &= ~HOST_FLAGS.devOnRender; } - emitLifecycleEvent(elm, 'componentDidUpdate'); + if (BUILD.lifecycleDOMEvents) { + emitLifecycleEvent(elm, 'componentDidUpdate'); + } endPostUpdate(); } @@ -413,7 +474,8 @@ export const forceUpdate = (ref: any) => { const isConnected = hostRef?.$hostElement$?.isConnected; if ( isConnected && - (hostRef.$flags$ & (HOST_FLAGS.hasRendered | HOST_FLAGS.isQueuedForUpdate)) === HOST_FLAGS.hasRendered + (hostRef.$flags$ & (HOST_FLAGS.hasRendered | HOST_FLAGS.isQueuedForUpdate)) === + HOST_FLAGS.hasRendered ) { scheduleUpdate(hostRef, false); } diff --git a/packages/core/src/runtime/vdom/_test_/__snapshots__/vdom-annotations.spec.tsx.snap b/packages/core/src/runtime/vdom/_test_/__snapshots__/vdom-annotations.spec.tsx.snap new file mode 100644 index 00000000000..9d15d959059 --- /dev/null +++ b/packages/core/src/runtime/vdom/_test_/__snapshots__/vdom-annotations.spec.tsx.snap @@ -0,0 +1,7 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`vdom-annotations > should add annotations when component-a-test and component-b-test is given as static component 1`] = `"
o.0.1
slotContent
o.0.2
slotContent
"`; + +exports[`vdom-annotations > should add annotations when component-a-test is given as static component 1`] = `"
o.0.1
slotContent
o.0.2
slotContent
"`; + +exports[`vdom-annotations > should add annotations when no static component is given 1`] = `"
o.0.1
slotContent
o.0.2
slotContent
"`; diff --git a/src/runtime/vdom/test/attributes.spec.ts b/packages/core/src/runtime/vdom/_test_/attributes.spec.ts similarity index 95% rename from src/runtime/vdom/test/attributes.spec.ts rename to packages/core/src/runtime/vdom/_test_/attributes.spec.ts index 1aa6a15e432..b859b811d3b 100755 --- a/src/runtime/vdom/test/attributes.spec.ts +++ b/packages/core/src/runtime/vdom/_test_/attributes.spec.ts @@ -1,6 +1,7 @@ -import { SVG_NS, XLINK_NS } from '@utils'; +import { expect, describe, it, beforeEach } from '@stencil/vitest'; +import type * as d from '@stencil/core'; -import type * as d from '../../../declarations'; +import { SVG_NS, XLINK_NS } from '../../../utils'; import { h, newVNode } from '../h'; import { patch } from '../vdom-render'; diff --git a/src/runtime/vdom/test/event-listeners.spec.ts b/packages/core/src/runtime/vdom/_test_/event-listeners.spec.ts similarity index 95% rename from src/runtime/vdom/test/event-listeners.spec.ts rename to packages/core/src/runtime/vdom/_test_/event-listeners.spec.ts index 60763b6ffab..dcd102f3ea8 100644 --- a/src/runtime/vdom/test/event-listeners.spec.ts +++ b/packages/core/src/runtime/vdom/_test_/event-listeners.spec.ts @@ -1,4 +1,6 @@ -import type * as d from '../../../declarations'; +import { expect, describe, it, beforeEach } from '@stencil/vitest'; +import type * as d from '@stencil/core'; + import { h, newVNode } from '../h'; import { patch } from '../vdom-render'; diff --git a/src/runtime/vdom/test/h.spec.ts b/packages/core/src/runtime/vdom/_test_/h.spec.ts similarity index 95% rename from src/runtime/vdom/test/h.spec.ts rename to packages/core/src/runtime/vdom/_test_/h.spec.ts index 9df79236599..10478fd80cd 100644 --- a/src/runtime/vdom/test/h.spec.ts +++ b/packages/core/src/runtime/vdom/_test_/h.spec.ts @@ -1,4 +1,6 @@ -import type * as d from '../../../declarations'; +import { expect, describe, it } from '@stencil/vitest'; +import type * as d from '@stencil/core'; + import { h, newVNode } from '../h'; describe('h()', () => { @@ -172,18 +174,6 @@ describe('h()', () => { expect(vnode.$attrs$.class).toEqual('enabled'); }); - it('should add class from className string', () => { - const vnode = h('div', { className: 'one point twenty-one gigawatts' }); - expect(vnode.$attrs$.class).toBeDefined(); - expect(vnode.$attrs$.class).toEqual('one point twenty-one gigawatts'); - }); - - it('should add class from className map of classnames and booleans', () => { - const vnode = h('div', { className: { save: true, the: true, clock: true, tower: true, hillvalley: false } }); - expect(vnode.$attrs$.class).toBeDefined(); - expect(vnode.$attrs$.class).toEqual('save the clock tower'); - }); - it('should add props', () => { const vnode = h('div', { id: 'my-id', checked: false, count: 0 }); expect(vnode.$attrs$).toBeDefined(); @@ -412,7 +402,7 @@ describe('h()', () => { }); it('replaceAttributes should return the attributes for the node', () => { - const FunctionalCmp: d.FunctionalComponent = (_nodeData, children, util) => { + const FunctionalCmp = (_nodeData: any, children: d.VNode[], util: d.FunctionalUtilities) => { return util.map(children, (child) => { return { ...child, @@ -457,7 +447,7 @@ describe('h()', () => { const ReplacementCmp: d.FunctionalComponent = (nodeData, children) => { return h('article', nodeData, h('p', null, ...children)); }; - const FunctionalCmp: d.FunctionalComponent = (_nodeData, children, util) => { + const FunctionalCmp = (_nodeData, children, util) => { return util.map(children, (child) => { return { ...child, diff --git a/src/runtime/vdom/test/is-same-vnode.spec.ts b/packages/core/src/runtime/vdom/_test_/is-same-vnode.spec.ts similarity index 96% rename from src/runtime/vdom/test/is-same-vnode.spec.ts rename to packages/core/src/runtime/vdom/_test_/is-same-vnode.spec.ts index 4cb8a80dcdd..92a07f1a8b0 100644 --- a/src/runtime/vdom/test/is-same-vnode.spec.ts +++ b/packages/core/src/runtime/vdom/_test_/is-same-vnode.spec.ts @@ -1,4 +1,6 @@ -// import type * as d from '../declarations'; +import { expect, describe, it } from '@stencil/vitest'; + +// import type * as d from '@stencil/core'; import { h } from '../h'; import { isSameVnode } from '../vdom-render'; diff --git a/src/runtime/vdom/test/jsx-runtime.spec.ts b/packages/core/src/runtime/vdom/_test_/jsx-runtime.spec.ts similarity index 78% rename from src/runtime/vdom/test/jsx-runtime.spec.ts rename to packages/core/src/runtime/vdom/_test_/jsx-runtime.spec.ts index 243a44e2a9e..616eb1d9cd9 100644 --- a/src/runtime/vdom/test/jsx-runtime.spec.ts +++ b/packages/core/src/runtime/vdom/_test_/jsx-runtime.spec.ts @@ -1,5 +1,6 @@ +import { expect, describe, it, vi } from '@stencil/vitest'; + import { jsx, jsxs } from '../jsx-runtime'; -import { jsxDEV } from '../jsx-dev-runtime'; describe('jsx-runtime', () => { describe('jsx() and jsxs()', () => { @@ -37,14 +38,14 @@ describe('jsx-runtime', () => { }); it('should handle ref in props', () => { - const refCallback = jest.fn(); + const refCallback = vi.fn(); const vnode = jsx('div', { id: 'test', ref: refCallback }); expect(vnode.$tag$).toBe('div'); expect(vnode.$attrs$).toEqual({ id: 'test', ref: refCallback }); }); it('should handle both key and ref', () => { - const refCallback = jest.fn(); + const refCallback = vi.fn(); const vnode = jsx('div', { id: 'test', key: 'my-key', ref: refCallback }, undefined); expect(vnode.$tag$).toBe('div'); expect(vnode.$key$).toBe('my-key'); @@ -75,21 +76,4 @@ describe('jsx-runtime', () => { expect(vnode.$attrs$).toEqual({ id: 'test', key: 'my-key' }); }); }); - - describe('jsxDEV()', () => { - it('should handle key and ref like jsx()', () => { - const refCallback = jest.fn(); - const vnode = jsxDEV('div', { id: 'test', key: 'my-key', ref: refCallback }); - expect(vnode.$tag$).toBe('div'); - expect(vnode.$key$).toBe('my-key'); - expect(vnode.$attrs$).toEqual({ id: 'test', key: 'my-key', ref: refCallback }); - }); - - it('should handle key as separate parameter', () => { - const vnode = jsxDEV('div', { id: 'test' }, 'param-key'); - expect(vnode.$tag$).toBe('div'); - expect(vnode.$key$).toBe('param-key'); - expect(vnode.$attrs$).toEqual({ id: 'test', key: 'param-key' }); - }); - }); }); diff --git a/src/runtime/vdom/test/patch-svg.spec.ts b/packages/core/src/runtime/vdom/_test_/patch-svg.spec.ts similarity index 89% rename from src/runtime/vdom/test/patch-svg.spec.ts rename to packages/core/src/runtime/vdom/_test_/patch-svg.spec.ts index bd391724498..7d496ed0534 100644 --- a/src/runtime/vdom/test/patch-svg.spec.ts +++ b/packages/core/src/runtime/vdom/_test_/patch-svg.spec.ts @@ -1,6 +1,7 @@ -import { SVG_NS } from '@utils'; +import { expect, describe, it, beforeEach } from '@stencil/vitest'; +import type * as d from '@stencil/core'; -import type * as d from '../../../declarations'; +import { SVG_NS } from '../../../utils'; import { h, newVNode } from '../h'; import { toVNode } from '../util'; import { patch } from '../vdom-render'; @@ -52,7 +53,10 @@ describe('renderer', () => { it('should not affect subsequence element', () => { patch( vnode0, - h('div', null, [h('svg', null, [h('title', null, 'Title'), h('circle', null)] as any), h('div', null)] as any), + h('div', null, [ + h('svg', null, [h('title', null, 'Title'), h('circle', null)] as any), + h('div', null), + ] as any), ); expect(hostElm.tagName).toEqual('DIV'); diff --git a/src/runtime/vdom/test/patch.spec.ts b/packages/core/src/runtime/vdom/_test_/patch.spec.ts similarity index 89% rename from src/runtime/vdom/test/patch.spec.ts rename to packages/core/src/runtime/vdom/_test_/patch.spec.ts index 724823bb957..a921c42fcfb 100755 --- a/src/runtime/vdom/test/patch.spec.ts +++ b/packages/core/src/runtime/vdom/_test_/patch.spec.ts @@ -1,7 +1,8 @@ import { shuffleArray } from '@stencil/core/testing'; -import { SVG_NS } from '@utils'; +import { expect, describe, it, beforeEach } from '@stencil/vitest'; +import type * as d from '@stencil/core'; -import type * as d from '../../../declarations'; +import { SVG_NS } from '../../../utils'; import { h, newVNode } from '../h'; import { toVNode } from '../util'; import { patch } from '../vdom-render'; @@ -27,14 +28,24 @@ describe('renderer', () => { const vnode0 = newVNode(null, null); vnode0.$elm$ = hostElm; - const vnode1 = h('my-tag', null, h(DoesNotRenderChildren, null, '88'), h(RendersChildren, null, 'DMC')); + const vnode1 = h( + 'my-tag', + null, + h(DoesNotRenderChildren, null, '88'), + h(RendersChildren, null, 'DMC'), + ); patch(vnode0, vnode1); expect(hostElm.tagName).toBe('MY-TAG'); expect(hostElm.childNodes[0].innerHTML).toBe('mph'); expect(hostElm.childNodes[1].innerHTML).toBe('DMC-12'); - const vnode2 = h('my-tag', null, h(DoesNotRenderChildren, null, '88'), h(RendersChildren, null, 'dmc')); + const vnode2 = h( + 'my-tag', + null, + h(DoesNotRenderChildren, null, '88'), + h(RendersChildren, null, 'dmc'), + ); patch(vnode1, vnode2); expect(hostElm.childNodes[0].innerHTML).toBe('mph'); @@ -84,7 +95,15 @@ describe('renderer', () => { hostElm = document.createElement('my-tag'); vnode0 = newVNode(null, null); vnode0.$elm$ = hostElm; - patch(vnode0, h('my-tag', null, h('span', null, 'Test Child'), h(functionalComp, { class: 'functional-cmp' }))); + patch( + vnode0, + h( + 'my-tag', + null, + h('span', null, 'Test Child'), + h(functionalComp, { class: 'functional-cmp' }), + ), + ); expect(hostElm.childNodes[0].tagName).toBe('SPAN'); expect(hostElm.childNodes[0].textContent).toBe('Test Child'); expect(hostElm.childNodes[1].tagName).toBe('SPAN'); @@ -100,7 +119,14 @@ describe('renderer', () => { hostElm = document.createElement('my-tag'); vnode0 = newVNode(null, null); vnode0.$elm$ = hostElm; - patch(vnode0, h('my-tag', null, h(functionalComp, { class: 'functional-cmp' }, h('span', null, 'Test Child')))); + patch( + vnode0, + h( + 'my-tag', + null, + h(functionalComp, { class: 'functional-cmp' }, h('span', null, 'Test Child')), + ), + ); expect(hostElm.childNodes[0].tagName).toBe('SPAN'); expect(hostElm.childNodes[0].className).toBe('functional-cmp'); expect(hostElm.childNodes[0].textContent).toBe('Test Child'); @@ -282,7 +308,7 @@ describe('renderer', () => { } function vnodeMap(arr: number[]) { - return h.apply(null, ['span', null, ...arr.map(spanNum)]); + return h('span', null, ...arr.map(spanNum)); } describe('addition of elements', () => { @@ -355,7 +381,13 @@ describe('renderer', () => { it('update one child with same key but different sel', () => { const vnode1 = h('span', { key: 'spans' }, ...[1, 2, 3].map(spanNum)); - const vnode2 = h('span', { key: 'span' }, ...[spanNum(1), h('i', { key: 2 }, '2'), spanNum(3)]); + const vnode2 = h( + 'span', + { key: 'span' }, + spanNum(1), + h('i', { key: 2 }, '2'), + spanNum(3), + ); patch(vnode0, vnode1); expect(map(inner, hostElm.children)).toEqual(['1', '2', '3']); patch(vnode1, vnode2); @@ -632,7 +664,11 @@ describe('renderer', () => { it('supports null/undefined children', () => { const vnode1 = h('i', null, ...[0, 1, 2, 3, 4, 5].map(spanNum)); - const vnode2 = h('i', null, ...[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(spanNum)); + const vnode2 = h( + 'i', + null, + ...[null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map(spanNum), + ); patch(vnode0, vnode1); expect(hostElm.children.length).toEqual(6); @@ -643,7 +679,7 @@ describe('renderer', () => { it('supports all null/undefined children', () => { const vnode1 = h('v1', null, ...[0, 1, 2, 3, 4, 5].map(spanNum)); - const vnode2 = h('v2', null, ...[null, null, undefined, null, null, undefined]); + const vnode2 = h('v2', null, null, null, undefined, null, null, undefined); const vnode3 = h('v3', null, ...[5, 4, 3, 2, 1, 0].map(spanNum)); patch(vnode0, vnode1); @@ -698,8 +734,8 @@ describe('renderer', () => { }); it('handles unmoved text nodes', () => { - const vnode1 = h('div', null, ...['Text', h('span', null, 'Span')]); - const vnode2 = h('div', null, ...['Text', h('span', null, 'Span')]); + const vnode1 = h('div', null, 'Text', h('span', null, 'Span')); + const vnode2 = h('div', null, 'Text', h('span', null, 'Span')); patch(vnode0, vnode1); expect(hostElm.childNodes[0].textContent).toEqual('Text'); @@ -709,8 +745,8 @@ describe('renderer', () => { }); it('handles changing text children', () => { - const vnode1 = h('div', null, ...['Text', h('span', null, 'Span')]); - const vnode2 = h('div', null, ...['Text2', h('span', null, 'Span')]); + const vnode1 = h('div', null, 'Text', h('span', null, 'Span')); + const vnode2 = h('div', null, 'Text2', h('span', null, 'Span')); patch(vnode0, vnode1); expect(hostElm.childNodes[0].textContent).toEqual('Text'); @@ -720,8 +756,8 @@ describe('renderer', () => { }); it('prepends element', () => { - const vnode1 = h('div', null, ...[h('span', null, 'World')]); - const vnode2 = h('div', null, ...[h('span', null, 'Hello'), h('span', null, 'World')]); + const vnode1 = h('div', null, h('span', null, 'World')); + const vnode2 = h('div', null, h('span', null, 'Hello'), h('span', null, 'World')); patch(vnode0, vnode1); expect(map(inner, hostElm.children)).toEqual(['World']); @@ -731,8 +767,8 @@ describe('renderer', () => { }); it('prepends element of different tag type', () => { - const vnode1 = h('div', null, ...[h('span', null, 'World')]); - const vnode2 = h('div', null, ...[h('div', null, 'Hello'), h('span', null, 'World')]); + const vnode1 = h('div', null, h('span', null, 'World')); + const vnode2 = h('div', null, h('div', null, 'Hello'), h('span', null, 'World')); patch(vnode0, vnode1); expect(map(inner, hostElm.children)).toEqual(['World']); @@ -743,8 +779,14 @@ describe('renderer', () => { }); it('removes elements', () => { - const vnode1 = h('div', null, ...[h('span', null, 'One'), h('span', null, 'Two'), h('span', null, 'Three')]); - const vnode2 = h('div', null, ...[h('span', null, 'One'), h('span', null, 'Three')]); + const vnode1 = h( + 'div', + null, + h('span', null, 'One'), + h('span', null, 'Two'), + h('span', null, 'Three'), + ); + const vnode2 = h('div', null, h('span', null, 'One'), h('span', null, 'Three')); patch(vnode0, vnode1); expect(map(inner, hostElm.children)).toEqual(['One', 'Two', 'Three']); @@ -766,7 +808,7 @@ describe('renderer', () => { it('removes a single text node when children are updated', () => { const vnode1 = h('div', null, 'One'); - const vnode2 = h('div', null, ...[h('div', null, 'Two'), h('span', null, 'Three')]); + const vnode2 = h('div', null, h('div', null, 'Two'), h('span', null, 'Three')); patch(vnode0, vnode1); expect(hostElm.textContent).toEqual('One'); @@ -792,8 +834,8 @@ describe('renderer', () => { }); it('removes a text node among other elements', () => { - const vnode1 = h('div', null, ...['One', h('span', null, 'Two')]); - const vnode2 = h('div', null, ...[h('div', null, 'Three')]); + const vnode1 = h('div', null, 'One', h('span', null, 'Two')); + const vnode2 = h('div', null, h('div', null, 'Three')); patch(vnode0, vnode1); expect(map(prop('textContent'), hostElm.childNodes)).toEqual(['One', 'Two']); @@ -806,8 +848,20 @@ describe('renderer', () => { }); it('reorders elements', () => { - const vnode1 = h('div', null, ...[h('span', null, 'One'), h('div', null, 'Two'), h('b', null, 'Three')]); - const vnode2 = h('div', null, ...[h('b', null, 'Three'), h('span', null, 'One'), h('div', null, 'Two')]); + const vnode1 = h( + 'div', + null, + h('span', null, 'One'), + h('div', null, 'Two'), + h('b', null, 'Three'), + ); + const vnode2 = h( + 'div', + null, + h('b', null, 'Three'), + h('span', null, 'One'), + h('div', null, 'Two'), + ); patch(vnode0, vnode1); expect(map(inner, hostElm.children)).toEqual(['One', 'Two', 'Three']); @@ -818,9 +872,27 @@ describe('renderer', () => { }); it('supports null/undefined children', () => { - const vnode1 = h('i', null, ...[null, h('i', null, '1'), h('i', null, '2'), null]); - const vnode2 = h('i', null, ...[h('i', null, '2'), undefined, undefined, h('i', null, '1'), undefined]); - const vnode3 = h('i', null, ...[null, h('i', null, '1'), undefined, null, h('i', null, '2'), undefined, null]); + const vnode1 = h('i', null, null, h('i', null, '1'), h('i', null, '2'), null); + const vnode2 = h( + 'i', + null, + h('i', null, '2'), + undefined, + undefined, + h('i', null, '1'), + undefined, + ); + const vnode3 = h( + 'i', + null, + null, + h('i', null, '1'), + undefined, + null, + h('i', null, '2'), + undefined, + null, + ); patch(vnode0, vnode1); expect(map(inner, hostElm.children)).toEqual(['1', '2']); @@ -833,9 +905,9 @@ describe('renderer', () => { }); it('supports all null/undefined children', () => { - const vnode1 = h('i', null, ...[h('i', null, '1'), h('i', null, '2')]); - const vnode2 = h('i', null, ...[null, null, undefined]); - const vnode3 = h('i', null, ...[h('i', null, '2'), h('i', null, '1')]); + const vnode1 = h('i', null, h('i', null, '1'), h('i', null, '2')); + const vnode2 = h('i', null, null, null, undefined); + const vnode3 = h('i', null, h('i', null, '2'), h('i', null, '1')); patch(vnode0, vnode1); diff --git a/src/runtime/vdom/test/scoped-slot.spec.tsx b/packages/core/src/runtime/vdom/_test_/scoped-slot.spec.tsx similarity index 80% rename from src/runtime/vdom/test/scoped-slot.spec.tsx rename to packages/core/src/runtime/vdom/_test_/scoped-slot.spec.tsx index a0eb453b78f..084ef71f784 100644 --- a/src/runtime/vdom/test/scoped-slot.spec.tsx +++ b/packages/core/src/runtime/vdom/_test_/scoped-slot.spec.tsx @@ -1,9 +1,10 @@ import { Component, forceUpdate, h, Prop } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it } from '@stencil/vitest'; describe('scoped slot', () => { it('should relocate nested default slot nodes', async () => { - @Component({ tag: 'ion-test', scoped: true }) + @Component({ tag: 'ion-test', encapsulation: { type: 'scoped' } }) class CmpA { render() { return ( @@ -25,7 +26,7 @@ describe('scoped slot', () => { }); it('should use components default slot text content', async () => { - @Component({ tag: 'ion-test', scoped: true }) + @Component({ tag: 'ion-test', encapsulation: { type: 'scoped' } }) class CmpA { render() { return ( @@ -49,7 +50,7 @@ describe('scoped slot', () => { }); it('should use components default slot node content', async () => { - @Component({ tag: 'ion-test', scoped: true }) + @Component({ tag: 'ion-test', encapsulation: { type: 'scoped' } }) class CmpA { render() { return ( @@ -69,16 +70,18 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('SPIDER'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('SLOT-FB'); - expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe('default content'); + expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe( + 'default content', + ); }); it('should relocate nested named slot nodes', async () => { - @Component({ tag: 'ion-test', scoped: true }) + @Component({ tag: 'ion-test', encapsulation: { type: 'scoped' } }) class CmpA { render() { return ( - + ); } @@ -96,7 +99,7 @@ describe('scoped slot', () => { }); it('no content', async () => { - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { render() { return ( @@ -107,7 +110,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { render() { return ; @@ -126,7 +129,7 @@ describe('scoped slot', () => { }); it('no content, nested child slot', async () => { - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { render() { return ( @@ -137,7 +140,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { render() { return ( @@ -163,7 +166,7 @@ describe('scoped slot', () => { }); it('should put parent content in child default slot', async () => { - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { render() { return ( @@ -176,7 +179,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { render() { return ; @@ -191,20 +194,22 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('HIPPO'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('AARDVARK'); - expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe('parent message'); + expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe( + 'parent message', + ); }); it('should relocate parent content after child content dynamically changes slot wrapper tag', async () => { - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { - @Prop() innerH = (

parent text

); + @Prop() innerH =

parent text

; render() { return {this.innerH}; } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { @Prop() Tag = 'section'; @@ -235,7 +240,7 @@ describe('scoped slot', () => { expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('H6'); expect(root.firstElementChild.textContent).toBe('parent text update'); - const child = root.querySelector('ion-child'); + const child = root.querySelector('ion-child'); child.Tag = 'article'; await waitForChanges(); @@ -246,7 +251,7 @@ describe('scoped slot', () => { }); it('should put parent content in child nested default slot', async () => { - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { render() { return ( @@ -259,7 +264,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { render() { return ( @@ -280,12 +285,16 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('BADGER'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('CAMEL'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('OWL'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('OWL'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .nodeName, ).toBe('DINGO'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .textContent, ).toBe('parent message'); forceUpdate(root); @@ -294,17 +303,21 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('BADGER'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('CAMEL'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('OWL'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('OWL'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .nodeName, ).toBe('DINGO'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .textContent, ).toBe('parent message'); }); it('should render conditional content into a nested default slot', async () => { - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { render() { return ( @@ -315,7 +328,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { test = 0; @@ -363,7 +376,7 @@ describe('scoped slot', () => { }); it('should update parent content in child default slot', async () => { - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { @Prop() msg = 'parent message'; render() { @@ -377,7 +390,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { render() { return ( @@ -396,10 +409,12 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('CHEETAH'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('CHIPMUNK'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('BEAR'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent).toBe( - 'parent message', - ); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('BEAR'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + ).toBe('parent message'); root.msg = 'change 1'; await waitForChanges(); @@ -407,8 +422,12 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('CHEETAH'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('CHIPMUNK'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('BEAR'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent).toBe('change 1'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('BEAR'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + ).toBe('change 1'); root.msg = 'change 2'; await waitForChanges(); @@ -416,12 +435,16 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('CHEETAH'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('CHIPMUNK'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('BEAR'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent).toBe('change 2'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('BEAR'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + ).toBe('change 2'); }); it('should update parent content inner text in child nested default slot', async () => { - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { @Prop() msg = 'parent message'; render() { @@ -433,7 +456,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { render() { return ( @@ -452,7 +475,9 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('ION-CHILD'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('BULL'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('WHALE'); - expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe('parent message'); + expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe( + 'parent message', + ); root.msg = 'change 1'; await waitForChanges(); @@ -474,26 +499,26 @@ describe('scoped slot', () => { it('should allow multiple slots with same name', async () => { let values = 0; - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { render() { return ( - {++values} - {++values} + {++values} + {++values} ); } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { render() { return ( - - + + ); } @@ -535,29 +560,29 @@ describe('scoped slot', () => { it('should only render nested named slots and default slot', async () => { let values = 0; - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { render() { return ( {(++values).toString()} - {++values} - {++values} + {++values} + {++values} ); } } - @Component({ tag: 'ion-child', scoped: true }) + @Component({ tag: 'ion-child', encapsulation: { type: 'scoped' } }) class Child { render() { return ( - + - + @@ -575,11 +600,19 @@ describe('scoped slot', () => { expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('FERRET'); expect(root.firstElementChild.firstElementChild.children[0].textContent).toBe('3'); expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('HORSE'); - expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe('BUTTERFLY'); + expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe( + 'BUTTERFLY', + ); expect(root.firstElementChild.firstElementChild.children[1].children[0].textContent).toBe('1'); - expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe('BULLFROG'); - expect(root.firstElementChild.firstElementChild.children[1].children[1].children[0].nodeName).toBe('FOX'); - expect(root.firstElementChild.firstElementChild.children[1].children[1].children[0].textContent).toBe('2'); + expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe( + 'BULLFROG', + ); + expect( + root.firstElementChild.firstElementChild.children[1].children[1].children[0].nodeName, + ).toBe('FOX'); + expect( + root.firstElementChild.firstElementChild.children[1].children[1].children[0].textContent, + ).toBe('2'); forceUpdate(root); await waitForChanges(); @@ -589,11 +622,19 @@ describe('scoped slot', () => { expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('FERRET'); expect(root.firstElementChild.firstElementChild.children[0].textContent).toBe('6'); expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('HORSE'); - expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe('BUTTERFLY'); + expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe( + 'BUTTERFLY', + ); expect(root.firstElementChild.firstElementChild.children[1].children[0].textContent).toBe('4'); - expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe('BULLFROG'); - expect(root.firstElementChild.firstElementChild.children[1].children[1].children[0].nodeName).toBe('FOX'); - expect(root.firstElementChild.firstElementChild.children[1].children[1].children[0].textContent).toBe('5'); + expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe( + 'BULLFROG', + ); + expect( + root.firstElementChild.firstElementChild.children[1].children[1].children[0].nodeName, + ).toBe('FOX'); + expect( + root.firstElementChild.firstElementChild.children[1].children[1].children[0].textContent, + ).toBe('5'); forceUpdate(root); await waitForChanges(); @@ -603,17 +644,25 @@ describe('scoped slot', () => { expect(root.firstElementChild.firstElementChild.children[0].nodeName).toBe('FERRET'); expect(root.firstElementChild.firstElementChild.children[0].textContent).toBe('9'); expect(root.firstElementChild.firstElementChild.children[1].nodeName).toBe('HORSE'); - expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe('BUTTERFLY'); + expect(root.firstElementChild.firstElementChild.children[1].children[0].nodeName).toBe( + 'BUTTERFLY', + ); expect(root.firstElementChild.firstElementChild.children[1].children[0].textContent).toBe('7'); - expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe('BULLFROG'); - expect(root.firstElementChild.firstElementChild.children[1].children[1].children[0].nodeName).toBe('FOX'); - expect(root.firstElementChild.firstElementChild.children[1].children[1].children[0].textContent).toBe('8'); + expect(root.firstElementChild.firstElementChild.children[1].children[1].nodeName).toBe( + 'BULLFROG', + ); + expect( + root.firstElementChild.firstElementChild.children[1].children[1].children[0].nodeName, + ).toBe('FOX'); + expect( + root.firstElementChild.firstElementChild.children[1].children[1].children[0].textContent, + ).toBe('8'); }); it('should allow nested default slots', async () => { let values = 0; - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { render() { return ( @@ -626,7 +675,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'test-1', scoped: true }) + @Component({ tag: 'test-1', encapsulation: { type: 'scoped' } }) class Test1 { render() { return ( @@ -637,7 +686,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'test-2', scoped: true }) + @Component({ tag: 'test-2', encapsulation: { type: 'scoped' } }) class Test2 { render() { return ( @@ -656,12 +705,16 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('TEST-1'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('SEAL'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('TEST-2'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('GOOSE'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('GOOSE'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .nodeName, ).toBe('GOAT'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .textContent, ).toBe('1'); forceUpdate(root); @@ -670,12 +723,16 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('TEST-1'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('SEAL'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('TEST-2'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('GOOSE'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('GOOSE'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .nodeName, ).toBe('GOAT'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .textContent, ).toBe('2'); forceUpdate(root); @@ -684,17 +741,21 @@ describe('scoped slot', () => { expect(root.firstElementChild.nodeName).toBe('TEST-1'); expect(root.firstElementChild.firstElementChild.nodeName).toBe('SEAL'); expect(root.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('TEST-2'); - expect(root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName).toBe('GOOSE'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nodeName, + ).toBe('GOOSE'); + expect( + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .nodeName, ).toBe('GOAT'); expect( - root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.textContent, + root.firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild + .textContent, ).toBe('3'); }); it('should allow nested default slots w/ default slot content', async () => { - @Component({ tag: 'ion-parent', scoped: true }) + @Component({ tag: 'ion-parent', encapsulation: { type: 'scoped' } }) class Parent { render() { return ( @@ -707,7 +768,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'test-1', scoped: true }) + @Component({ tag: 'test-1', encapsulation: { type: 'scoped' } }) class Test1 { render() { return ( @@ -720,7 +781,7 @@ describe('scoped slot', () => { } } - @Component({ tag: 'test-2', scoped: true }) + @Component({ tag: 'test-2', encapsulation: { type: 'scoped' } }) class Test2 { render() { return ( @@ -779,7 +840,7 @@ describe('scoped slot', () => { }); it("should hide the slot's fallback content for a scoped component when slot content passed in", async () => { - @Component({ tag: 'fallback-test', scoped: true }) + @Component({ tag: 'fallback-test', encapsulation: { type: 'scoped' } }) class ScopedFallbackSlotTest { render() { return ( @@ -801,7 +862,7 @@ describe('scoped slot', () => { }); it("should hide the slot's fallback content for a non-shadow component when slot content passed in", async () => { - @Component({ tag: 'fallback-test', shadow: false }) + @Component({ tag: 'fallback-test', encapsulation: { type: 'none' } }) class NonShadowFallbackSlotTest { render() { return ( diff --git a/src/runtime/vdom/test/set-accessor.spec.ts b/packages/core/src/runtime/vdom/_test_/set-accessor.spec.ts similarity index 93% rename from src/runtime/vdom/test/set-accessor.spec.ts rename to packages/core/src/runtime/vdom/_test_/set-accessor.spec.ts index 87c1b9e0549..42960e40aca 100644 --- a/src/runtime/vdom/test/set-accessor.spec.ts +++ b/packages/core/src/runtime/vdom/_test_/set-accessor.spec.ts @@ -1,4 +1,6 @@ -import { BUILD } from '@app-data'; +import { expect, describe, it, beforeEach, afterEach, vi } from '@stencil/vitest'; +import { BUILD } from 'virtual:app-data'; +import { plt } from 'virtual:platform'; import { parseClassList, setAccessor } from '../set-accessor'; @@ -10,8 +12,12 @@ describe('setAccessor for custom elements', () => { }); describe('event listener', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should allow public method starting with "on" and capital 3rd character', () => { - const addEventSpy = jest.spyOn(elm, 'addEventListener'); + const addEventSpy = vi.spyOn(plt, 'ael'); elm.onMyMethod = () => { /**/ @@ -26,8 +32,8 @@ describe('setAccessor for custom elements', () => { }); it('should remove standardized event listener when has old value, but no new', () => { - const addEventSpy = jest.spyOn(elm, 'addEventListener'); - const removeEventSpy = jest.spyOn(elm, 'removeEventListener'); + const addEventSpy = vi.spyOn(plt, 'ael'); + const removeEventSpy = vi.spyOn(plt, 'rel'); const orgValue = () => { /**/ @@ -37,13 +43,13 @@ describe('setAccessor for custom elements', () => { setAccessor(elm, 'onClick', orgValue, undefined, false, 0); expect(addEventSpy).toHaveBeenCalledTimes(1); - expect(addEventSpy).toHaveBeenCalledWith('click', orgValue, false); - expect(removeEventSpy).toHaveBeenCalledWith('click', orgValue, false); + expect(addEventSpy).toHaveBeenCalledWith(elm, 'click', orgValue, false); + expect(removeEventSpy).toHaveBeenCalledWith(elm, 'click', orgValue, false); }); it('should remove standardized multiple-word then add event listener w/ different value', () => { - const addEventSpy = jest.spyOn(elm, 'addEventListener'); - const removeEventSpy = jest.spyOn(elm, 'removeEventListener'); + const addEventSpy = vi.spyOn(plt, 'ael'); + const removeEventSpy = vi.spyOn(plt, 'rel'); const orgValue = () => { /**/ @@ -52,13 +58,13 @@ describe('setAccessor for custom elements', () => { setAccessor(elm, 'onMouseOver', orgValue, undefined, false, 0); - expect(addEventSpy).toHaveBeenCalledWith('mouseover', orgValue, false); - expect(removeEventSpy).toHaveBeenCalledWith('mouseover', orgValue, false); + expect(addEventSpy).toHaveBeenCalledWith(elm, 'mouseover', orgValue, false); + expect(removeEventSpy).toHaveBeenCalledWith(elm, 'mouseover', orgValue, false); }); it('should remove standardized then add event listener w/ different value', () => { - const addEventSpy = jest.spyOn(elm, 'addEventListener'); - const removeEventSpy = jest.spyOn(elm, 'removeEventListener'); + const addEventSpy = vi.spyOn(plt, 'ael'); + const removeEventSpy = vi.spyOn(plt, 'rel'); const orgValue = () => { /**/ @@ -75,8 +81,8 @@ describe('setAccessor for custom elements', () => { }); it('should add custom event listener when no old value', () => { - const addEventSpy = jest.spyOn(elm, 'addEventListener'); - const removeEventSpy = jest.spyOn(elm, 'removeEventListener'); + const addEventSpy = vi.spyOn(plt, 'ael'); + const removeEventSpy = vi.spyOn(plt, 'rel'); const newValue = () => { /**/ @@ -84,13 +90,13 @@ describe('setAccessor for custom elements', () => { setAccessor(elm, 'onIonChange', undefined, newValue, false, 0); - expect(addEventSpy).toHaveBeenCalledWith('ionChange', newValue, false); + expect(addEventSpy).toHaveBeenCalledWith(elm, 'ionChange', newValue, false); expect(removeEventSpy).not.toHaveBeenCalled(); }); it('should add standardized multiple-word event listener when no old value', () => { - const addEventSpy = jest.spyOn(elm, 'addEventListener'); - const removeEventSpy = jest.spyOn(elm, 'removeEventListener'); + const addEventSpy = vi.spyOn(plt, 'ael'); + const removeEventSpy = vi.spyOn(plt, 'rel'); const newValue = () => { /**/ @@ -98,13 +104,13 @@ describe('setAccessor for custom elements', () => { setAccessor(elm, 'onMouseOver', undefined, newValue, false, 0); - expect(addEventSpy).toHaveBeenCalledWith('mouseover', newValue, false); + expect(addEventSpy).toHaveBeenCalledWith(elm, 'mouseover', newValue, false); expect(removeEventSpy).not.toHaveBeenCalled(); }); it('should add standardized event listener when no old value', () => { - const addEventSpy = jest.spyOn(elm, 'addEventListener'); - const removeEventSpy = jest.spyOn(elm, 'removeEventListener'); + const addEventSpy = vi.spyOn(plt, 'ael'); + const removeEventSpy = vi.spyOn(plt, 'rel'); const newValue = () => { /**/ @@ -112,13 +118,13 @@ describe('setAccessor for custom elements', () => { setAccessor(elm, 'onClick', undefined, newValue, false, 0); - expect(addEventSpy).toHaveBeenCalledWith('click', newValue, false); + expect(addEventSpy).toHaveBeenCalledWith(elm, 'click', newValue, false); expect(removeEventSpy).not.toHaveBeenCalled(); }); it('should add a capture style event listener', () => { - const addEventSpy = jest.spyOn(elm, 'addEventListener'); - const removeEventSpy = jest.spyOn(elm, 'removeEventListener'); + const addEventSpy = vi.spyOn(plt, 'ael'); + const removeEventSpy = vi.spyOn(plt, 'rel'); const newValue = () => { /**/ @@ -126,13 +132,13 @@ describe('setAccessor for custom elements', () => { setAccessor(elm, 'onClickCapture', undefined, newValue, false, 0); - expect(addEventSpy).toHaveBeenCalledWith('click', newValue, true); + expect(addEventSpy).toHaveBeenCalledWith(elm, 'click', newValue, true); expect(removeEventSpy).not.toHaveBeenCalled(); }); it('should remove a capture style event listener', () => { - const addEventSpy = jest.spyOn(elm, 'addEventListener'); - const removeEventSpy = jest.spyOn(elm, 'removeEventListener'); + const addEventSpy = vi.spyOn(plt, 'ael'); + const removeEventSpy = vi.spyOn(plt, 'rel'); const orgValue = () => { /**/ @@ -142,8 +148,8 @@ describe('setAccessor for custom elements', () => { setAccessor(elm, 'onClickCapture', orgValue, undefined, false, 0); expect(addEventSpy).toHaveBeenCalledTimes(1); - expect(addEventSpy).toHaveBeenCalledWith('click', orgValue, true); - expect(removeEventSpy).toHaveBeenCalledWith('click', orgValue, true); + expect(addEventSpy).toHaveBeenCalledWith(elm, 'click', orgValue, true); + expect(removeEventSpy).toHaveBeenCalledWith(elm, 'click', orgValue, true); }); }); @@ -845,7 +851,15 @@ describe('setAccessor for standard html elements', () => { expect(elm.className).toEqual('something-new a-scope-id'); elm.className = ''; - setAccessor(elm, 'class', 'something-old a-scope-id-something', 'something-new', false, 0, true); + setAccessor( + elm, + 'class', + 'something-old a-scope-id-something', + 'something-new', + false, + 0, + true, + ); expect(elm.className).toEqual('something-new a-scope-id a-scope-id-something'); // just check it reverts to normal behavior after initial render @@ -941,9 +955,18 @@ describe('setAccessor for standard html elements', () => { elm.style.setProperty('margin', '20px'); elm.style.setProperty('font-size', '88px'); - expect(elm.style.cssText).toEqual('color: black; padding: 20px; margin: 20px; font-size: 88px;'); + expect(elm.style.cssText).toEqual( + 'color: black; padding: 20px; margin: 20px; font-size: 88px;', + ); - setAccessor(elm, 'style', { color: 'black', padding: '20px', fontSize: '88px' }, undefined, false, 0); + setAccessor( + elm, + 'style', + { color: 'black', padding: '20px', fontSize: '88px' }, + undefined, + false, + 0, + ); expect(elm.style.cssText).toEqual('margin: 20px;'); setAccessor(elm, 'style', { margin: '20px' }, { margin: '30px', color: 'orange' }, false, 0); @@ -953,12 +976,12 @@ describe('setAccessor for standard html elements', () => { it('uses setAttribute if element has not setter', () => { const elm = document.createElement('button'); - const spy = jest.spyOn(elm, 'setAttribute'); + const spy = vi.spyOn(elm, 'setAttribute'); setAccessor(elm, 'form', undefined, 'some-form', false, 0); expect(spy.mock.calls).toEqual([['form', 'some-form']]); const elm2 = document.createElement('button'); - const spy2 = jest.spyOn(elm2, 'setAttribute'); + const spy2 = vi.spyOn(elm2, 'setAttribute'); setAccessor(elm2, 'textContent', undefined, 'some-content', false, 0); expect(spy2.mock.calls).toEqual([]); }); @@ -1019,7 +1042,7 @@ describe('setAccessor for standard html elements', () => { it('should force attribute even for properties that exist on element', () => { const nativeElm = document.createElement('input'); - const spy = jest.spyOn(nativeElm, 'setAttribute'); + const spy = vi.spyOn(nativeElm, 'setAttribute'); setAccessor(nativeElm, 'attr:id', undefined, 'my-id', false, 0); expect(spy).toHaveBeenCalledWith('id', 'my-id'); }); @@ -1037,7 +1060,7 @@ describe('setAccessor for standard html elements', () => { }); it('should not set attribute when old and new values are equal', () => { - const spy = jest.spyOn(elm, 'setAttribute'); + const spy = vi.spyOn(elm, 'setAttribute'); setAccessor(elm, 'attr:data-value', 'same', 'same', false, 0); expect(spy).not.toHaveBeenCalled(); }); @@ -1080,7 +1103,7 @@ describe('setAccessor for standard html elements', () => { }); it('should not set attribute when using prop: prefix', () => { - const spy = jest.spyOn(elm, 'setAttribute'); + const spy = vi.spyOn(elm, 'setAttribute'); setAccessor(elm, 'prop:value', undefined, 'test', false, 0); expect(spy).not.toHaveBeenCalled(); }); diff --git a/src/runtime/vdom/test/update-element.spec.ts b/packages/core/src/runtime/vdom/_test_/update-element.spec.ts similarity index 95% rename from src/runtime/vdom/test/update-element.spec.ts rename to packages/core/src/runtime/vdom/_test_/update-element.spec.ts index 8666eb597c0..c3dbe9aa9ac 100644 --- a/src/runtime/vdom/test/update-element.spec.ts +++ b/packages/core/src/runtime/vdom/_test_/update-element.spec.ts @@ -1,4 +1,6 @@ -import type * as d from '../../../declarations'; +import { expect, describe, it, vi } from '@stencil/vitest'; +import type * as d from '@stencil/core'; + import { NODE_TYPE } from '../../runtime-constants'; import { newVNode } from '../h'; import * as setAccessor from '../set-accessor'; @@ -146,7 +148,7 @@ describe('updateElement', () => { }); it('max test', () => { - const spy = jest.spyOn(setAccessor, 'setAccessor'); + const spy = vi.spyOn(setAccessor, 'setAccessor'); const elm = document.createElement('section') as HTMLElement; const initialVNode: null = null; const firstVNode = createTestNode({ @@ -173,7 +175,16 @@ describe('updateElement', () => { }); updateElement(initialVNode, firstVNode, false); expect(spy).toHaveBeenCalledTimes(4); - expect(spy).toHaveBeenNthCalledWith(1, elm, 'content', undefined, 'attributes removed', false, 0, undefined); + expect(spy).toHaveBeenNthCalledWith( + 1, + elm, + 'content', + undefined, + 'attributes removed', + false, + 0, + undefined, + ); expect(spy).toHaveBeenNthCalledWith(2, elm, 'padding', undefined, false, false, 0, undefined); expect(spy).toHaveBeenNthCalledWith(3, elm, 'bold', undefined, 'false', false, 0, undefined); expect(spy).toHaveBeenNthCalledWith(4, elm, 'no-attr', undefined, null, false, 0, undefined); diff --git a/packages/core/src/runtime/vdom/_test_/util.spec.ts b/packages/core/src/runtime/vdom/_test_/util.spec.ts new file mode 100644 index 00000000000..ab9d9d02bbe --- /dev/null +++ b/packages/core/src/runtime/vdom/_test_/util.spec.ts @@ -0,0 +1,49 @@ +import { expect, describe, it } from '@stencil/vitest'; + +import { toVNode } from '../util'; + +describe('toVNode()', () => { + it('should create element w/ child elements and text nodes', () => { + const elm = document.createElement('h1'); + elm.innerHTML = '
1 2
'; + + const vnode = toVNode(elm); + + expect(vnode.$elm$).toBe(elm); + expect(vnode.$tag$).toBe('h1'); + + expect(vnode.$children$).toBeDefined(); + expect(vnode.$children$.length).toBe(1); + + expect(vnode.$children$[0].$tag$).toBe('div'); + + expect(vnode.$children$[0].$children$).toBeDefined(); + expect(vnode.$children$[0].$children$.length).toBe(3); + + expect(vnode.$children$[0].$children$[0].$text$).toBe(' 1 '); + + expect(vnode.$children$[0].$children$[1].$tag$).toBe('span'); + expect(vnode.$children$[0].$children$[2].$text$).toBe(' '); + + expect(vnode.$children$[0].$children$[1].$children$[0].$text$).toBe(' 2 '); + }); + + it('should create element w/ child text node', () => { + const elm = document.createElement('h1'); + elm.textContent = '88mph'; + const vnode = toVNode(elm); + expect(vnode.$elm$).toBe(elm); + expect(vnode.$tag$).toBe('h1'); + expect(vnode.$children$).toBeDefined(); + expect(vnode.$children$.length).toBe(1); + expect(vnode.$children$[0].$text$).toBe('88mph'); + }); + + it('should create element', () => { + const elm = document.createElement('h1'); + const vnode = toVNode(elm); + expect(vnode.$elm$).toBe(elm); + expect(vnode.$tag$).toBe('h1'); + expect(vnode.$children$).toBeNull(); + }); +}); diff --git a/src/runtime/vdom/test/vdom-annotations.spec.tsx b/packages/core/src/runtime/vdom/_test_/vdom-annotations.spec.tsx similarity index 92% rename from src/runtime/vdom/test/vdom-annotations.spec.tsx rename to packages/core/src/runtime/vdom/_test_/vdom-annotations.spec.tsx index 482b8116ea1..9eda2300c78 100644 --- a/src/runtime/vdom/test/vdom-annotations.spec.tsx +++ b/packages/core/src/runtime/vdom/_test_/vdom-annotations.spec.tsx @@ -1,5 +1,6 @@ import { Component, h } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { expect, describe, it, beforeEach } from '@stencil/vitest'; import { insertVdomAnnotations } from '../vdom-annotations'; @@ -7,7 +8,7 @@ describe('vdom-annotations', () => { let root: HTMLElement; beforeEach(async () => { - @Component({ tag: 'component-a-test', scoped: true }) + @Component({ tag: 'component-a-test', encapsulation: { type: 'scoped' } }) class ComponentA { render() { return ( @@ -18,7 +19,7 @@ describe('vdom-annotations', () => { } } - @Component({ tag: 'component-b-test', scoped: true }) + @Component({ tag: 'component-b-test', encapsulation: { type: 'scoped' } }) class ComponentB { render() { return ( diff --git a/src/runtime/vdom/test/vdom-render.spec.tsx b/packages/core/src/runtime/vdom/_test_/vdom-render.spec.tsx similarity index 91% rename from src/runtime/vdom/test/vdom-render.spec.tsx rename to packages/core/src/runtime/vdom/_test_/vdom-render.spec.tsx index ce64749e475..130d3813835 100644 --- a/src/runtime/vdom/test/vdom-render.spec.tsx +++ b/packages/core/src/runtime/vdom/_test_/vdom-render.spec.tsx @@ -1,3 +1,5 @@ +import { expect, describe, it } from '@stencil/vitest'; + import { h, newVNode } from '../h'; import { isSameVnode, patch } from '../vdom-render'; @@ -8,7 +10,11 @@ describe('template elements', () => { vnode0.$elm$ = hostElm; // Create a template with children - const vnode1 = h('div', null, h('template', null, h('span', null, 'Hello'), h('p', null, 'World'))); + const vnode1 = h( + 'div', + null, + h('template', null, h('span', null, 'Hello'), h('p', null, 'World')), + ); patch(vnode0, vnode1); @@ -31,7 +37,11 @@ describe('template elements', () => { const vnode0 = newVNode(null, null); vnode0.$elm$ = hostElm; - const vnode1 = h('div', null, h('template', null, h('div', { class: 'test' }, 'Content to clone'))); + const vnode1 = h( + 'div', + null, + h('template', null, h('div', { class: 'test' }, 'Content to clone')), + ); patch(vnode0, vnode1); diff --git a/src/runtime/vdom/h.ts b/packages/core/src/runtime/vdom/h.ts similarity index 94% rename from src/runtime/vdom/h.ts rename to packages/core/src/runtime/vdom/h.ts index 63f454329d5..15729133051 100644 --- a/src/runtime/vdom/h.ts +++ b/packages/core/src/runtime/vdom/h.ts @@ -7,11 +7,11 @@ * Modified for Stencil's compiler and vdom */ -import { BUILD } from '@app-data'; -import { consoleDevError, consoleDevWarn, transformTag } from '@platform'; -import { isComplexType } from '../../utils/helpers'; +import { BUILD } from 'virtual:app-data'; +import { consoleDevError, consoleDevWarn, transformTag } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; +import { isComplexType } from '../../utils/helpers'; // export function h(nodeName: string | d.FunctionalComponent, vnodeData: d.PropsType, child?: d.ChildType): d.VNode; // export function h(nodeName: string | d.FunctionalComponent, vnodeData: d.PropsType, ...children: d.ChildType[]): d.VNode; @@ -61,9 +61,8 @@ Empty objects can also be the cause, look for JSX comments that became objects.` if (BUILD.slotRelocation && vnodeData.name) { slotName = vnodeData.name; } - // normalize class / className attributes if (BUILD.vdomClass) { - const classData = vnodeData.className || vnodeData.class; + const classData = vnodeData.class; if (classData) { vnodeData.class = typeof classData !== 'object' @@ -221,6 +220,8 @@ const validateInputProperties = (inputElm: HTMLInputElement): void => { const maxIndex = props.indexOf('max'); const stepIndex = props.indexOf('step'); if (value < typeIndex || value < minIndex || value < maxIndex || value < stepIndex) { - consoleDevWarn(`The "value" prop of should be set after "min", "max", "type" and "step"`); + consoleDevWarn( + `The "value" prop of should be set after "min", "max", "type" and "step"`, + ); } }; diff --git a/packages/core/src/runtime/vdom/jsx-runtime.ts b/packages/core/src/runtime/vdom/jsx-runtime.ts new file mode 100644 index 00000000000..7bfe181b484 --- /dev/null +++ b/packages/core/src/runtime/vdom/jsx-runtime.ts @@ -0,0 +1,31 @@ +import { h } from './h'; + +export function jsx(type: any, props: any, key?: string) { + const { children, ...rest } = props ?? {}; + + if (key !== undefined && !('key' in rest)) { + rest.key = key; + } + + const attrs = hasKeys(rest) ? rest : null; + + if (children !== undefined) { + return Array.isArray(children) ? h(type, attrs, ...children) : h(type, attrs, children); + } + return h(type, attrs); +} + +/** + * @alias + */ +export const jsxs = jsx; + +/** + * @alias + */ +export const jsxDEV = jsx; + +function hasKeys(obj: object) { + for (const _ in obj) return true; + return false; +} diff --git a/src/runtime/vdom/set-accessor.ts b/packages/core/src/runtime/vdom/set-accessor.ts similarity index 84% rename from src/runtime/vdom/set-accessor.ts rename to packages/core/src/runtime/vdom/set-accessor.ts index 4e34951be24..0306704638c 100644 --- a/src/runtime/vdom/set-accessor.ts +++ b/packages/core/src/runtime/vdom/set-accessor.ts @@ -7,11 +7,11 @@ * Modified for Stencil's compiler and vdom */ -import { BUILD } from '@app-data'; -import { getHostRef, isMemberInElement, plt, win } from '@platform'; -import { isComplexType } from '../../utils/helpers'; +import { BUILD } from 'virtual:app-data'; +import { getHostRef, isMemberInElement, plt, win } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; +import { isComplexType } from '../../utils/helpers'; import { NODE_TYPE, VNODE_FLAGS, XLINK_NS } from '../runtime-constants'; import { queueRefAttachment } from './vdom-render'; @@ -134,7 +134,7 @@ export const setAccessor = ( // the "Capture" suffix and make sure the event listener is setup to handle the capture event. const capture = memberName.endsWith(CAPTURE_EVENT_SUFFIX); // Make sure we only replace the last instance of "Capture" - memberName = memberName.replace(CAPTURE_EVENT_REGEX, ''); + memberName = memberName.replace(/*@__PURE__*/ new RegExp(CAPTURE_EVENT_SUFFIX + '$'), ''); if (oldValue) { plt.rel(elm, memberName, oldValue, capture); @@ -176,7 +176,7 @@ export const setAccessor = ( const propName = memberName.slice(5); try { (elm as any)[propName] = newValue; - } catch (e) { + } catch { /** * in case someone tries to set a read-only property, we just ignore it */ @@ -185,6 +185,35 @@ export const setAccessor = ( } else if (BUILD.vdomPropOrAttr) { // Set property if it exists and it's not a SVG const isComplex = isComplexType(newValue); + + // Check if this is an undefined custom element (tag contains '-' but not registered yet). + // For these elements, we queue the prop values to be applied when the element upgrades. + // This handles the case where parent components render child components before the child + // module is loaded (e.g., with lazy-loading custom-elements). + // We only queue props, not standard HTML attributes like aria-*, data-*, etc. + // which should be set directly as attributes. + // Note: This only applies to non-lazy-load builds (custom-elements bundle) since + // lazy-load builds handle component loading differently. + if (!BUILD.lazyLoad) { + const isStandardAttr = ln.startsWith('aria-') || ln.startsWith('data-'); + const isUndefinedCE = + !isProp && + !isStandardAttr && + elm.tagName?.includes('-') && + elm.tagName !== 'SLOT-FB' && + typeof customElements !== 'undefined' && + !customElements.get(elm.tagName.toLowerCase()); + + if (isUndefinedCE) { + // Queue the prop for when the element is defined and upgraded + if (!(elm as any)['s-pp']) { + (elm as any)['s-pp'] = new Map(); + } + (elm as any)['s-pp'].set(memberName, newValue); + return; + } + } + if ((isProp || (isComplex && newValue !== null)) && !isSvg) { try { if (!elm.tagName.includes('-')) { @@ -203,7 +232,7 @@ export const setAccessor = ( } else if ((elm as any)[memberName] !== newValue) { (elm as any)[memberName] = newValue; } - } catch (e) { + } catch { /** * in case someone tries to set a read-only property, e.g. "namespaceURI", we just ignore it */ @@ -219,7 +248,7 @@ export const setAccessor = ( */ let xlink = false; if (BUILD.vdomXlink) { - if (ln !== (ln = ln.replace(/^xlink\:?/, ''))) { + if (ln !== (ln = ln.replace(/^xlink:?/, ''))) { memberName = ln; xlink = true; } @@ -253,7 +282,9 @@ const parseClassListRegex = /\s/; * @param value className string, e.g. "foo bar baz" * @returns list of classes, e.g. ["foo", "bar", "baz"] */ -export const parseClassList = /*@__PURE__*/ (value: string | SVGAnimatedString | undefined | null): string[] => { +export const parseClassList = /*@__PURE__*/ ( + value: string | SVGAnimatedString | undefined | null, +): string[] => { // Can't use `value instanceof SVGAnimatedString` because it'll break in non-browser environments // see https://developer.mozilla.org/docs/Web/API/SVGAnimatedString for more information if (typeof value === 'object' && value && 'baseVal' in value) { @@ -267,4 +298,3 @@ export const parseClassList = /*@__PURE__*/ (value: string | SVGAnimatedString | return value.split(parseClassListRegex); }; const CAPTURE_EVENT_SUFFIX = 'Capture'; -const CAPTURE_EVENT_REGEX = new RegExp(CAPTURE_EVENT_SUFFIX + '$'); diff --git a/src/runtime/vdom/update-element.ts b/packages/core/src/runtime/vdom/update-element.ts similarity index 80% rename from src/runtime/vdom/update-element.ts rename to packages/core/src/runtime/vdom/update-element.ts index 4ae37be3e40..1c449c4477a 100644 --- a/src/runtime/vdom/update-element.ts +++ b/packages/core/src/runtime/vdom/update-element.ts @@ -1,6 +1,6 @@ -import { BUILD } from '@app-data'; +import { BUILD } from 'virtual:app-data'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; import { NODE_TYPE } from '../runtime-constants'; import { setAccessor } from './set-accessor'; @@ -22,6 +22,19 @@ export const updateElement = ( isSvgMode: boolean, isInitialRender?: boolean, ): void => { + const oldVnodeAttrs = (oldVnode && oldVnode.$attrs$) || {}; + const newVnodeAttrs = newVnode.$attrs$ || {}; + + const oldKeys = Object.keys(oldVnodeAttrs); + const newKeys = Object.keys(newVnodeAttrs); + + if (!BUILD.updatable && newKeys.length === 0) { + return; + } + if (BUILD.updatable && oldKeys.length === 0 && newKeys.length === 0) { + return; + } + // if the element passed in is a shadow root, which is a document fragment // then we want to be adding attrs/props to the shadow root's "host" element // if it's not a shadow root, then we add attrs/props to the same element @@ -29,12 +42,10 @@ export const updateElement = ( newVnode.$elm$.nodeType === NODE_TYPE.DocumentFragment && newVnode.$elm$.host ? newVnode.$elm$.host : (newVnode.$elm$ as any); - const oldVnodeAttrs = (oldVnode && oldVnode.$attrs$) || {}; - const newVnodeAttrs = newVnode.$attrs$ || {}; if (BUILD.updatable) { // remove attributes no longer present on the vnode by setting them to undefined - for (const memberName of sortedAttrNames(Object.keys(oldVnodeAttrs))) { + for (const memberName of sortedAttrNames(oldKeys)) { if (!(memberName in newVnodeAttrs)) { setAccessor( elm, @@ -50,7 +61,7 @@ export const updateElement = ( } // add new & update changed attributes - for (const memberName of sortedAttrNames(Object.keys(newVnodeAttrs))) { + for (const memberName of sortedAttrNames(newKeys)) { setAccessor( elm, memberName, @@ -75,9 +86,10 @@ export const updateElement = ( * @returns a list of attribute names, sorted if they include `"ref"` */ function sortedAttrNames(attrNames: string[]): string[] { + if (!BUILD.vdomRef) { + return attrNames; + } return attrNames.includes('ref') - ? // we need to sort these to ensure that `'ref'` is the last attr - [...attrNames.filter((attr) => attr !== 'ref'), 'ref'] - : // no need to sort, return the original array - attrNames; + ? [...attrNames.filter((attr) => attr !== 'ref'), 'ref'] + : attrNames; } diff --git a/packages/core/src/runtime/vdom/util.ts b/packages/core/src/runtime/vdom/util.ts new file mode 100755 index 00000000000..8c2b59d2b00 --- /dev/null +++ b/packages/core/src/runtime/vdom/util.ts @@ -0,0 +1,35 @@ +import type * as d from '@stencil/core'; + +import { NODE_TYPE } from '../runtime-constants'; +import { newVNode } from './h'; + +/** + * Derive a tree of virtual DOM nodes from a DOM node, handling the DOM node's + * children (if any) + * + * @param node a DOM node to use as a 'template' + * @returns a virtual DOM node based on the supplied DOM node + */ +export function toVNode(node: Node): d.VNode | null { + if (node.nodeType === NODE_TYPE.TextNode) { + const vnode: d.VNode = newVNode(null, node.textContent); + vnode.$elm$ = node; + return vnode; + } else if (node.nodeType === NODE_TYPE.ElementNode) { + const vnode: d.VNode = newVNode(node.nodeName.toLowerCase(), null); + vnode.$elm$ = node; + + const childNodes = (node as any).__childNodes || node.childNodes; + let childVnode: d.VNode; + + for (let i = 0, l = childNodes.length; i < l; i++) { + childVnode = toVNode(childNodes[i]); + if (childVnode) { + (vnode.$children$ ||= []).push(childVnode); + } + } + return vnode; + } + + return null; +} diff --git a/src/runtime/vdom/vdom-annotations.ts b/packages/core/src/runtime/vdom/vdom-annotations.ts similarity index 95% rename from src/runtime/vdom/vdom-annotations.ts rename to packages/core/src/runtime/vdom/vdom-annotations.ts index 3c8155bb8f1..191991ddd76 100644 --- a/src/runtime/vdom/vdom-annotations.ts +++ b/packages/core/src/runtime/vdom/vdom-annotations.ts @@ -1,6 +1,6 @@ -import { getHostRef } from '@platform'; +import { getHostRef } from 'virtual:platform'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; import { COMMENT_NODE_ID, CONTENT_REF_ID, @@ -30,7 +30,8 @@ export const insertVdomAnnotations = (doc: Document, staticComponents: string[]) * Initiated `docData` object from the document if it exists to ensure we * maintain the same `docData` object across multiple hydration hydration runs. */ - const docData: d.DocData = STENCIL_DOC_DATA in doc ? (doc[STENCIL_DOC_DATA] as d.DocData) : { ...DEFAULT_DOC_DATA }; + const docData: d.DocData = + STENCIL_DOC_DATA in doc ? (doc[STENCIL_DOC_DATA] as d.DocData) : { ...DEFAULT_DOC_DATA }; docData.staticComponents = new Set(staticComponents); const orgLocationNodes: d.RenderNode[] = []; @@ -126,7 +127,10 @@ const parseVNodeAnnotations = ( * we need to insert the vnode annotations on the host element children as well * as on the children from its shadowRoot if there is one */ - const childNodes = [...Array.from(node.childNodes), ...Array.from(node.shadowRoot?.childNodes || [])]; + const childNodes = [ + ...Array.from(node.childNodes), + ...Array.from(node.shadowRoot?.childNodes || []), + ]; childNodes.forEach((childNode) => { const hostRef = getHostRef(childNode as d.RuntimeRef); if (hostRef != null && !docData.staticComponents.has(childNode.nodeName.toLowerCase())) { @@ -253,8 +257,8 @@ const insertChildVNodeAnnotations = ( if (vnodeChild.$children$ != null) { // Increment depth each time we recur deeper into the tree const childDepth = depth + 1; - vnodeChild.$children$.forEach((vnode, index) => { - insertChildVNodeAnnotations(doc, vnode, cmpData, hostId, childDepth, index); + vnodeChild.$children$.forEach((vnode, childIndex) => { + insertChildVNodeAnnotations(doc, vnode, cmpData, hostId, childDepth, childIndex); }); } }; diff --git a/src/runtime/vdom/vdom-render.ts b/packages/core/src/runtime/vdom/vdom-render.ts similarity index 91% rename from src/runtime/vdom/vdom-render.ts rename to packages/core/src/runtime/vdom/vdom-render.ts index c404c062600..cc37904ecd3 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/packages/core/src/runtime/vdom/vdom-render.ts @@ -6,13 +6,14 @@ * * Modified for Stencil's renderer and slot projection */ -import { BUILD } from '@app-data'; -import { consoleDevError, plt, supportsShadow, win } from '@platform'; +import { BUILD } from 'virtual:app-data'; +import { consoleDevError, getHostRef, plt, win } from 'virtual:platform'; +import type * as d from '@stencil/core'; + import { CMP_FLAGS, HTML_NS, NODE_TYPES, SVG_NS } from '../../utils/constants'; import { isDef } from '../../utils/helpers'; - -import type * as d from '../../declarations'; import { patchParentNode } from '../dom-extras'; +import { getShadowRoot } from '../element'; import { NODE_TYPE, PLATFORM_FLAGS, VNODE_FLAGS } from '../runtime-constants'; import { dispatchSlotChangeEvent, @@ -21,7 +22,7 @@ import { patchSlotNode, updateFallbackSlotVisibility, } from '../slot-polyfill-utils'; -import { h, isHost, newVNode } from './h'; +import { h, isHost, newVNode as createVNode } from './h'; import { updateElement } from './update-element'; let scopeId: string; @@ -37,8 +38,8 @@ let isSvgMode = false; * These ensure that ref callbacks are called in the correct order: * first all removal callbacks (with null), then all attachment callbacks (with elements). */ -let refCallbacksToRemove: Array<() => void> = []; -let refCallbacksToAttach: Array<() => void> = []; +const refCallbacksToRemove: Array<() => void> = []; +const refCallbacksToAttach: Array<() => void> = []; /** * Create a DOM Node corresponding to one of the children of a given VNode. @@ -102,7 +103,7 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: } if (!win.document) { - throw new Error("You are trying to render a Stencil component in an environment that doesn't support the DOM."); + throw new Error('No DOM environment available for rendering.'); } // create element @@ -110,12 +111,16 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: BUILD.svg ? win.document.createElementNS( isSvgMode ? SVG_NS : HTML_NS, - !useNativeShadowDom && BUILD.slotRelocation && newVNode.$flags$ & VNODE_FLAGS.isSlotFallback + !useNativeShadowDom && + BUILD.slotRelocation && + newVNode.$flags$ & VNODE_FLAGS.isSlotFallback ? 'slot-fb' : (newVNode.$tag$ as string), ) : win.document.createElement( - !useNativeShadowDom && BUILD.slotRelocation && newVNode.$flags$ & VNODE_FLAGS.isSlotFallback + !useNativeShadowDom && + BUILD.slotRelocation && + newVNode.$flags$ & VNODE_FLAGS.isSlotFallback ? 'slot-fb' : (newVNode.$tag$ as string), ) @@ -140,7 +145,8 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: } if (newVNode.$children$) { // For template elements, children should be appended to the content DocumentFragment - const appendTarget = newVNode.$tag$ === 'template' ? (elm as HTMLTemplateElement).content : elm; + const appendTarget = + newVNode.$tag$ === 'template' ? (elm as HTMLTemplateElement).content : elm; for (i = 0; i < newVNode.$children$.length; ++i) { // create the node childNode = createElm(oldParentVNode, newVNode, i); @@ -185,7 +191,8 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: patchSlotNode(elm); // check if we've got an old vnode for this slot - oldVNode = oldParentVNode && oldParentVNode.$children$ && oldParentVNode.$children$[childIndex]; + oldVNode = + oldParentVNode && oldParentVNode.$children$ && oldParentVNode.$children$[childIndex]; if (oldVNode && oldVNode.$tag$ === newVNode.$tag$ && oldParentVNode.$elm$) { // we've got an old slot vnode and the wrapper is being replaced // so let's move the old slot content to the root of the element currently being rendered @@ -214,9 +221,9 @@ const relocateToHostRoot = (parentElm: Element) => { const host = parentElm.closest(hostTagName.toLowerCase()); if (host != null) { - const contentRefNode = (Array.from((host as d.RenderNode).__childNodes || host.childNodes) as d.RenderNode[]).find( - (ref) => ref['s-cr'], - ); + const contentRefNode = ( + Array.from((host as d.RenderNode).__childNodes || host.childNodes) as d.RenderNode[] + ).find((ref) => ref['s-cr']); const childNodeArray = Array.from( (parentElm as d.RenderNode).__childNodes || parentElm.childNodes, ) as d.RenderNode[]; @@ -308,10 +315,15 @@ const addVnodes = ( startIdx: number, endIdx: number, ) => { - let containerElm = ((BUILD.slotRelocation && parentElm['s-cr'] && parentElm['s-cr'].parentNode) || parentElm) as any; + let containerElm = ((BUILD.slotRelocation && parentElm['s-cr'] && parentElm['s-cr'].parentNode) || + parentElm) as any; let childNode: Node; - if (BUILD.shadowDom && (containerElm as any).shadowRoot && containerElm.tagName === hostTagName) { - containerElm = (containerElm as any).shadowRoot; + if (BUILD.shadowDom && containerElm.tagName === hostTagName) { + // Use getShadowRoot to handle both open and closed shadow DOM + const shadow = getShadowRoot(containerElm); + if (shadow) { + containerElm = shadow; + } } // For template elements, children should be added to the content DocumentFragment @@ -324,7 +336,11 @@ const addVnodes = ( childNode = createElm(null, parentVNode, startIdx); if (childNode) { vnodes[startIdx].$elm$ = childNode as any; - insertBefore(containerElm, childNode as d.RenderNode, BUILD.slotRelocation ? referenceNode(before) : before); + insertBefore( + containerElm, + childNode as d.RenderNode, + BUILD.slotRelocation ? referenceNode(before) : before, + ); } } } @@ -461,7 +477,8 @@ const updateChildren = ( let elmToMove: d.VNode; // For template elements, we need to work with the content DocumentFragment - const containerElm = newVNode.$tag$ === 'template' ? (parentElm as HTMLTemplateElement).content : parentElm; + const containerElm = + newVNode.$tag$ === 'template' ? (parentElm as HTMLTemplateElement).content : parentElm; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { @@ -503,7 +520,10 @@ const updateChildren = ( // // In this situation we need to patch `newEndVnode` onto `oldStartVnode` // and move the DOM element for `oldStartVnode`. - if (BUILD.slotRelocation && (oldStartVnode.$tag$ === 'slot' || newEndVnode.$tag$ === 'slot')) { + if ( + BUILD.slotRelocation && + (oldStartVnode.$tag$ === 'slot' || newEndVnode.$tag$ === 'slot') + ) { putBackInOriginalLocation(oldStartVnode.$elm$.parentNode, false); } patch(oldStartVnode, newEndVnode, isInitialRender); @@ -543,7 +563,10 @@ const updateChildren = ( // (which will handle updating any changed attributes, reconciling their // children etc) but we also need to move the DOM node to which // `oldEndVnode` corresponds. - if (BUILD.slotRelocation && (oldStartVnode.$tag$ === 'slot' || newEndVnode.$tag$ === 'slot')) { + if ( + BUILD.slotRelocation && + (oldStartVnode.$tag$ === 'slot' || newEndVnode.$tag$ === 'slot') + ) { putBackInOriginalLocation(oldEndVnode.$elm$.parentNode, false); } patch(oldEndVnode, newStartVnode, isInitialRender); @@ -799,7 +822,8 @@ const markSlotContentForRelocation = (elm: d.RenderNode) => { if (childNode['s-sr'] && (node = childNode['s-cr']) && node.parentNode) { // first get the content reference comment node ('s-cr'), then we get // its parent, which is where all the host content is now - hostContentNodes = (node.parentNode as d.RenderNode).__childNodes || node.parentNode.childNodes; + hostContentNodes = + (node.parentNode as d.RenderNode).__childNodes || node.parentNode.childNodes; const slotName = childNode['s-sn']; // iterate through all the nodes under the location where the host was @@ -888,13 +912,15 @@ const markSlotContentForRelocation = (elm: d.RenderNode) => { * * @param vNode a virtual DOM node */ -export const nullifyVNodeRefs = (vNode: d.VNode) => { +const nullifyVNodeRefs = (vNode: d.VNode) => { if (BUILD.vdomRef) { if (vNode.$attrs$ && vNode.$attrs$.ref) { // Queue the ref removal callback to be called later refCallbacksToRemove.push(() => vNode.$attrs$.ref(null)); } - vNode.$children$ && vNode.$children$.map(nullifyVNodeRefs); + if (vNode.$children$) { + vNode.$children$.map(nullifyVNodeRefs); + } } }; @@ -945,15 +971,36 @@ export const insertBefore = ( reference?: d.RenderNode | d.PatchedSlotNode, isInitialLoad?: boolean, ): Node => { - // if (BUILD.slotRelocation) { - if (BUILD.scoped && typeof newNode['s-sn'] === 'string' && !!newNode['s-sr'] && !!newNode['s-cr']) { + if ( + BUILD.scoped && + typeof newNode['s-sn'] === 'string' && + !!newNode['s-sr'] && + !!newNode['s-cr'] + ) { // this is a slot node - addRemoveSlotScopedClass(newNode['s-cr'], newNode, parent as d.RenderNode, newNode.parentElement); + addRemoveSlotScopedClass( + newNode['s-cr'], + newNode, + parent as d.RenderNode, + newNode.parentElement, + ); } else if (typeof newNode['s-sn'] === 'string') { // this is a slotted node. - if (BUILD.experimentalSlotFixes && parent.getRootNode().nodeType !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE) { + const hostElm = newNode['s-hn'] && (parent as Element).closest?.(newNode['s-hn']); + const shouldPatchSlottedNodes = + BUILD.lightDomPatches || + BUILD.slotChildNodes || + (BUILD.patchAll && + !!( + hostElm && getHostRef(hostElm as d.HostElement)?.$cmpMeta$.$flags$ & CMP_FLAGS.patchAll + )); + + if ( + shouldPatchSlottedNodes && // we don't need to patch this node if it's nested in a shadow root + parent.getRootNode().nodeType !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE + ) { patchParentNode(newNode); } // potentially use the patched insertBefore method. This will correctly slot the new node @@ -992,24 +1039,25 @@ function addRemoveSlotScopedClass( ) { // if the new node to move is slotted, // find it's original parent component and see if has a scope id - let scopeId: string; + let slotScopeId: string; if ( reference && typeof slotNode['s-sn'] === 'string' && !!slotNode['s-sr'] && reference.parentNode && (reference.parentNode as d.RenderNode)['s-sc'] && - (scopeId = slotNode['s-si'] || (reference.parentNode as d.RenderNode)['s-sc']) + (slotScopeId = slotNode['s-si'] || (reference.parentNode as d.RenderNode)['s-sc']) ) { const scopeName = slotNode['s-sn']; const hostName = slotNode['s-hn']; // we found the original parent component's scoped id // let's add a scoped-slot class to this slotted node's parent - newParent.classList?.add(scopeId + '-s'); + newParent.classList?.add(slotScopeId + '-s'); - if (oldParent && oldParent.classList?.contains(scopeId + '-s')) { - let child = ((oldParent as d.RenderNode).__childNodes || oldParent.childNodes)[0] as d.RenderNode; + if (oldParent && oldParent.classList?.contains(slotScopeId + '-s')) { + let child = ((oldParent as d.RenderNode).__childNodes || + oldParent.childNodes)[0] as d.RenderNode; let found = false; while (child) { @@ -1022,7 +1070,7 @@ function addRemoveSlotScopedClass( // there are no other slots in the old parent // let's remove the scoped-slot class - if (!found) oldParent.classList.remove(scopeId + '-s'); + if (!found) oldParent.classList.remove(slotScopeId + '-s'); } } } @@ -1048,10 +1096,14 @@ interface RelocateNodeData { * @param renderFnResults the virtual DOM nodes to be rendered * @param isInitialLoad whether or not this is the first call after page load */ -export const renderVdom = (hostRef: d.HostRef, renderFnResults: d.VNode | d.VNode[], isInitialLoad = false) => { +export const renderVdom = ( + hostRef: d.HostRef, + renderFnResults: d.VNode | d.VNode[], + isInitialLoad = false, +) => { const hostElm = hostRef.$hostElement$; const cmpMeta = hostRef.$cmpMeta$; - const oldVNode: d.VNode = hostRef.$vnode$ || newVNode(null, null); + const oldVNode: d.VNode = hostRef.$vnode$ || createVNode(null, null); const isHostElement = isHost(renderFnResults); // if `renderFnResults` is a Host node then we can use it directly. If not, @@ -1115,16 +1167,18 @@ render() { rootVnode.$tag$ = null; rootVnode.$flags$ |= VNODE_FLAGS.isHost; hostRef.$vnode$ = rootVnode; - rootVnode.$elm$ = oldVNode.$elm$ = (BUILD.shadowDom ? hostElm.shadowRoot || hostElm : hostElm) as any; + rootVnode.$elm$ = oldVNode.$elm$ = ( + BUILD.shadowDom ? getShadowRoot(hostElm) || hostElm : hostElm + ) as any; if (BUILD.scoped || BUILD.shadowDom) { scopeId = hostElm['s-sc']; } useNativeShadowDom = - supportsShadow && !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && !(cmpMeta.$flags$ & CMP_FLAGS.shadowNeedsScopedCss); + if (BUILD.slotRelocation) { contentRef = hostElm['s-cr']; @@ -1171,7 +1225,7 @@ render() { if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode && isInitialLoad) { // Store the initial value of `hidden` so we can reset it later when // moving nodes around. - nodeToRelocate['s-ih'] = nodeToRelocate.hidden ?? false; + nodeToRelocate['s-ih'] = !!nodeToRelocate.hidden; } if (slotRefNode) { @@ -1189,7 +1243,11 @@ render() { // we need to do some additional checks to make sure we're inserting the node in the correct order. // The use case here would be that we have multiple nodes being relocated to the same slot. So, we want // to make sure they get inserted into their new home in the same order they were declared in their original location. - if (!BUILD.hydrateServerSide && insertBeforeNode && insertBeforeNode.nodeType === NODE_TYPE.ElementNode) { + if ( + !BUILD.hydrateServerSide && + insertBeforeNode && + insertBeforeNode.nodeType === NODE_TYPE.ElementNode + ) { let orgLocationNode = nodeToRelocate['s-ol']?.previousSibling as d.RenderNode | null; while (orgLocationNode) { let refNode = (orgLocationNode['s-nr'] as d.RenderNode) ?? null; @@ -1197,7 +1255,8 @@ render() { if ( refNode && refNode['s-sn'] === nodeToRelocate['s-sn'] && - parentNodeRef === ((refNode as d.PatchedSlotNode).__parentNode || refNode.parentNode) + parentNodeRef === + ((refNode as d.PatchedSlotNode).__parentNode || refNode.parentNode) ) { refNode = refNode.nextSibling as d.RenderNode | null; @@ -1217,8 +1276,10 @@ render() { } } - const parent = (nodeToRelocate as d.PatchedSlotNode).__parentNode || nodeToRelocate.parentNode; - const nextSibling = (nodeToRelocate as d.PatchedSlotNode).__nextSibling || nodeToRelocate.nextSibling; + const parent = + (nodeToRelocate as d.PatchedSlotNode).__parentNode || nodeToRelocate.parentNode; + const nextSibling = + (nodeToRelocate as d.PatchedSlotNode).__nextSibling || nodeToRelocate.nextSibling; if ((!insertBeforeNode && parentNodeRef !== parent) || nextSibling !== insertBeforeNode) { // we've checked that it's worth while to relocate // since that the node to relocate @@ -1232,9 +1293,14 @@ render() { insertBefore(parentNodeRef, nodeToRelocate, insertBeforeNode, isInitialLoad); // // If this is a comment node that represents a hidden text node, convert it back to text - if (nodeToRelocate.nodeType === NODE_TYPE.CommentNode && nodeToRelocate.nodeValue.startsWith('s-nt-')) { + if ( + nodeToRelocate.nodeType === NODE_TYPE.CommentNode && + nodeToRelocate.nodeValue.startsWith('s-nt-') + ) { // create a text node to replace the comment node - const textNode = win.document.createTextNode(nodeToRelocate.nodeValue.replace(/^s-nt-/, '')) as any; + const textNode = win.document.createTextNode( + nodeToRelocate.nodeValue.replace(/^s-nt-/, ''), + ) as any; // Copy over Stencil properties textNode['s-hn'] = nodeToRelocate['s-hn']; // host (component) name textNode['s-sn'] = nodeToRelocate['s-sn']; // slot name @@ -1251,12 +1317,17 @@ render() { // This solves a problem where a `slot` is dynamically rendered and `hidden` may have // been set on content originally, but now it has a slot to go to so it should have // the value it was defined as having in the DOM, not what we overrode it to. - if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode && nodeToRelocate.tagName !== 'SLOT-FB') { + if ( + nodeToRelocate.nodeType === NODE_TYPE.ElementNode && + nodeToRelocate.tagName !== 'SLOT-FB' + ) { nodeToRelocate.hidden = nodeToRelocate['s-ih'] ?? false; } } } - nodeToRelocate && typeof slotRefNode['s-rf'] === 'function' && slotRefNode['s-rf'](slotRefNode); + if (nodeToRelocate && typeof slotRefNode['s-rf'] === 'function') { + slotRefNode['s-rf'](slotRefNode); + } } else if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode) { // this node doesn't have a slot home to go to, so let's hide it nodeToRelocate.hidden = true; diff --git a/src/hydrate/platform/test/__mocks__/@app-globals/index.ts b/packages/core/src/server/platform/_test_/__mocks__/@app-globals/index.ts similarity index 100% rename from src/hydrate/platform/test/__mocks__/@app-globals/index.ts rename to packages/core/src/server/platform/_test_/__mocks__/@app-globals/index.ts diff --git a/packages/core/src/server/platform/_test_/serialize-shadow-root-opts.spec.ts b/packages/core/src/server/platform/_test_/serialize-shadow-root-opts.spec.ts new file mode 100644 index 00000000000..fb8f1e09ef3 --- /dev/null +++ b/packages/core/src/server/platform/_test_/serialize-shadow-root-opts.spec.ts @@ -0,0 +1,54 @@ +import { expect, describe, it, afterEach, vi } from '@stencil/vitest'; + +import { tagRequiresScoped } from '../ssr-app'; + +describe('tagRequiresScoped', () => { + afterEach(async () => { + vi.resetModules(); + }); + + it('should return true for a component with serializeShadowRoot: true', () => { + expect(tagRequiresScoped('cmp-a', true)).toBe(false); + }); + + it('should return false for a component serializeShadowRoot: false', () => { + expect(tagRequiresScoped('cmp-b', false)).toBe(true); + }); + + it('should return false for a component with serializeShadowRoot: undefined', () => { + expect(tagRequiresScoped('cmp-c', undefined)).toBe(false); + }); + + it('should return true for a component with serializeShadowRoot: "scoped"', () => { + expect(tagRequiresScoped('cmp-d', 'scoped')).toBe(true); + }); + + it('should return false for a component with serializeShadowRoot: "declarative-shadow-dom"', () => { + expect(tagRequiresScoped('cmp-e', 'declarative-shadow-dom')).toBe(false); + }); + + it('should return true for a component when tag is in scoped list', () => { + expect(tagRequiresScoped('cmp-f', { scoped: ['cmp-f'], default: 'scoped' })).toBe(true); + }); + + it('should return false for a component when tag is not scoped list', () => { + expect( + tagRequiresScoped('cmp-g', { scoped: ['cmp-f'], default: 'declarative-shadow-dom' }), + ).toBe(false); + }); + + it('should return true for a component when default is scoped', () => { + expect( + tagRequiresScoped('cmp-g', { 'declarative-shadow-dom': ['cmp-f'], default: 'scoped' }), + ).toBe(true); + }); + + it('should return false for a component when default is declarative-shadow-dom', () => { + expect( + tagRequiresScoped('cmp-g', { + 'declarative-shadow-dom': ['cmp-f'], + default: 'declarative-shadow-dom', + }), + ).toBe(false); + }); +}); diff --git a/src/hydrate/platform/h-async.ts b/packages/core/src/server/platform/h-async.ts similarity index 87% rename from src/hydrate/platform/h-async.ts rename to packages/core/src/server/platform/h-async.ts index a60ca5c9560..065c5ac32d5 100644 --- a/src/hydrate/platform/h-async.ts +++ b/packages/core/src/server/platform/h-async.ts @@ -1,7 +1,7 @@ -import { consoleDevError } from '@platform'; -import { h } from '@runtime'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; +import { h } from '../../runtime'; +import { consoleDevError } from './'; export const hAsync = (nodeName: any, vnodeData: any, ...children: d.ChildType[]) => { if (Array.isArray(children) && children.length > 0) { diff --git a/packages/core/src/server/platform/index.ts b/packages/core/src/server/platform/index.ts new file mode 100644 index 00000000000..84c5fe20cbf --- /dev/null +++ b/packages/core/src/server/platform/index.ts @@ -0,0 +1,272 @@ +/** + * This file: + * 1) Overrides client specific functions with server specific implementations + * 2) Re-export the rest of the runtime API from the client index, so that the server platform can use it as well + **/ + +import { BUILD } from 'virtual:app-data'; +import type * as d from '@stencil/core'; + +import { CMP_FLAGS } from '../../utils/constants'; +import { reWireGetterSetter } from '../../utils/es2022-rewire-class-members'; + +/** + * Access transformTag via the closure-scoped $stencilTagTransform object. + * This object is defined in the factory closure (SSR_FACTORY_INTRO). + * We declare it here to satisfy TypeScript, but at runtime it will be + * provided by the factory closure scope. + */ +declare const $stencilTagTransform: { transformTag: (tag: string) => string }; + +let customError: d.ErrorHandler; + +export const cmpModules = new Map(); + +const getModule = (tagName: string): d.ComponentConstructor | null => { + if (typeof tagName === 'string') { + tagName = tagName.toLowerCase(); + const cmpModule = cmpModules.get(tagName); + if (cmpModule != null) { + return cmpModule[tagName]; + } + } + return null; +}; + +export const loadModule = ( + cmpMeta: d.ComponentRuntimeMeta, + _hostRef: d.HostRef, + _hmrVersionId?: string, +): d.ComponentConstructor | null => { + return getModule(cmpMeta.$tagName$); +}; + +export const isMemberInElement = (elm: any, memberName: string) => { + if (elm != null) { + if (memberName in elm) { + return true; + } + const cstr = getModule(elm.nodeName); + if (cstr != null) { + const hostRef: d.ComponentNativeConstructor = cstr as any; + if (hostRef != null && hostRef.cmpMeta != null && hostRef.cmpMeta.$members$ != null) { + return memberName in hostRef.cmpMeta.$members$; + } + } + } + return false; +}; + +export const registerComponents = (Cstrs: d.ComponentNativeConstructor[]) => { + for (const Cstr of Cstrs) { + // using this format so it follows exactly how client-side modules work + const exportName = Cstr.cmpMeta.$tagName$; + // Access transformTag from the closure-scoped $stencilTagTransform object + // This ensures we use the same instance as the runner (prevents duplication) + const transformedTagName = $stencilTagTransform.transformTag(exportName); + + cmpModules.set(exportName, { + [exportName]: Cstr, + }); + if (transformedTagName !== exportName) { + cmpModules.set(transformedTagName, { + [transformedTagName]: Cstr, + }); + } + } +}; + +export const win = window; + +export const readTask = (cb: Function) => { + nextTick(() => { + try { + cb(); + } catch (e) { + consoleError(e); + } + }); +}; + +export const writeTask = (cb: Function) => { + nextTick(() => { + try { + cb(); + } catch (e) { + consoleError(e); + } + }); +}; + +const resolved = /*@__PURE__*/ Promise.resolve(); +export const nextTick = (cb: () => void) => resolved.then(cb); + +const defaultConsoleError = (e: any) => { + if (e != null) { + console.error(e.stack || e.message || e); + } +}; + +export const consoleError: d.ErrorHandler = (e: any, el?: any) => + (customError || defaultConsoleError)(e, el); + +export const consoleDevError = (..._: any[]) => { + /* noop for hydrate */ +}; + +export const consoleDevWarn = (..._: any[]) => { + /* noop for hydrate */ +}; + +export const consoleDevInfo = (..._: any[]) => { + /* noop for hydrate */ +}; + +export const setErrorHandler = (handler: d.ErrorHandler) => (customError = handler); + +export const plt: d.PlatformRuntime = { + $flags$: 0, + $resourcesUrl$: '', + jmp: (h) => h(), + raf: (h) => requestAnimationFrame(h), + ael: (el, eventName, listener, opts) => el.addEventListener(eventName, listener, opts), + rel: (el, eventName, listener, opts) => el.removeEventListener(eventName, listener, opts), + ce: (eventName, opts) => new win.CustomEvent(eventName, opts), +}; + +export const setPlatformHelpers = (helpers: { + jmp?: (c: any) => any; + raf?: (c: any) => number; + ael?: (el: any, eventName: string, listener: any, options: any) => void; + rel?: (el: any, eventName: string, listener: any, options: any) => void; + ce?: (eventName: string, opts?: any) => any; +}) => { + Object.assign(plt, helpers); +}; + +export const supportsListenerOptions = false; + +export const supportsConstructableStylesheets = false; +export const supportsMutableAdoptedStyleSheets = false; + +export const getHostRef = (ref: d.RuntimeRef) => { + if (ref.__s_ghr) { + return ref.__s_ghr(); + } + + return undefined; +}; + +export const registerInstance = (lazyInstance: any, hostRef: d.HostRef) => { + if (!hostRef) return undefined; + lazyInstance.__s_ghr = () => hostRef; + hostRef.$lazyInstance$ = lazyInstance; + + if (hostRef.$cmpMeta$.$flags$ & CMP_FLAGS.hasModernPropertyDecls && (BUILD.state || BUILD.prop)) { + reWireGetterSetter(lazyInstance, hostRef); + } + return hostRef; +}; + +export const registerHost = (elm: d.HostElement, cmpMeta: d.ComponentRuntimeMeta) => { + const hostRef: d.HostRef = { + $flags$: 0, + $cmpMeta$: cmpMeta, + $hostElement$: elm, + $instanceValues$: new Map(), + $serializerValues$: new Map(), + $renderCount$: 0, + }; + hostRef.$fetchedCbList$ = []; + hostRef.$onInstancePromise$ = new Promise((r) => (hostRef.$onInstanceResolve$ = r)); + hostRef.$onReadyPromise$ = new Promise((r) => (hostRef.$onReadyResolve$ = r)); + elm['s-p'] = []; + elm['s-rc'] = []; + elm.__s_ghr = () => hostRef; + + return hostRef; +}; + +export const Build: d.UserBuildConditionals = { + isDev: false, + isBrowser: false, + isServer: true, + isTesting: false, +}; + +export const styles: d.StyleMap = new Map(); +export const modeResolutionChain: d.ResolutionHandler[] = []; + +/** + * Server-side implementation of getAssetPath. + * + * Unlike the client-side version, this doesn't use import.meta.url as a fallback + * because it doesn't make sense in the bundled hydrate factory context. + * The base URL must come from plt.$resourcesUrl$ (set via hydration options). + * @param path - The relative path to the asset + * @returns The resolved asset path, which may be an absolute URL if resourcesUrl is set to an external URL, or a relative path if resourcesUrl is a relative path or not set at all + */ +export const getAssetPath = (path: string) => { + // In the server/hydrate context, resourcesUrl should be set via ssrDocument options + // If not set, default to './' which is a reasonable default for server-side rendering + const base = plt.$resourcesUrl$ || './'; + const assetUrl = new URL(path, base); + return assetUrl.origin !== win.location.origin ? assetUrl.href : assetUrl.pathname; +}; + +/** + * Sets the base URL for resolving asset paths in the server/hydrate context. + * @param path - The base URL to use for resolving asset paths. This should typically be set to the same value as the `resourcesUrl` option passed to `ssrDocument` to ensure that asset paths are resolved correctly in the server/hydrate context. + * If not set, it defaults to './', which is a reasonable default for server-side rendering. + * @returns void + */ +export const setAssetPath = (path: string) => (plt.$resourcesUrl$ = path); + +/** + * Checks to see any components are rendered with `scoped` + * @param opts - SSR options + */ +export const setScopedSsr = (opts: d.SsrFactoryOptions) => { + scopedSSR = + BUILD.shadowDom && + opts.serializeShadowRoot !== false && + opts.serializeShadowRoot !== 'declarative-shadow-dom'; +}; +export const needsScopedSSR = () => scopedSSR; + +let scopedSSR = false; + +export { hAsync as h } from './h-async'; +export { ssrApp } from './ssr-app'; +export { BUILD, Env, NAMESPACE } from 'virtual:app-data'; +export { + addHostEventListeners, + bootstrapLazy, + connectedCallback, + createEvent, + defineCustomElement, + disconnectedCallback, + forceModeUpdate, + forceUpdate, + Fragment, + getElement, + getMode, + getRenderingRef, + getShadowRoot, + getValue, + Host, + insertVdomAnnotations, + jsx, + jsxs, + Mixin, + parsePropertyValue, + postUpdateComponent, + proxyComponent, + proxyCustomElement, + renderVdom, + setMode, + setNonce, + setTagTransformer, + setValue, + transformTag, +} from '../../runtime'; diff --git a/src/hydrate/platform/proxy-host-element.ts b/packages/core/src/server/platform/proxy-host-element.ts similarity index 90% rename from src/hydrate/platform/proxy-host-element.ts rename to packages/core/src/server/platform/proxy-host-element.ts index 44f48506258..70e28aaf7e1 100644 --- a/src/hydrate/platform/proxy-host-element.ts +++ b/packages/core/src/server/platform/proxy-host-element.ts @@ -1,8 +1,9 @@ -import { consoleError, getHostRef } from '@platform'; -import { getValue, parsePropertyValue, setValue } from '@runtime'; -import { CMP_FLAGS, createShadowRoot, MEMBER_FLAGS } from '@utils'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; +import { getValue, parsePropertyValue, setValue } from '../../runtime'; +import { CMP_FLAGS, MEMBER_FLAGS } from '../../utils/constants'; +import { createShadowRoot } from '../../utils/shadow-root'; +import { consoleError, getHostRef } from './index'; export function proxyHostElement(elm: d.HostElement, cstr: d.ComponentConstructor): void { const cmpMeta = cstr.cmpMeta; @@ -58,7 +59,11 @@ export function proxyHostElement(elm: d.HostElement, cstr: d.ComponentConstructo } } else { // otherwise, convert from string to correct type - attrPropVal = parsePropertyValue(attrValue, memberFlags, !!(cmpMeta.$flags$ & CMP_FLAGS.formAssociated)); + attrPropVal = parsePropertyValue( + attrValue, + memberFlags, + !!(cmpMeta.$flags$ & CMP_FLAGS.formAssociated), + ); } } diff --git a/packages/core/src/server/platform/ssr-app.ts b/packages/core/src/server/platform/ssr-app.ts new file mode 100644 index 00000000000..1e3fa9e05a6 --- /dev/null +++ b/packages/core/src/server/platform/ssr-app.ts @@ -0,0 +1,431 @@ +import { globalScripts } from 'virtual:app-globals'; +import type * as d from '@stencil/core'; + +import { connectedCallback, insertVdomAnnotations, addHostEventListeners } from '../../runtime'; +import { CMP_FLAGS } from '../../utils/constants'; +import { proxyHostElement } from './proxy-host-element'; +import { getHostRef, loadModule, plt, registerHost, setScopedSsr } from './index'; + +/** + * Native setTimeout/clearTimeout captured before globalThis is shadowed in the factory closure. + * These are used for the hydrate timeout timer to avoid being affected by constrainTimeouts + * which modifies the MockWindow's setTimeout behavior. + * Defined in SSR_FACTORY_INTRO (ssr-factory-closure.ts). + */ +declare const $nativeSetTimeout: typeof setTimeout; +declare const $nativeClearTimeout: typeof clearTimeout; + +/** + * SSR a Document by patching the DOM APIs to wait for components to be connected and hydrated + * before allowing them to be added to the document. + * Once all components are hydrated, the `afterSsr` callback is called so that the caller can serialize + * the document to HTML and send it back to the client. + * @param win The window to use for SSR. This should be a patched window created by `patchDomImplementation`. + * @param opts The options to use for SSR. This is used to configure which components should be hydrated, how long to wait for hydration, etc. + * @param results The results object to store the hydration results. + * @param afterSsr The callback to be called after SSR is complete. + * @param resolve The resolve function to be called when SSR is complete. + */ +export function ssrApp( + win: Window & typeof globalThis, + opts: d.SsrFactoryOptions, + results: d.SsrResults, + afterSsr: ( + win: Window, + opts: d.SsrFactoryOptions, + results: d.SsrResults, + resolve: (results: d.SsrResults) => void, + ) => void, + resolve: (results: d.SsrResults) => void, +) { + const connectedElements = new Set(); + const createdElements = new Set(); + const waitingElements = new Set(); + const orgDocumentCreateElement = win.document.createElement; + const orgDocumentCreateElementNS = win.document.createElementNS; + const resolved = Promise.resolve(); + setScopedSsr(opts); + + let tmrId: any; + let ranCompleted = false; + + function hydratedComplete() { + $nativeClearTimeout(tmrId); + createdElements.clear(); + connectedElements.clear(); + + if (!ranCompleted) { + ranCompleted = true; + try { + if (opts.clientSsrAnnotations) { + insertVdomAnnotations(win.document, opts.staticComponents); + } + + win.dispatchEvent(new win.Event('DOMContentLoaded')); + + win.document.createElement = orgDocumentCreateElement; + win.document.createElementNS = orgDocumentCreateElementNS; + } catch (e) { + renderCatchError(opts, results, e); + } + } + + afterSsr(win, opts, results, resolve); + } + + function hydratedError(err: any) { + renderCatchError(opts, results, err); + hydratedComplete(); + } + + function timeoutExceeded() { + hydratedError(`Hydrate exceeded timeout${waitingOnElementsMsg(waitingElements)}`); + } + + try { + function patchedConnectedCallback(this: d.HostElement) { + return connectElement(this); + } + + function patchElement(elm: d.HostElement) { + if (isValidComponent(elm, opts)) { + // this element is a valid component + + const hostRef = getHostRef(elm); + if (!hostRef) { + // we haven't registered this component's host element yet + + // get the component's constructor + const Cstr = loadModule( + { + $tagName$: elm.nodeName.toLowerCase(), + $flags$: null, + }, + null, + ) as d.ComponentConstructor; + + if (Cstr != null && Cstr.cmpMeta != null) { + // we found valid component metadata + + if ( + opts.serializeShadowRoot !== false && + !!(Cstr.cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && + (tagRequiresScoped(elm.tagName, opts.serializeShadowRoot) || + // Closed shadow DOM must use scoped CSS during SSR because: + // 1. DSD with shadowrootmode="closed" creates a shadow root that JS can't access + // 2. Stencil's hydration needs to access the shadow root to verify/update content + // The client will then attach a proper closed shadow root during hydration + !!(Cstr.cmpMeta.$flags$ & CMP_FLAGS.shadowModeClosed)) + ) { + // this component requires scoped css encapsulation during SSR + const cmpMeta = Cstr.cmpMeta; + cmpMeta.$flags$ |= CMP_FLAGS.shadowNeedsScopedCss; + + // 'cmpMeta' is a getter only, so needs redefining + Object.defineProperty(Cstr as any, 'cmpMeta', { + get: function (this: any) { + return cmpMeta; + }, + }); + } + + createdElements.add(elm); + elm.connectedCallback = patchedConnectedCallback; + + // register the host element + registerHost(elm, Cstr.cmpMeta); + + // proxy the host element with the component's metadata + proxyHostElement(elm, Cstr); + } + } + } + } + + function patchChild(elm: any) { + if (elm != null && elm.nodeType === 1) { + patchElement(elm); + const children = elm.children; + for (let i = 0, ii = children.length; i < ii; i++) { + patchChild(children[i]); + } + } + } + + function connectElement(elm: HTMLElement) { + createdElements.delete(elm); + + if (isValidComponent(elm, opts) && results.hydratedCount < opts.maxHydrateCount) { + // this is a valid component to hydrate + // and we haven't hit our max hydrated count yet + + if (!connectedElements.has(elm) && shouldHydrate(elm)) { + // we haven't connected this component yet + // and all of its ancestor elements are valid too + + // add it to our Set so we know it's already being connected + connectedElements.add(elm); + return hydrateComponent.call(elm, win, results, elm.nodeName, elm, waitingElements); + } + } + + return resolved; + } + + function waitLoop(): Promise { + const toConnect = Array.from(createdElements).filter((elm) => elm.parentElement); + if (toConnect.length > 0) { + return Promise.all(toConnect.map(connectElement)).then(waitLoop); + } + return resolved; + } + + win.document.createElement = function patchedCreateElement(tagName: string) { + const elm = orgDocumentCreateElement.call(win.document, tagName); + patchElement(elm); + return elm; + }; + + win.document.createElementNS = function patchedCreateElement( + namespaceURI: string, + tagName: string, + ) { + const elm = orgDocumentCreateElementNS.call(win.document, namespaceURI, tagName); + patchElement(elm as d.HostElement); + return elm; + } as (typeof window)['document']['createElementNS']; + + // Use the native setTimeout captured before globalThis was shadowed, + // to avoid being affected by constrainTimeouts which sets MockWindow.__maxTimeout = 0 + tmrId = $nativeSetTimeout(timeoutExceeded, opts.timeout); + + plt.$resourcesUrl$ = new URL(opts.resourcesUrl || './', win.document.baseURI).href; + + globalScripts(); + + patchChild(win.document.body); + + waitLoop().then(hydratedComplete).catch(hydratedError); + } catch (e) { + hydratedError(e); + } +} + +async function hydrateComponent( + this: HTMLElement, + win: Window & typeof globalThis, + results: d.SsrResults, + tagName: string, + elm: d.HostElement, + waitingElements: Set, +) { + tagName = tagName.toLowerCase(); + const Cstr = loadModule( + { + $tagName$: tagName, + $flags$: null, + }, + null, + ) as d.ComponentConstructor; + + if (Cstr != null) { + const cmpMeta = Cstr.cmpMeta; + + if (cmpMeta != null) { + waitingElements.add(elm); + const hostRef = getHostRef(this); + if (!hostRef) { + return; + } + addHostEventListeners(this, hostRef, cmpMeta.$listeners$); + + try { + connectedCallback(elm); + await elm.componentOnReady(); + + results.hydratedCount++; + + const ref = getHostRef(elm); + const modeName = !ref?.$modeName$ ? '$' : ref?.$modeName$; + if (!results.components.some((c) => c.tag === tagName && c.mode === modeName)) { + results.components.push({ + tag: tagName, + mode: modeName, + count: 0, + depth: -1, + }); + } + } catch (e) { + win.console.error(e); + } + waitingElements.delete(elm); + } + } +} + +function isValidComponent(elm: Element, opts: d.SsrFactoryOptions) { + if (elm != null && elm.nodeType === 1) { + // playing it safe and not using elm.tagName or elm.localName on purpose + const tagName = elm.nodeName; + if (typeof tagName === 'string' && tagName.includes('-')) { + if (opts.excludeComponents.includes(tagName.toLowerCase())) { + // this tagName we DO NOT want to hydrate + return false; + } + // all good, this is a valid component + return true; + } + } + return false; +} + +function shouldHydrate(elm: Element): boolean { + if (elm.nodeType === 9) { + return true; + } + if (NO_HYDRATE_TAGS.has(elm.nodeName)) { + return false; + } + if (elm.hasAttribute('no-prerender')) { + return false; + } + const parentNode = elm.parentNode; + if (parentNode == null) { + return true; + } + + return shouldHydrate(parentNode as Element); +} + +const NO_HYDRATE_TAGS = new Set([ + 'CODE', + 'HEAD', + 'IFRAME', + 'INPUT', + 'OBJECT', + 'OUTPUT', + 'NOSCRIPT', + 'PRE', + 'SCRIPT', + 'SELECT', + 'STYLE', + 'TEMPLATE', + 'TEXTAREA', +]); + +function renderCatchError(opts: d.SsrFactoryOptions, results: d.SsrResults, err: any) { + const diagnostic: d.Diagnostic = { + level: 'error', + type: 'build', + header: 'Hydrate Error', + messageText: '', + relFilePath: undefined, + absFilePath: undefined, + lines: [], + }; + + if (opts.url) { + try { + const u = new URL(opts.url); + if (u.pathname !== '/') { + diagnostic.header += ': ' + u.pathname; + } + } catch {} + } + + if (err != null) { + if (err.stack != null) { + diagnostic.messageText = err.stack.toString(); + } else if (err.message != null) { + diagnostic.messageText = err.message.toString(); + } else { + diagnostic.messageText = err.toString(); + } + } + + results.diagnostics.push(diagnostic); +} + +function printTag(elm: HTMLElement) { + let tag = `<${elm.nodeName.toLowerCase()}`; + if (Array.isArray(elm.attributes)) { + for (let i = 0; i < elm.attributes.length; i++) { + const attr = elm.attributes[i]; + tag += ` ${attr.name}`; + if (attr.value !== '') { + tag += `="${attr.value}"`; + } + } + } + tag += `>`; + return tag; +} + +function waitingOnElementMsg(waitingElement: HTMLElement) { + let msg = ''; + if (waitingElement) { + const lines = []; + + msg = ' - waiting on:'; + let elm = waitingElement; + while (elm && elm.nodeType !== 9 && elm.nodeName !== 'BODY') { + lines.unshift(printTag(elm)); + elm = elm.parentElement; + } + + let indent = ''; + for (const ln of lines) { + indent += ' '; + msg += `\n${indent}${ln}`; + } + } + return msg; +} + +function waitingOnElementsMsg(waitingElements: Set) { + return Array.from(waitingElements).map(waitingOnElementMsg); +} + +/** + * Determines if the tag requires a declarative shadow dom + * or a scoped / light dom during SSR. + * + * @param tagName - component tag name + * @param opts - serializeShadowRoot options + * @returns `true` when the tag requires a scoped / light dom during SSR + */ +export function tagRequiresScoped( + tagName: string, + opts: d.SsrFactoryOptions['serializeShadowRoot'], +) { + if (typeof opts === 'string') { + return opts === 'scoped'; + } + + if (typeof opts === 'boolean') { + return !opts; + } + + if (typeof opts === 'object') { + tagName = tagName.toLowerCase(); + + if ( + Array.isArray(opts['declarative-shadow-dom']) && + opts['declarative-shadow-dom'].includes(tagName) + ) { + // if the tag is in the dsd array, return dsd + return false; + } else if ( + (!Array.isArray(opts.scoped) || !opts.scoped.includes(tagName)) && + opts.default === 'declarative-shadow-dom' + ) { + // if the tag is not in the scoped array and the default is dsd, return dsd + return false; + } else { + // otherwise, return scoped + return true; + } + } + + return false; +} diff --git a/packages/core/src/server/readme.md b/packages/core/src/server/readme.md new file mode 100644 index 00000000000..b3e709e79d6 --- /dev/null +++ b/packages/core/src/server/readme.md @@ -0,0 +1,54 @@ +# server + +Server-side rendering (SSR) and hydration runtime. + +## Overview + +This directory provides the Node.js platform implementation for rendering Stencil components on the server. It's the counterpart to `client/` - both implement the `@platform` interface but for different environments. + +Previously named `hydrate/`, renamed to `server/` in v5 for clarity. + +## Directory Structure + +| Directory | Purpose | +| ----------- | -------------------------------------------------- | +| `platform/` | Server platform implementation (mirrors `client/`) | +| `runner/` | Hydrate script execution and orchestration | + +## Key Concepts + +### Hydration + +The process of: + +1. Rendering components to HTML on the server +2. Serializing component state into the HTML +3. "Hydrating" on the client - attaching event listeners and state without re-rendering + +### Platform Abstraction + +The runtime uses `@platform` imports that resolve differently: + +- Browser build → `client/` +- Server build → `server/` + +This allows the same component code to run in both environments. + +## Usage + +The server runtime is bundled into a "hydrate script" via the `dist-hydrate-script` output target: + +```ts +import { renderToString } from './dist/hydrate'; + +const result = await renderToString(''); +console.log(result.html); +``` + +## Public API + +Exposed via `@stencil/core/runtime/server`: + +- `renderToString()` - Render to HTML string +- `ssrDocument()` - Hydrate an existing document +- `serializeDocumentToString()` - Serialize DOM to string \ No newline at end of file diff --git a/src/hydrate/runner/create-window.ts b/packages/core/src/server/runner/create-window.ts similarity index 85% rename from src/hydrate/runner/create-window.ts rename to packages/core/src/server/runner/create-window.ts index b14b3d70e93..e6933a3430e 100644 --- a/src/hydrate/runner/create-window.ts +++ b/packages/core/src/server/runner/create-window.ts @@ -1,4 +1,4 @@ -import { cloneWindow, MockWindow } from '@stencil/core/mock-doc'; +import { cloneWindow, MockWindow } from '@stencil/mock-doc'; const templateWindows = new Map(); diff --git a/packages/core/src/server/runner/index.ts b/packages/core/src/server/runner/index.ts new file mode 100644 index 00000000000..8ab650760c6 --- /dev/null +++ b/packages/core/src/server/runner/index.ts @@ -0,0 +1,12 @@ +export { createWindowFromHtml } from './create-window'; +export { + hydrateDocument, + ssrDocument, + renderToString, + serializeDocumentToString, + streamToString, +} from './render'; +export { resetSsrDocData } from './window-initialize'; + +import { setTagTransformer, transformTag } from '../../runtime'; +export { setTagTransformer, transformTag }; diff --git a/src/hydrate/runner/inspect-element.ts b/packages/core/src/server/runner/inspect-element.ts similarity index 91% rename from src/hydrate/runner/inspect-element.ts rename to packages/core/src/server/runner/inspect-element.ts index d9f43406b0a..25ad6f4e2a9 100644 --- a/src/hydrate/runner/inspect-element.ts +++ b/packages/core/src/server/runner/inspect-element.ts @@ -1,7 +1,10 @@ -import type * as d from '../../declarations'; +import type * as d from '@stencil/core'; -export function inspectElement(results: d.HydrateResults, elm: Element, depth: number) { - const children = [...Array.from(elm.children), ...Array.from(elm.shadowRoot ? elm.shadowRoot.children : [])]; +export function inspectElement(results: d.SsrResults, elm: Element, depth: number) { + const children = [ + ...Array.from(elm.children), + ...Array.from(elm.shadowRoot ? elm.shadowRoot.children : []), + ]; for (let i = 0, ii = children.length; i < ii; i++) { const childElm = children[i]; @@ -86,7 +89,7 @@ export function inspectElement(results: d.HydrateResults, elm: Element, depth: n } function collectAttributes(node: Element) { - const parsedElm: d.HydrateElement = {}; + const parsedElm: d.SsrElement = {}; const attrs = node.attributes; for (let i = 0, ii = attrs.length; i < ii; i++) { const attr = attrs.item(i); diff --git a/src/hydrate/runner/patch-dom-implementation.ts b/packages/core/src/server/runner/patch-dom-implementation.ts similarity index 91% rename from src/hydrate/runner/patch-dom-implementation.ts rename to packages/core/src/server/runner/patch-dom-implementation.ts index eaa05f4ac8f..650495fb6b6 100644 --- a/src/hydrate/runner/patch-dom-implementation.ts +++ b/packages/core/src/server/runner/patch-dom-implementation.ts @@ -1,8 +1,8 @@ -import { MockWindow, patchWindow } from '@stencil/core/mock-doc'; +import { MockWindow, patchWindow } from '@stencil/mock-doc'; import type * as d from '../../declarations'; -export function patchDomImplementation(doc: any, opts: d.HydrateFactoryOptions) { +export function patchDomImplementation(doc: any, opts: d.SsrFactoryOptions) { let win: MockWindow; if (doc.defaultView != null) { @@ -40,7 +40,7 @@ export function patchDomImplementation(doc: any, opts: d.HydrateFactoryOptions) try { // @ts-expect-error Assigning the baseURI prevents JavaScript optimizers from treating this as dead code win.__stencil_baseURI = doc.baseURI; - } catch (e) { + } catch { Object.defineProperty(doc, 'baseURI', { get() { const baseElm = doc.querySelector('base[href]'); diff --git a/src/hydrate/runner/render-utils.ts b/packages/core/src/server/runner/render-utils.ts similarity index 78% rename from src/hydrate/runner/render-utils.ts rename to packages/core/src/server/runner/render-utils.ts index 40e79bcece7..672f5196472 100644 --- a/src/hydrate/runner/render-utils.ts +++ b/packages/core/src/server/runner/render-utils.ts @@ -1,7 +1,7 @@ -import type * as d from '../../declarations'; +import type * as d from '@stencil/core'; -export function normalizeHydrateOptions(inputOpts?: d.HydrateDocumentOptions) { - const outputOpts: d.HydrateFactoryOptions = Object.assign( +export function normalizeHydrateOptions(inputOpts?: d.SsrDocumentOptions) { + const outputOpts: d.SsrFactoryOptions = Object.assign( { serializeToHtml: false, destroyWindow: false, @@ -10,8 +10,8 @@ export function normalizeHydrateOptions(inputOpts?: d.HydrateDocumentOptions) { inputOpts || {}, ); - if (typeof outputOpts.clientHydrateAnnotations !== 'boolean') { - outputOpts.clientHydrateAnnotations = true; + if (typeof outputOpts.clientSsrAnnotations !== 'boolean') { + outputOpts.clientSsrAnnotations = true; } if (typeof outputOpts.constrainTimeouts !== 'boolean') { @@ -31,13 +31,17 @@ export function normalizeHydrateOptions(inputOpts?: d.HydrateDocumentOptions) { } if (Array.isArray(outputOpts.excludeComponents)) { - outputOpts.excludeComponents = outputOpts.excludeComponents.filter(filterValidTags).map(mapValidTags); + outputOpts.excludeComponents = outputOpts.excludeComponents + .filter(filterValidTags) + .map(mapValidTags); } else { outputOpts.excludeComponents = []; } if (Array.isArray(outputOpts.staticComponents)) { - outputOpts.staticComponents = outputOpts.staticComponents.filter(filterValidTags).map(mapValidTags); + outputOpts.staticComponents = outputOpts.staticComponents + .filter(filterValidTags) + .map(mapValidTags); } else { outputOpts.staticComponents = []; } @@ -53,15 +57,15 @@ function mapValidTags(tag: string) { return tag.trim().toLowerCase(); } -export function generateHydrateResults(opts: d.HydrateDocumentOptions) { +export function generateHydrateResults(opts: d.SsrDocumentOptions) { if (typeof opts.url !== 'string') { opts.url = `https://hydrate.stenciljs.com/`; } if (typeof opts.buildId !== 'string') { - opts.buildId = createHydrateBuildId(); + opts.buildId = createSsrBuildId(); } - const results: d.HydrateResults = { + const results: d.SsrResults = { buildId: opts.buildId, diagnostics: [], url: opts.url, @@ -101,7 +105,7 @@ export function generateHydrateResults(opts: d.HydrateDocumentOptions) { return results; } -export const createHydrateBuildId = () => { +export const createSsrBuildId = () => { // should be case insensitive because it could be in a URL // and shouldn't start with a number cuz we might use it as a js prop let chars = 'abcdefghijklmnopqrstuvwxyz'; @@ -117,7 +121,7 @@ export const createHydrateBuildId = () => { }; export function renderBuildDiagnostic( - results: d.HydrateResults, + results: d.SsrResults, level: 'error' | 'warn' | 'info' | 'log' | 'debug', header: string, msg: string, @@ -144,11 +148,11 @@ export function renderBuildDiagnostic( return diagnostic; } -export function renderBuildError(results: d.HydrateResults, msg?: string) { +export function renderBuildError(results: d.SsrResults, msg?: string) { return renderBuildDiagnostic(results, 'error', 'Hydrate Error', msg || ''); } -export function renderCatchError(results: d.HydrateResults, err: any) { +export function renderCatchError(results: d.SsrResults, err: any) { const diagnostic = renderBuildError(results); if (err != null) { diff --git a/packages/core/src/server/runner/render.ts b/packages/core/src/server/runner/render.ts new file mode 100644 index 00000000000..20bff369072 --- /dev/null +++ b/packages/core/src/server/runner/render.ts @@ -0,0 +1,346 @@ +import { ssrFactory } from '@stencil/core/runtime/server/ssr-factory'; +import { MockWindow, serializeNodeToHtml } from '@stencil/mock-doc'; +import { modeResolutionChain, setMode } from 'virtual:platform'; +import type { + SsrDocumentOptions, + SsrFactoryOptions, + SsrResults, + SerializeDocumentOptions, +} from '@stencil/core'; + +import { updateCanonicalLink } from '../../compiler/html/canonical-link'; +import { relocateMetaCharset } from '../../compiler/html/relocate-meta-charset'; +import { removeUnusedStyles } from '../../compiler/html/remove-unused-styles'; +import { HYDRATED_STYLE_ID } from '../../runtime'; +import { hasError } from '../../utils/message-utils'; +import { inspectElement } from './inspect-element'; +import { patchDomImplementation } from './patch-dom-implementation'; +import { + generateHydrateResults, + normalizeHydrateOptions, + renderBuildError, + renderCatchError, +} from './render-utils'; +import { initializeWindow } from './window-initialize'; + +const NOOP = () => {}; + +/** + * Renders HTML to a string, returning the full hydration results. + * This is the primary SSR function and is portable (no Node.js dependencies). + * @param html - the HTML string or document to render + * @param options - serialization options + * @returns the hydration results + */ +export function renderToString( + html: string | any, + options?: SerializeDocumentOptions, +): Promise { + const opts = normalizeHydrateOptions(options); + /** + * Makes the rendered DOM not being rendered to a string. + */ + opts.serializeToHtml = true; + /** + * Set the flag whether or not we like to render into a declarative shadow root. + */ + opts.fullDocument = typeof opts.fullDocument === 'boolean' ? opts.fullDocument : true; + /** + * Defines whether we render the shadow root as a declarative shadow root or as scoped shadow root. + */ + opts.serializeShadowRoot = + typeof opts.serializeShadowRoot === 'undefined' + ? 'declarative-shadow-dom' + : opts.serializeShadowRoot; + /** + * Make sure we wait for components to be hydrated. + */ + opts.constrainTimeouts = false; + + return ssrDocument(html, opts); +} + +/** + * Renders HTML and returns a web-standard ReadableStream. + * Works in Node 22+, Cloudflare Workers, Deno, Bun, and any WinterCG-compatible runtime. + * @param html - the HTML string or document to render + * @param options - serialization options + * @returns a ReadableStream + */ +export function streamToString( + html: string | any, + options?: SerializeDocumentOptions, +): ReadableStream { + return new ReadableStream({ + async start(controller) { + controller.enqueue((await renderToString(html, options)).html); + controller.close(); + }, + }); +} + +/** + * Server side renders a document or HTML string, returning the full render results. + * This is portable (no Node.js dependencies). + * @param doc - the document or HTML string to render + * @param options - hydration options + * @returns the render results + */ +export function ssrDocument(doc: any | string, options?: SsrDocumentOptions): Promise { + const opts = normalizeHydrateOptions(options); + /** + * Defines whether we render the shadow root as a declarative shadow root or as scoped shadow root. + */ + opts.serializeShadowRoot = + typeof opts.serializeShadowRoot === 'undefined' + ? 'declarative-shadow-dom' + : opts.serializeShadowRoot; + + let win: MockWindow | null = null; + const results = generateHydrateResults(opts); + + if (hasError(results.diagnostics)) { + return Promise.resolve(results); + } + + if (typeof doc === 'string') { + try { + opts.destroyWindow = true; + opts.destroyDocument = true; + win = new MockWindow(doc); + return render(win, opts, results).then(() => results); + } catch (e) { + if (win && win.close) { + win.close(); + } + win = null; + renderCatchError(results, e); + return Promise.resolve(results); + } + } + + if (isValidDocument(doc)) { + try { + opts.destroyDocument = false; + win = patchDomImplementation(doc, opts); + return render(win, opts, results).then(() => results); + } catch (e) { + if (win && win.close) { + win.close(); + } + win = null; + renderCatchError(results, e); + return Promise.resolve(results); + } + } + + renderBuildError( + results, + `Invalid html or document. Must be either a valid "html" string, or DOM "document".`, + ); + return Promise.resolve(results); +} + +/** + * v4 Compat + * @alias + * @deprecated Use `ssrDocument()` instead + */ +export const hydrateDocument = ssrDocument; + +async function render(win: MockWindow, opts: SsrFactoryOptions, results: SsrResults) { + if ( + 'process' in globalThis && + typeof process.on === 'function' && + !(process as any).__stencilErrors + ) { + (process as any).__stencilErrors = true; + process.on('unhandledRejection', (e) => { + console.log('unhandledRejection', e); + }); + } + + initializeWindow(win, win.document, opts, results); + const beforeHydrateFn = + typeof (opts.beforeSsr || opts.beforeHydrate) === 'function' + ? opts.beforeSsr || opts.beforeHydrate + : NOOP; + try { + await Promise.resolve(beforeHydrateFn(win.document)); + return new Promise((resolve) => { + if (Array.isArray(opts.modes)) { + /** + * Reset the mode resolution chain as we expect every `renderToString` call to render + * the components in new environment/document. + */ + modeResolutionChain.length = 0; + opts.modes.forEach((mode) => setMode(mode)); + } + return ssrFactory(win, opts, results, afterSsr, resolve); + }); + } catch (e) { + renderCatchError(results, e); + return finalizeSsr(win, win.document, opts, results); + } +} + +async function afterSsr( + win: MockWindow, + opts: SsrFactoryOptions, + results: SsrResults, + resolve: (results: SsrResults) => void, +) { + const afterSsrFn = + typeof (opts.afterSsr || opts.afterHydrate) === 'function' + ? opts.afterSsr || opts.afterHydrate + : NOOP; + try { + await Promise.resolve(afterSsrFn(win.document)); + return resolve(finalizeSsr(win, win.document, opts, results)); + } catch (e) { + renderCatchError(results, e); + return resolve(finalizeSsr(win, win.document, opts, results)); + } +} + +function finalizeSsr(win: MockWindow, doc: Document, opts: SsrFactoryOptions, results: SsrResults) { + try { + inspectElement(results, doc.documentElement, 0); + + if (opts.removeUnusedStyles !== false) { + try { + removeUnusedStyles(doc, results.diagnostics); + } catch (e) { + renderCatchError(results, e); + } + } + + if (typeof opts.title === 'string') { + try { + doc.title = opts.title; + } catch (e) { + renderCatchError(results, e); + } + } + + results.title = doc.title; + + if (opts.removeScripts) { + removeScripts(doc.documentElement); + } + + const styles = doc.querySelectorAll('head style'); + if (styles.length > 0) { + results.styles.push( + ...Array.from(styles).map((style) => ({ + href: style.getAttribute('href'), + id: style.getAttribute(HYDRATED_STYLE_ID), + content: style.textContent, + })), + ); + } + + try { + updateCanonicalLink(doc, opts.canonicalUrl); + } catch (e) { + renderCatchError(results, e); + } + + try { + relocateMetaCharset(doc); + } catch {} + + if (!hasError(results.diagnostics)) { + results.httpStatus = 200; + } + + try { + const metaStatus = doc.head.querySelector('meta[http-equiv="status"]'); + if (metaStatus != null) { + const metaStatusContent = metaStatus.getAttribute('content'); + if (metaStatusContent && metaStatusContent.length > 0) { + results.httpStatus = parseInt(metaStatusContent, 10); + } + } + } catch (e) { + renderCatchError(results, e); + } + + if (opts.clientSsrAnnotations) { + doc.documentElement.classList.add('hydrated'); + } + + if (opts.serializeToHtml) { + results.html = serializeDocumentToString(doc, opts); + } + } catch (e) { + renderCatchError(results, e); + } + + destroyWindow(win, doc, opts, results); + return results; +} + +function destroyWindow( + win: MockWindow, + doc: Document, + opts: SsrFactoryOptions, + results: SsrResults, +) { + if (!opts.destroyWindow) { + return; + } + + try { + if (!opts.destroyDocument) { + (win as any).document = null; + (doc as any).defaultView = null; + } + + if (win.close) { + win.close(); + } + } catch (e) { + renderCatchError(results, e); + } +} + +export function serializeDocumentToString(doc: Document, opts: SsrFactoryOptions) { + return serializeNodeToHtml(doc, { + approximateLineWidth: opts.approximateLineWidth, + outerHtml: false, + prettyHtml: opts.prettyHtml, + removeAttributeQuotes: opts.removeAttributeQuotes, + removeBooleanAttributeQuotes: opts.removeBooleanAttributeQuotes, + removeEmptyAttributes: opts.removeEmptyAttributes, + removeHtmlComments: opts.removeHtmlComments, + serializeShadowRoot: opts.serializeShadowRoot, + fullDocument: opts.fullDocument, + }); +} + +function isValidDocument(doc: Document) { + return ( + doc != null && + doc.nodeType === 9 && + doc.documentElement != null && + doc.documentElement.nodeType === 1 && + doc.body != null && + doc.body.nodeType === 1 + ); +} + +function removeScripts(elm: HTMLElement) { + const children = elm.children; + for (let i = children.length - 1; i >= 0; i--) { + const child = children[i]; + removeScripts(child as any); + + if ( + child.nodeName === 'SCRIPT' || + (child.nodeName === 'LINK' && child.getAttribute('rel') === 'modulepreload') + ) { + child.remove(); + } + } +} diff --git a/src/hydrate/runner/runtime-log.ts b/packages/core/src/server/runner/runtime-log.ts similarity index 78% rename from src/hydrate/runner/runtime-log.ts rename to packages/core/src/server/runner/runtime-log.ts index 83ed3ec4018..7ba79527ce7 100644 --- a/src/hydrate/runner/runtime-log.ts +++ b/packages/core/src/server/runner/runtime-log.ts @@ -1,21 +1,21 @@ -import { MockWindow } from '@stencil/core/mock-doc'; +import { MockWindow } from '@stencil/mock-doc'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; import { renderBuildDiagnostic, renderCatchError } from './render-utils'; -export function runtimeLogging(win: MockWindow, opts: d.HydrateDocumentOptions, results: d.HydrateResults) { +export function runtimeLogging(win: MockWindow, opts: d.SsrDocumentOptions, results: d.SsrResults) { try { const pathname = win.location.pathname; win.console.error = (...msgs: any[]) => { const errMsg = msgs - .reduce((errMsg, m) => { + .reduce((acc, m) => { if (m) { if (m.stack != null) { - return errMsg + ' ' + String(m.stack); + return acc + ' ' + String(m.stack); } else { if (m.message != null) { - return errMsg + ' ' + String(m.message); + return acc + ' ' + String(m.message); } } } diff --git a/packages/core/src/server/runner/ssr-factory.ts b/packages/core/src/server/runner/ssr-factory.ts new file mode 100644 index 00000000000..c5cb5605ace --- /dev/null +++ b/packages/core/src/server/runner/ssr-factory.ts @@ -0,0 +1,39 @@ +import { MockWindow } from '@stencil/mock-doc'; +import type * as d from '@stencil/core'; + +/** + * This is a stub function that will be replaced during compilation with the actual + * SSR factory function from the factory bundle. + * @param win The window to use for SSR. This should be a patched window created by `patchDomImplementation`. + * @param opts The options to use for SSR. This is used to configure which components should be hydrated, how long to wait for hydration, etc. + * @param results The results object to store the hydration results. + * @param afterSsr The callback to be called after SSR is complete. + * @param resolve The resolve function to be called when SSR is complete. + */ +export function ssrFactory( + win: MockWindow, + opts: d.SsrDocumentOptions, + results: d.SsrResults, + afterSsr: ( + win: MockWindow, + opts: DocOptions, + results: d.SsrResults, + resolve: (results: d.SsrResults) => void, + ) => void, + resolve: (results: d.SsrResults) => void, +) { + // These statements prevent the parameters from being tree-shaken + // The actual implementation is injected during the build process + void win; + void opts; + void results; + void afterSsr; + void resolve; +} + +/** + * These are stub exports that will be replaced during compilation with the actual + * tag transform functions from the factory bundle. + */ +export const setTagTransformer: d.TagTransformer = null as any; +export const transformTag: (tag: T) => T = null as any; diff --git a/packages/core/src/server/runner/window-initialize.ts b/packages/core/src/server/runner/window-initialize.ts new file mode 100644 index 00000000000..33cb123b65b --- /dev/null +++ b/packages/core/src/server/runner/window-initialize.ts @@ -0,0 +1,82 @@ +import { constrainTimeouts, type MockWindow } from '@stencil/mock-doc'; +import type * as d from '@stencil/core'; + +import { STENCIL_DOC_DATA } from '../../runtime/runtime-constants'; +import { runtimeLogging } from './runtime-log'; + +/** + * Maintain a unique `docData` object across multiple hydration runs + * to ensure that host ids remain unique. + */ +const docData: d.DocData = { + hostIds: 0, + rootLevelIds: 0, + staticComponents: new Set(), +} as d.DocData; + +/** + * Reset the docData counters. Useful for testing to ensure deterministic IDs. + */ +export function resetSsrDocData() { + docData.hostIds = 0; + docData.rootLevelIds = 0; + docData.staticComponents.clear(); +} + +export function initializeWindow( + win: MockWindow, + doc: Document, + opts: d.SsrDocumentOptions, + results: d.SsrResults, +) { + if (typeof opts.url === 'string') { + try { + win.location.href = opts.url; + } catch {} + } + + if (typeof opts.userAgent === 'string') { + try { + win.navigator.userAgent = opts.userAgent; + } catch {} + } + if (typeof opts.cookie === 'string') { + try { + doc.cookie = opts.cookie; + } catch {} + } + if (typeof opts.referrer === 'string') { + try { + (doc as any).referrer = opts.referrer; + } catch {} + } + if (typeof opts.direction === 'string') { + try { + doc.documentElement.setAttribute('dir', opts.direction); + } catch {} + } + if (typeof opts.language === 'string') { + try { + doc.documentElement.setAttribute('lang', opts.language); + } catch {} + } + if (typeof opts.buildId === 'string') { + try { + doc.documentElement.setAttribute('data-stencil-build', opts.buildId); + } catch {} + } + + try { + win.customElements = null; + } catch {} + + if (opts.constrainTimeouts) { + constrainTimeouts(win); + } + + runtimeLogging(win, opts, results); + + (doc as d.StencilDocument)[STENCIL_DOC_DATA] = docData; + + return win; +} diff --git a/src/sys/node/test/test-worker-main.ts b/packages/core/src/sys/node/_test_/fixtures/test-worker-main.ts similarity index 77% rename from src/sys/node/test/test-worker-main.ts rename to packages/core/src/sys/node/_test_/fixtures/test-worker-main.ts index 0e5cd6aff1f..c32a3128d06 100644 --- a/src/sys/node/test/test-worker-main.ts +++ b/packages/core/src/sys/node/_test_/fixtures/test-worker-main.ts @@ -1,4 +1,4 @@ -import { NodeWorkerMain } from '../node-worker-main'; +import { NodeWorkerMain } from '../../node-worker-main'; export class TestWorkerMain extends NodeWorkerMain { constructor(workerId: number) { diff --git a/packages/core/src/sys/node/_test_/node-lazy-require.spec.ts b/packages/core/src/sys/node/_test_/node-lazy-require.spec.ts new file mode 100644 index 00000000000..bf8ba97f405 --- /dev/null +++ b/packages/core/src/sys/node/_test_/node-lazy-require.spec.ts @@ -0,0 +1,94 @@ +/// + +import fs from 'node:fs'; +import { expect, describe, it } from '@stencil/vitest'; + +import { buildError } from '../../../utils'; +import { LazyDependencies, NodeLazyRequire } from '../node-lazy-require'; +import { NodeResolveModule } from '../node-resolve-module'; + +vi.mock('node:fs', () => ({ + default: { + readFileSync: vi.fn(), + }, +})); + +vi.mock('../node-resolve-module', () => ({ + NodeResolveModule: class { + resolveModule = vi.fn().mockReturnValue('/fake/path/to/jest/package.json'); + }, +})); + +const mockPackageJson = (version: string) => JSON.stringify({ version }); + +describe('node-lazy-require', () => { + describe('NodeLazyRequire', () => { + describe('ensure', () => { + const jestTestRange = (maxVersion = '38.0.1'): LazyDependencies => ({ + jest: { + minVersion: '2.0.7', + recommendedVersion: '36.0.1', + maxVersion, + }, + }); + + function setup(versionRange: LazyDependencies) { + const resolveModule = new NodeResolveModule(); + const nodeLazyRequire = new NodeLazyRequire(resolveModule, versionRange); + return nodeLazyRequire; + } + + it.each(['2.0.7', '10.10.10', '38.0.1', '38.0.2', '38.5.17'])( + 'should not error if installed package has a suitable major version (%p)', + async (testVersion) => { + const nodeLazyRequire = setup(jestTestRange()); + vi.mocked(fs.readFileSync).mockReturnValue(mockPackageJson(testVersion)); + const diagnostics = await nodeLazyRequire.ensure('.', ['jest']); + expect(diagnostics.length).toBe(0); + }, + ); + + it.each(['2.0.7', '10.10.10', '36.0.1', '38.0.2', '38.5.17'])( + 'should never error with versions above minVersion if there is no maxVersion supplied (%p)', + async (testVersion) => { + const nodeLazyRequire = setup(jestTestRange(undefined)); + vi.mocked(fs.readFileSync).mockReturnValue(mockPackageJson(testVersion)); + const diagnostics = await nodeLazyRequire.ensure('.', ['jest']); + expect(diagnostics.length).toBe(0); + }, + ); + + it.each(['38', undefined])( + 'should error w/ installed version too low and maxVersion=%p', + async (maxVersion) => { + const range = jestTestRange(maxVersion); + const nodeLazyRequire = setup(range); + vi.mocked(fs.readFileSync).mockReturnValue(mockPackageJson('1.1.1')); + const [error] = await nodeLazyRequire.ensure('.', ['jest']); + expect(error).toEqual({ + ...buildError([]), + header: + 'Please install supported versions of dev dependencies with either npm or yarn.', + messageText: `npm install --save-dev jest@${range.jest.recommendedVersion}`, + }); + }, + ); + + it.each(['100.1.1', '38.0.1-alpha.0'])( + 'should error if the installed version of a package is too high (%p)', + async (version) => { + const range = jestTestRange(); + const nodeLazyRequire = setup(range); + vi.mocked(fs.readFileSync).mockReturnValue(mockPackageJson(version)); + const [error] = await nodeLazyRequire.ensure('.', ['jest']); + expect(error).toEqual({ + ...buildError([]), + header: + 'Please install supported versions of dev dependencies with either npm or yarn.', + messageText: `npm install --save-dev jest@${range.jest.recommendedVersion}`, + }); + }, + ); + }); + }); +}); diff --git a/src/sys/node/test/worker-manager.spec.ts b/packages/core/src/sys/node/_test_/worker-manager.spec.ts similarity index 94% rename from src/sys/node/test/worker-manager.spec.ts rename to packages/core/src/sys/node/_test_/worker-manager.spec.ts index e8da48fb9ab..63a8bdf4d9f 100644 --- a/src/sys/node/test/worker-manager.spec.ts +++ b/packages/core/src/sys/node/_test_/worker-manager.spec.ts @@ -1,6 +1,8 @@ -import type * as d from '../../../declarations'; +import { expect, describe, it, beforeEach } from '@stencil/vitest'; +import type * as d from '@stencil/core'; + import { getNextWorker } from '../node-worker-controller'; -import { TestWorkerMain } from './test-worker-main'; +import { TestWorkerMain } from './fixtures/test-worker-main'; const incr = (function* () { let i = 1; diff --git a/src/sys/node/index.ts b/packages/core/src/sys/node/index.ts similarity index 100% rename from src/sys/node/index.ts rename to packages/core/src/sys/node/index.ts diff --git a/src/sys/node/logger/test/terminal-logger.spec.ts b/packages/core/src/sys/node/logger/_test_/terminal-logger.spec.ts similarity index 84% rename from src/sys/node/logger/test/terminal-logger.spec.ts rename to packages/core/src/sys/node/logger/_test_/terminal-logger.spec.ts index 0205088ef30..014b8c6f63f 100644 --- a/src/sys/node/logger/test/terminal-logger.spec.ts +++ b/packages/core/src/sys/node/logger/_test_/terminal-logger.spec.ts @@ -1,10 +1,13 @@ -import { bgRed, blue, bold, cyan, dim, gray, green, magenta, red, yellow } from 'ansi-colors'; +import { expect, describe, it, vi } from '@stencil/vitest'; +import chalk from 'chalk'; -import { LOG_LEVELS, LogLevel } from '../../../../declarations'; +import { LOG_LEVELS, type LogLevel } from '../../../../declarations/stencil-public-compiler'; import { setupConsoleMocker } from '../../../../testing/testing-utils'; import { createNodeLoggerSys } from '../index'; import { createTerminalLogger, shouldLog } from '../terminal-logger'; +const { bgRed, blue, bold, cyan, dim, gray, green, magenta, red, yellow } = chalk; + describe('terminal-logger', () => { describe('shouldLog helper', () => { it.each(LOG_LEVELS)("should log errors at level '%s'", (currentLevel: LogLevel) => { @@ -47,14 +50,14 @@ describe('terminal-logger', () => { const loggerSys = createNodeLoggerSys(); - loggerSys.memoryUsage = jest.fn().mockReturnValue(10_000_000); + loggerSys.memoryUsage = vi.fn().mockReturnValue(10_000_000); - const writeLogsMock = jest.fn(); + const writeLogsMock = vi.fn(); loggerSys.writeLogs = writeLogsMock; const logger = createTerminalLogger(loggerSys); - jest.useFakeTimers().setSystemTime(new Date(2022, 6, 3, 9, 32, 32, 32)); + vi.useFakeTimers().setSystemTime(new Date(2022, 6, 3, 9, 32, 32, 32)); return { logger, logMock, warnMock, errorMock, writeLogsMock }; } @@ -63,7 +66,9 @@ describe('terminal-logger', () => { const { logger, logMock } = setup(); logger.setLevel('debug'); logger.debug('my debug message'); - expect(logMock).toHaveBeenCalledWith(`${cyan('[32:32.0]')} my debug message ${dim(' MEM: 10.0MB')}`); + expect(logMock).toHaveBeenCalledWith( + `${cyan('[32:32.0]')} my debug message ${dim('MEM: 10.0MB')}`, + ); }); it("supports 'info' level", () => { @@ -85,7 +90,7 @@ describe('terminal-logger', () => { }); describe('color support', () => { - it('re-packages some ansi-colors functions', () => { + it('re-packages some chalk color functions', () => { const { logger } = setup(); expect(logger.bgRed('test message')).toBe(bgRed('test message')); expect(logger.blue('test message')).toBe(blue('test message')); @@ -113,7 +118,7 @@ describe('terminal-logger', () => { expect(logger.red('test message')).toBe('test message'); expect(logger.yellow('test message')).toBe('test message'); // This has to be re-enabled because this actually toggles - // a boolean declared inside of the ansi-colors module + // chalk.level which affects all subsequent color calls logger.enableColors(true); }); }); @@ -143,11 +148,17 @@ describe('terminal-logger', () => { it('has basic support for timespans', function () { const { logger, logMock } = setup(); const timespan = logger.createTimeSpan('start the timespan'); - jest.advanceTimersByTime(10_000); + vi.advanceTimersByTime(10_000); timespan.finish('finish the timespan'); - expect(logMock).toHaveBeenNthCalledWith(1, `${dim('[32:32.0]')} start the timespan ${dim('...')}`); - expect(logMock).toHaveBeenNthCalledWith(2, `${dim('[32:42.0]')} finish the timespan ${dim('in 10.00 s')}`); + expect(logMock).toHaveBeenNthCalledWith( + 1, + `${dim('[32:32.0]')} start the timespan ${dim('...')}`, + ); + expect(logMock).toHaveBeenNthCalledWith( + 2, + `${dim('[32:42.0]')} finish the timespan ${dim('in 10.00 s')}`, + ); }); describe('debug timespan', function () { @@ -155,7 +166,7 @@ describe('terminal-logger', () => { const { logger, logMock } = setup(); logger.setLevel('debug'); const timespan = logger.createTimeSpan('start the timespan', true); - jest.advanceTimersByTime(10_000); + vi.advanceTimersByTime(10_000); timespan.finish('finish the timespan'); expect(logMock).toHaveBeenNthCalledWith( @@ -173,7 +184,7 @@ describe('terminal-logger', () => { logger.setLogFilePath!('testfile.txt'); logger.setLevel('debug'); const timespan = logger.createTimeSpan('start the timespan', true); - jest.advanceTimersByTime(10_000); + vi.advanceTimersByTime(10_000); timespan.finish('finish the timespan'); logger.writeLogs!(false); @@ -191,7 +202,7 @@ describe('terminal-logger', () => { const { logger, logMock } = setup(); logger.setLevel(level); const timespan = logger.createTimeSpan('start the timespan', true); - jest.advanceTimersByTime(10_000); + vi.advanceTimersByTime(10_000); timespan.finish('finish the timespan'); expect(logMock).not.toHaveBeenCalled(); }, @@ -201,11 +212,17 @@ describe('terminal-logger', () => { it('reports the number of milliseconds if timespan takes under a second', () => { const { logger, logMock } = setup(); const timespan = logger.createTimeSpan('start the timespan'); - jest.advanceTimersByTime(10); + vi.advanceTimersByTime(10); timespan.finish('finish the timespan'); - expect(logMock).toHaveBeenNthCalledWith(1, `${dim('[32:32.0]')} start the timespan ${dim('...')}`); - expect(logMock).toHaveBeenNthCalledWith(2, `${dim('[32:32.0]')} finish the timespan ${dim('in 10 ms')}`); + expect(logMock).toHaveBeenNthCalledWith( + 1, + `${dim('[32:32.0]')} start the timespan ${dim('...')}`, + ); + expect(logMock).toHaveBeenNthCalledWith( + 2, + `${dim('[32:32.0]')} finish the timespan ${dim('in 10 ms')}`, + ); }); it("doesn't report an exact time if it's less than 1ms", function () { @@ -213,7 +230,10 @@ describe('terminal-logger', () => { const timespan = logger.createTimeSpan('start the timespan'); timespan.finish('finish the timespan'); - expect(logMock).toHaveBeenNthCalledWith(1, `${dim('[32:32.0]')} start the timespan ${dim('...')}`); + expect(logMock).toHaveBeenNthCalledWith( + 1, + `${dim('[32:32.0]')} start the timespan ${dim('...')}`, + ); expect(logMock).toHaveBeenNthCalledWith( 2, `${dim('[32:32.0]')} finish the timespan ${dim('in less than 1 ms')}`, @@ -224,7 +244,7 @@ describe('terminal-logger', () => { const { logger, writeLogsMock } = setup(); logger.setLogFilePath!('testfile.txt'); const timespan = logger.createTimeSpan('start the timespan'); - jest.advanceTimersByTime(10_000); + vi.advanceTimersByTime(10_000); timespan.finish('finish the timespan'); logger.writeLogs!(false); diff --git a/packages/core/src/sys/node/logger/index.ts b/packages/core/src/sys/node/logger/index.ts new file mode 100644 index 00000000000..4dfdec7b8f9 --- /dev/null +++ b/packages/core/src/sys/node/logger/index.ts @@ -0,0 +1,100 @@ +import fs from 'node:fs'; +import path from 'path'; +import type { Logger } from '@stencil/core'; + +import { createTerminalLogger, TerminalLoggerSys } from './terminal-logger'; + +/** + * Create a logger to run in a Node environment + * + * @returns the created logger + */ +export const createNodeLogger = (): Logger => { + const loggerSys = createNodeLoggerSys(); + const logger = createTerminalLogger(loggerSys); + return logger; +}; + +/** + * Create a logger sys object for use in a Node.js environment + * + * The `TerminalLoggerSys` interface basically abstracts away some + * environment-specific details so that the terminal logger can deal with + * things in a (potentially) platform-agnostic way. + * + * @returns a configured logger sys object + */ +export function createNodeLoggerSys(): TerminalLoggerSys { + const cwd = () => process.cwd(); + + const emoji = (e: string) => (process.platform !== 'win32' ? e : ''); + + /** + * Get the number of columns for the terminal to use when printing + * @returns the number of columns to use + */ + const getColumns = () => { + const min_columns = 60; + const max_columns = 120; + const defaultWidth = 80; + + const terminalWidth = process?.stdout?.columns ?? defaultWidth; + return Math.max(Math.min(terminalWidth, max_columns), min_columns); + }; + + const memoryUsage = () => process.memoryUsage().rss; + + const relativePath = (from: string, to: string) => path.relative(from, to); + + const writeLogs = (logFilePath: string, log: string, append: boolean) => { + if (append) { + try { + fs.accessSync(logFilePath); + } catch { + append = false; + } + } + + if (append) { + fs.appendFileSync(logFilePath, log); + } else { + fs.writeFileSync(logFilePath, log); + } + }; + + const createLineUpdater = async () => { + const readline = await import('readline'); + let promise = Promise.resolve(); + const update = (text: string) => { + text = text.substring(0, process.stdout.columns - 5) + '\x1b[0m'; + return (promise = promise.then(() => { + return new Promise((resolve) => { + readline.clearLine(process.stdout, 0); + readline.cursorTo(process.stdout, 0, undefined); + process.stdout.write(text, resolve); + }); + })); + }; + + const stop = () => { + return update('\x1B[?25h'); + }; + + // hide cursor + process.stdout.write('\x1B[?25l'); + return { + update, + stop, + }; + }; + + return { + cwd, + emoji, + getColumns, + memoryUsage, + relativePath, + writeLogs, + createLineUpdater, + }; +} diff --git a/src/sys/node/logger/terminal-logger.ts b/packages/core/src/sys/node/logger/terminal-logger.ts similarity index 91% rename from src/sys/node/logger/terminal-logger.ts rename to packages/core/src/sys/node/logger/terminal-logger.ts index 1fbdce3483c..a37eaeeb2ea 100644 --- a/src/sys/node/logger/terminal-logger.ts +++ b/packages/core/src/sys/node/logger/terminal-logger.ts @@ -1,21 +1,46 @@ -import ansiColor, { bgRed, blue, bold, cyan, dim, gray, green, magenta, red, yellow } from 'ansi-colors'; - -import { +import chalk, { type ChalkInstance } from 'chalk'; +import type { Diagnostic, - LOG_LEVELS, Logger, LoggerLineUpdater, LoggerTimeSpan, LogLevel, PrintLine, -} from '../../../declarations'; +} from '@stencil/core'; + +import { LOG_LEVELS } from '../../../declarations/stencil-public-compiler'; + +// Re-export chalk color functions for convenience +const { bgRed, blue, bold, cyan, dim, gray, green, magenta, red, yellow } = chalk; /** - * A type to capture the range of functions exported by the ansi-colors module - * Unfortunately they don't make a type like this available directly, so we have - * to do a little DIY. + * A type to capture chalk style names */ -type AnsiColorVariant = keyof typeof ansiColor.styles; +type ChalkColorVariant = + | 'yellow' + | 'red' + | 'magenta' + | 'green' + | 'gray' + | 'cyan' + | 'blue' + | 'bold' + | 'dim'; + +/** + * Map of color names to chalk functions for dynamic access + */ +const chalkColors: Record = { + yellow: chalk.yellow, + red: chalk.red, + magenta: chalk.magenta, + green: chalk.green, + gray: chalk.gray, + cyan: chalk.cyan, + blue: chalk.blue, + bold: chalk.bold, + dim: chalk.dim, +}; /** * Create a logger for outputting information to a terminal environment @@ -110,10 +135,10 @@ export const createTerminalLogger = (loggerSys: TerminalLoggerSys): Logger => { } }; - const timespanStart = (startMsg: string, debug: boolean, appendTo?: string[]) => { + const timespanStart = (startMsg: string, isDebug: boolean, appendTo?: string[]) => { const msg = [`${startMsg} ${dim('...')}`]; - if (debug) { + if (isDebug) { if (shouldLog(currentLogLevel, 'debug')) { formatMemoryUsage(msg); const lines = wordWrap(msg, loggerSys.getColumns()); @@ -147,16 +172,16 @@ export const createTerminalLogger = (loggerSys: TerminalLoggerSys): Logger => { const timespanFinish = ( finishMsg: string, timeSuffix: string, - colorName: AnsiColorVariant, + colorName: ChalkColorVariant, textBold: boolean, newLineSuffix: boolean, - debug: boolean, + isDebug: boolean, appendTo?: string[], ) => { let msg = finishMsg; - if (colorName) { - msg = ansiColor[colorName](finishMsg); + if (colorName && chalkColors[colorName]) { + msg = chalkColors[colorName](finishMsg); } if (textBold) { msg = bold(msg); @@ -164,7 +189,7 @@ export const createTerminalLogger = (loggerSys: TerminalLoggerSys): Logger => { msg += ' ' + dim(timeSuffix); - if (debug) { + if (isDebug) { if (shouldLog(currentLogLevel, 'debug')) { const m = [msg]; formatMemoryUsage(m); @@ -190,12 +215,12 @@ export const createTerminalLogger = (loggerSys: TerminalLoggerSys): Logger => { } }; - const createTimeSpan = (startMsg: string, debug = false, appendTo?: string[]) => { + const createTimeSpan = (startMsg: string, isDebug = false, appendTo?: string[]) => { const start = Date.now(); const duration = () => Date.now() - start; const timeSpan: LoggerTimeSpan = { duration, - finish: (finishMsg, colorName: AnsiColorVariant, textBold, newLineSuffix) => { + finish: (finishMsg, colorName: ChalkColorVariant, textBold, newLineSuffix) => { const dur = duration(); let time: string; @@ -210,12 +235,12 @@ export const createTerminalLogger = (loggerSys: TerminalLoggerSys): Logger => { } } - timespanFinish(finishMsg, time, colorName, !!textBold, !!newLineSuffix, debug, appendTo); + timespanFinish(finishMsg, time, colorName, !!textBold, !!newLineSuffix, isDebug, appendTo); return dur; }, }; - timespanStart(startMsg, debug, appendTo); + timespanStart(startMsg, isDebug, appendTo); return timeSpan; }; @@ -248,7 +273,7 @@ export const createTerminalLogger = (loggerSys: TerminalLoggerSys): Logger => { try { queueWriteLog('F', ['--------------------------------------']); loggerSys.writeLogs(logFilePath, writeLogQueue.join('\n'), append); - } catch (e) {} + } catch {} } writeLogQueue.length = 0; @@ -256,10 +281,10 @@ export const createTerminalLogger = (loggerSys: TerminalLoggerSys): Logger => { /** * Callback to enable / disable colored output in logs - * @param useColors the new value for the `enabled` toggle on ansi-color + * @param useColors whether to enable colors (chalk.level = 3) or disable (chalk.level = 0) */ const enableColors = (useColors: boolean) => { - ansiColor.enabled = useColors; + chalk.level = useColors ? 3 : 0; }; /** @@ -400,7 +425,11 @@ export const createTerminalLogger = (loggerSys: TerminalLoggerSys): Logger => { * @param errorLength the length of the error, how many characters should be highlighted * @returns the highlighted error */ - const highlightError = (errorLine: string, errorCharStart: number, errorLength: number = 0): string => { + const highlightError = ( + errorLine: string, + errorCharStart: number, + errorLength: number = 0, + ): string => { let rightSideChars = errorLine.length - errorCharStart + errorLength - 1; while (errorLine.length + INDENT.length > loggerSys.getColumns()) { if (errorCharStart > errorLine.length - errorCharStart + errorLength && errorCharStart > 5) { @@ -566,7 +595,7 @@ const clampTwoDigits = (n: number): string => ('0' + n.toString()).slice(-2); * @param columns the maximum number of columns to occupy per line * @returns the wrapped message */ -export const wordWrap = (msg: any[], columns: number): string[] => { +const wordWrap = (msg: any[], columns: number): string[] => { const lines: string[] = []; const words: any[] = []; @@ -631,8 +660,8 @@ export const wordWrap = (msg: any[], columns: number): string[] => { lines.push(line); } - return lines.map((line) => { - return (line as any).trimRight(); + return lines.map((l) => { + return (l as any).trimRight(); }); }; @@ -651,10 +680,10 @@ const removeLeadingWhitespace = (orgLines: PrintLine[]): ReadonlyArray { + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + await copyFile(src, dest); + return; + } catch (err: any) { + const isRetryable = ['EBUSY', 'EPERM', 'EACCES'].includes(err.code); + if (!isRetryable || attempt === maxRetries) { + throw err; + } + // Exponential backoff: 100ms, 200ms, 400ms + await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt))); + } + } +} + +export async function nodeCopyTasks(copyTasks: Required[], srcDir: string) { + const results: d.CopyResults = { + diagnostics: [], + dirPaths: [], + filePaths: [], + }; + + try { + copyTasks = flatOne(await Promise.all(copyTasks.map((task) => processGlobs(task, srcDir)))); + + const allCopyTasks: d.CopyTask[] = []; + + // figure out all the file copy tasks we'll have + // by digging down through any directory copy tasks + while (copyTasks.length > 0) { + const tasks = copyTasks.splice(0, 100); + + await Promise.all(tasks.map((copyTask) => processCopyTask(results, allCopyTasks, copyTask))); + } + + // figure out which directories we'll need to make first + const mkDirs = ensureDirs(allCopyTasks); + + try { + await Promise.all(mkDirs.map((dir) => mkdir(dir, { recursive: true }))); + } catch {} + + while (allCopyTasks.length > 0) { + const tasks = allCopyTasks.splice(0, 100); + + await Promise.all(tasks.map((copyTask) => copyFileWithRetry(copyTask.src, copyTask.dest))); + } + } catch (e: any) { + catchError(results.diagnostics, e); + } + + return results; +} + +async function processGlobs( + copyTask: Required, + srcDir: string, +): Promise[]> { + return isGlob(copyTask.src) + ? await processGlobTask(copyTask, srcDir) + : [ + { + src: getSrcAbsPath(srcDir, copyTask.src), + dest: copyTask.keepDirStructure ? path.join(copyTask.dest, copyTask.src) : copyTask.dest, + warn: copyTask.warn, + ignore: copyTask.ignore, + keepDirStructure: copyTask.keepDirStructure, + }, + ]; +} + +function getSrcAbsPath(srcDir: string, src: string) { + if (path.isAbsolute(src)) { + return src; + } + return path.join(srcDir, src); +} + +async function processGlobTask( + copyTask: Required, + srcDir: string, +): Promise[]> { + const files = await asyncGlob(copyTask.src, { + cwd: srcDir, + nodir: true, + ignore: copyTask.ignore, + }); + return files.map((globRelPath) => createGlobCopyTask(copyTask, srcDir, globRelPath)); +} + +function createGlobCopyTask( + copyTask: Required, + srcDir: string, + globRelPath: string, +): Required { + const dest = path.join( + copyTask.dest, + copyTask.keepDirStructure ? globRelPath : path.basename(globRelPath), + ); + return { + src: path.join(srcDir, globRelPath), + dest, + ignore: copyTask.ignore, + warn: copyTask.warn, + keepDirStructure: copyTask.keepDirStructure, + }; +} + +async function processCopyTask( + results: d.CopyResults, + allCopyTasks: d.CopyTask[], + copyTask: d.CopyTask, +) { + try { + copyTask.src = normalizePath(copyTask.src); + copyTask.dest = normalizePath(copyTask.dest); + + // get the stats for this src to see if it's a directory or not + const stats = await stat(copyTask.src); + if (stats.isDirectory()) { + // still a directory, keep digging down + if (!results.dirPaths.includes(copyTask.dest)) { + results.dirPaths.push(copyTask.dest); + } + + await processCopyTaskDirectory(results, allCopyTasks, copyTask); + } else if (!shouldIgnore(copyTask)) { + // this is a file we should copy + if (!results.filePaths.includes(copyTask.dest)) { + results.filePaths.push(copyTask.dest); + } + + allCopyTasks.push(copyTask); + } + } catch (e) { + if (copyTask.warn !== false) { + const err = buildError(results.diagnostics); + if (e instanceof Error) { + err.messageText = e.message; + } + } + } +} + +async function processCopyTaskDirectory( + results: d.CopyResults, + allCopyTasks: d.CopyTask[], + copyTask: d.CopyTask, +) { + try { + const dirItems = await readdir(copyTask.src); + + await Promise.all( + dirItems.map(async (dirItem) => { + const subCopyTask: d.CopyTask = { + src: path.join(copyTask.src, dirItem), + dest: path.join(copyTask.dest, dirItem), + warn: copyTask.warn, + }; + + await processCopyTask(results, allCopyTasks, subCopyTask); + }), + ); + } catch (e: any) { + catchError(results.diagnostics, e); + } +} + +function ensureDirs(copyTasks: d.CopyTask[]) { + const mkDirs: string[] = []; + + copyTasks.forEach((copyTask) => { + addMkDir(mkDirs, path.dirname(copyTask.dest)); + }); + + mkDirs.sort((a, b) => { + const partsA = a.split('/').length; + const partsB = b.split('/').length; + + if (partsA < partsB) return -1; + if (partsA > partsB) return 1; + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + + return mkDirs; +} + +function addMkDir(mkDirs: string[], destDir: string) { + destDir = normalizePath(destDir); + + if (destDir === ROOT_DIR || destDir + '/' === ROOT_DIR || destDir === '') { + return; + } + + if (!mkDirs.includes(destDir)) { + mkDirs.push(destDir); + } +} + +const ROOT_DIR = normalizePath(path.resolve('/')); + +function shouldIgnore({ src, ignore = [] }: d.CopyTask) { + const filePath = src.trim().toLowerCase(); + return ignore.some((ignoreFile) => filePath.endsWith(ignoreFile)); +} + +export function asyncGlob( + pattern: string, + opts: { cwd?: string; nodir?: boolean; ignore?: string[]; [key: string]: any } = {}, +): Promise { + const { nodir, ...rest } = opts; + return glob(pattern, { ...rest, onlyFiles: nodir ?? true }); +} diff --git a/packages/core/src/sys/node/node-fs-promisify.ts b/packages/core/src/sys/node/node-fs-promisify.ts new file mode 100644 index 00000000000..cbdb73ad1a4 --- /dev/null +++ b/packages/core/src/sys/node/node-fs-promisify.ts @@ -0,0 +1 @@ +export { copyFile, mkdir, readdir, stat } from 'node:fs/promises'; diff --git a/src/sys/node/node-lazy-require.ts b/packages/core/src/sys/node/node-lazy-require.ts similarity index 89% rename from src/sys/node/node-lazy-require.ts rename to packages/core/src/sys/node/node-lazy-require.ts index 5ffaf23f5e2..3818c07d8ce 100644 --- a/src/sys/node/node-lazy-require.ts +++ b/packages/core/src/sys/node/node-lazy-require.ts @@ -1,11 +1,11 @@ -import { buildError } from '@utils'; -import fs from 'graceful-fs'; +import fs from 'node:fs'; import path from 'path'; -import semverLte from 'semver/functions/lte'; -import major from 'semver/functions/major'; -import satisfies from 'semver/functions/satisfies'; +import semverLte from 'semver/functions/lte.js'; +import major from 'semver/functions/major.js'; +import satisfies from 'semver/functions/satisfies.js'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; +import { buildError } from '../../utils'; import { NodeResolveModule } from './node-resolve-module'; /** @@ -68,11 +68,14 @@ export class NodeLazyRequire implements d.LazyRequire { ensureModuleIds.forEach((ensureModuleId) => { if (!this.ensured.has(ensureModuleId)) { - const { minVersion, recommendedVersion, maxVersion } = this.lazyDependencies[ensureModuleId]; + const { minVersion, recommendedVersion, maxVersion } = + this.lazyDependencies[ensureModuleId]; try { const pkgJsonPath = this.nodeResolveModule.resolveModule(fromDir, ensureModuleId); - const installedPkgJson: d.PackageJsonData = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); + const installedPkgJson: d.PackageJsonData = JSON.parse( + fs.readFileSync(pkgJsonPath, 'utf8'), + ); const installedVersionIsGood = maxVersion ? // if maxVersion, check that `minVersion <= installedVersion <= maxVersion` @@ -84,7 +87,7 @@ export class NodeLazyRequire implements d.LazyRequire { this.ensured.add(ensureModuleId); return; } - } catch (e) {} + } catch {} // if we get here we didn't get to the `return` above, so either 1) there was some error // reading the package.json or 2) the version wasn't in our specified version range. problemDeps.push(`${ensureModuleId}@${recommendedVersion}`); diff --git a/src/sys/node/node-resolve-module.ts b/packages/core/src/sys/node/node-resolve-module.ts similarity index 90% rename from src/sys/node/node-resolve-module.ts rename to packages/core/src/sys/node/node-resolve-module.ts index 1f0e6045522..559fc573626 100644 --- a/src/sys/node/node-resolve-module.ts +++ b/packages/core/src/sys/node/node-resolve-module.ts @@ -1,8 +1,8 @@ -import { normalizePath } from '@utils'; -import fs from 'graceful-fs'; +import fs from 'node:fs'; import path from 'path'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; +import { normalizePath } from '../../utils'; export class NodeResolveModule { private resolveModuleCache = new Map(); @@ -65,7 +65,13 @@ export class NodeResolveModule { while (dir !== root) { dir = normalizePath(path.dirname(dir)); - typesPackageJsonFilePath = path.join(dir, 'node_modules', moduleSplt[0], moduleSplt[1], 'package.json'); + typesPackageJsonFilePath = path.join( + dir, + 'node_modules', + moduleSplt[0], + moduleSplt[1], + 'package.json', + ); if (!fs.existsSync(typesPackageJsonFilePath)) { continue; diff --git a/src/sys/node/node-setup-process.ts b/packages/core/src/sys/node/node-setup-process.ts similarity index 83% rename from src/sys/node/node-setup-process.ts rename to packages/core/src/sys/node/node-setup-process.ts index a09c4ee9caa..e4ea720da7e 100644 --- a/src/sys/node/node-setup-process.ts +++ b/packages/core/src/sys/node/node-setup-process.ts @@ -1,6 +1,6 @@ -import { shouldIgnoreError } from '@utils'; +import type { Logger } from '@stencil/core'; -import type { Logger } from '../../declarations'; +import { shouldIgnoreError } from '../../utils'; export function setupNodeProcess(c: { process: any; logger: Logger }) { c.process.on(`unhandledRejection`, (e: any) => { diff --git a/src/sys/node/node-stencil-version-checker.ts b/packages/core/src/sys/node/node-stencil-version-checker.ts similarity index 93% rename from src/sys/node/node-stencil-version-checker.ts rename to packages/core/src/sys/node/node-stencil-version-checker.ts index 155440d7644..bcc6905a51c 100644 --- a/src/sys/node/node-stencil-version-checker.ts +++ b/packages/core/src/sys/node/node-stencil-version-checker.ts @@ -1,10 +1,10 @@ -import { isString, noop } from '@utils'; -import fs from 'graceful-fs'; +import fs from 'node:fs'; import { tmpdir } from 'os'; import path from 'path'; -import semverLt from 'semver/functions/lt'; +import semverLt from 'semver/functions/lt.js'; +import type { Logger, PackageJsonData } from '@stencil/core'; -import type { Logger, PackageJsonData } from '../../declarations'; +import { isString, noop } from '../../utils'; const REGISTRY_URL = `https://registry.npmjs.org/@stencil/core`; const CHECK_INTERVAL = 1000 * 60 * 60 * 24 * 7; @@ -98,7 +98,7 @@ function getLastCheck() { if (!err && isString(data)) { try { resolve(JSON.parse(data)); - } catch (e) {} + } catch {} } resolve(null); }); @@ -127,7 +127,10 @@ function printUpdateMessage(logger: Logger, currentVersion: string, latestVersio CHANGELOG, ]; - const lineLength = msg.reduce((longest, line) => (line.length > longest ? line.length : longest), 0); + const lineLength = msg.reduce( + (longest, line) => (line.length > longest ? line.length : longest), + 0, + ); const o: string[] = []; diff --git a/packages/core/src/sys/node/node-sys.ts b/packages/core/src/sys/node/node-sys.ts new file mode 100644 index 00000000000..7a493e85cd9 --- /dev/null +++ b/packages/core/src/sys/node/node-sys.ts @@ -0,0 +1,667 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import { cpus, freemem, platform, release, tmpdir, totalmem } from 'node:os'; +import * as os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import * as parcelWatcher from '@parcel/watcher'; +import type { + CompilerFileWatcher, + CompilerFileWatcherCallback, + CompilerSystem, + CompilerSystemCreateDirectoryResults, + CompilerSystemRealpathResults, + CompilerSystemRemoveFileResults, + CompilerSystemWriteFileResults, + Logger, +} from '@stencil/core'; + +import { buildEvents } from '../../compiler/events'; +import { isFunction, normalizePath } from '../../utils'; +import { asyncGlob, nodeCopyTasks } from './node-copy-tasks'; +import { NodeLazyRequire } from './node-lazy-require'; +import { NodeResolveModule } from './node-resolve-module'; +import { checkVersion } from './node-stencil-version-checker'; +import { NodeWorkerController } from './node-worker-controller'; + +const __dirname = import.meta.dirname; + +/** + * Create a node.js-specific {@link CompilerSystem} to be used when Stencil is + * run from the CLI or via the public API in a node context. + * + * This takes an optional param supplying a `process` object to be used. + * + * @param c an optional object wrapping `process` and `logger` objects + * @returns a node.js `CompilerSystem` object + */ +export function createNodeSys(c: { process?: any; logger?: Logger } = {}): CompilerSystem { + const prcs: NodeJS.Process = c?.process ?? global.process; + const logger: Logger | undefined = c?.logger; + const destroys = new Set<() => Promise | void>(); + const onInterruptsCallbacks: (() => void)[] = []; + + const sysCpus = cpus(); + const hardwareConcurrency = sysCpus.length; + const osPlatform = platform(); + + // Note: tsdown bundles this into a chunk at dist/, so __dirname = dist/ + const compilerExecutingPath = path.join(__dirname, 'compiler', 'index.mjs'); + + const runInterruptsCallbacks = () => { + const returnValues: Promise[] = []; + let cb: () => any; + while (isFunction((cb = onInterruptsCallbacks.pop()))) { + try { + const rtn = cb(); + returnValues.push(rtn); + } catch {} + } + return Promise.all(returnValues).then(() => {}); + }; + + const sys: CompilerSystem = { + name: 'node', + version: prcs.versions.node, + access(p) { + return new Promise((resolve) => { + fs.access(p, (err) => resolve(!err)); + }); + }, + accessSync(p) { + let hasAccess = false; + try { + fs.accessSync(p); + hasAccess = true; + } catch {} + return hasAccess; + }, + addDestroy(cb) { + destroys.add(cb); + }, + removeDestroy(cb) { + destroys.delete(cb); + }, + applyPrerenderGlobalPatch(opts) { + // Node 18+ has native fetch, Headers, Request, Response globally available + opts.window.fetch = global.fetch; + opts.window.Headers = global.Headers; + opts.window.Request = global.Request; + opts.window.Response = global.Response; + }, + fetch: global.fetch, + checkVersion, + copyFile(src, dst) { + return new Promise((resolve) => { + fs.copyFile(src, dst, (err) => { + resolve(!err); + }); + }); + }, + createDir(p, opts) { + return new Promise((resolve) => { + if (opts) { + fs.mkdir(p, opts, (err) => { + resolve({ + basename: path.basename(p), + dirname: path.dirname(p), + path: p, + newDirs: [], + error: err, + }); + }); + } else { + fs.mkdir(p, (err) => { + resolve({ + basename: path.basename(p), + dirname: path.dirname(p), + path: p, + newDirs: [], + error: err, + }); + }); + } + }); + }, + createDirSync(p, opts) { + const results: CompilerSystemCreateDirectoryResults = { + basename: path.basename(p), + dirname: path.dirname(p), + path: p, + newDirs: [], + error: null, + }; + try { + fs.mkdirSync(p, opts); + } catch (e) { + results.error = e; + } + return results; + }, + createWorkerController(maxConcurrentWorkers) { + const forkModulePath = path.join(__dirname, 'sys', 'node', 'worker.mjs'); + return new NodeWorkerController(forkModulePath, maxConcurrentWorkers); + }, + async destroy() { + const waits: Promise[] = []; + destroys.forEach((cb) => { + try { + const rtn = cb(); + if (rtn && typeof rtn.then === 'function') { + waits.push(rtn); + } + } catch (e) { + console.error(`node sys destroy: ${e}`); + } + }); + if (waits.length > 0) { + await Promise.all(waits); + } + destroys.clear(); + }, + dynamicImport(p) { + // Use pathToFileURL for proper Windows support (drive letters like D: would be interpreted as URL schemes) + return import(pathToFileURL(p).href); + }, + encodeToBase64(str) { + return Buffer.from(str).toString('base64'); + }, + exit: async (exitCode) => { + await runInterruptsCallbacks(); + process.exitCode = exitCode; + }, + getCurrentDirectory() { + return normalizePath(prcs.cwd()); + }, + getCompilerExecutingPath() { + return compilerExecutingPath; + }, + getEnvironmentVar(key) { + return process.env[key]; + }, + getLocalModulePath() { + return null; + }, + getRemoteModuleUrl() { + return null; + }, + glob: asyncGlob, + hardwareConcurrency, + isSymbolicLink(p: string) { + return new Promise((resolve) => { + try { + fs.lstat(p, (err, stats) => { + if (err) { + resolve(false); + } else { + resolve(stats.isSymbolicLink()); + } + }); + } catch { + resolve(false); + } + }); + }, + nextTick: prcs.nextTick, + normalizePath, + onProcessInterrupt: (cb) => { + if (!onInterruptsCallbacks.includes(cb)) { + onInterruptsCallbacks.push(cb); + } + }, + platformPath: path, + readDir(p) { + return new Promise((resolve) => { + fs.readdir(p, (err, files) => { + if (err) { + resolve([]); + } else { + resolve( + files.map((f) => { + return normalizePath(path.join(p, f)); + }), + ); + } + }); + }); + }, + isTTY() { + return !!process?.stdout?.isTTY; + }, + readDirSync(p) { + try { + return fs.readdirSync(p).map((f) => { + return normalizePath(path.join(p, f)); + }); + } catch {} + return []; + }, + readFile(p: string, encoding?: string) { + if (encoding === 'binary') { + return new Promise((resolve) => { + fs.readFile(p, (_, data) => { + resolve(data); + }); + }); + } + return new Promise((resolve) => { + fs.readFile(p, 'utf8', (_, data) => { + resolve(data); + }); + }); + }, + readFileSync(p) { + try { + return fs.readFileSync(p, 'utf8'); + } catch {} + return undefined; + }, + homeDir() { + try { + return os.homedir(); + } catch {} + return undefined; + }, + realpath(p) { + return new Promise((resolve) => { + fs.realpath(p, 'utf8', (e, data) => { + resolve({ + path: data, + error: e, + }); + }); + }); + }, + realpathSync(p) { + const results: CompilerSystemRealpathResults = { + path: undefined, + error: null, + }; + try { + results.path = fs.realpathSync(p, 'utf8'); + } catch (e) { + results.error = e; + } + return results; + }, + rename(oldPath, newPath) { + return new Promise((resolve) => { + fs.rename(oldPath, newPath, (error) => { + resolve({ + oldPath, + newPath, + error, + oldDirs: [], + oldFiles: [], + newDirs: [], + newFiles: [], + renamed: [], + isFile: false, + isDirectory: false, + }); + }); + }); + }, + resolvePath(p) { + return normalizePath(p); + }, + removeDir(p, opts) { + return new Promise((resolve) => { + const recursive = !!(opts && opts.recursive); + if (recursive) { + fs.rm(p, { recursive: true, force: true }, (err) => { + resolve({ + basename: path.basename(p), + dirname: path.dirname(p), + path: p, + removedDirs: [], + removedFiles: [], + error: err, + }); + }); + } else { + fs.rmdir(p, (err) => { + resolve({ + basename: path.basename(p), + dirname: path.dirname(p), + path: p, + removedDirs: [], + removedFiles: [], + error: err, + }); + }); + } + }); + }, + removeDirSync(p, opts) { + try { + const recursive = !!(opts && opts.recursive); + if (recursive) { + fs.rmSync(p, { recursive: true, force: true }); + } else { + fs.rmdirSync(p); + } + return { + basename: path.basename(p), + dirname: path.dirname(p), + path: p, + removedDirs: [], + removedFiles: [], + error: null, + }; + } catch (e) { + return { + basename: path.basename(p), + dirname: path.dirname(p), + path: p, + removedDirs: [], + removedFiles: [], + error: e, + }; + } + }, + removeFile(p) { + return new Promise((resolve) => { + fs.unlink(p, (err) => { + resolve({ + basename: path.basename(p), + dirname: path.dirname(p), + path: p, + error: err, + }); + }); + }); + }, + removeFileSync(p) { + const results: CompilerSystemRemoveFileResults = { + basename: path.basename(p), + dirname: path.dirname(p), + path: p, + error: null, + }; + try { + fs.unlinkSync(p); + } catch (e) { + results.error = e; + } + return results; + }, + setupCompiler(_c) { + sys.watchTimeout = 80; + sys.events = buildEvents(); + + // Track active subscriptions for cleanup + const activeSubscriptions = new Map>(); + + /** + * Watch a directory for changes using @parcel/watcher. + * Uses native file system events (FSEvents on macOS, inotify on Linux, ReadDirectoryChangesW on Windows) + * for efficient, low-latency change detection. + * + * @param p - the directory path to watch + * @param callback - the callback function when changes occur + * @param _recursive - unused recursive flag + * @returns a file watcher with a close method + */ + sys.watchDirectory = (p, callback, _recursive) => { + logger?.debug(`NODE_SYS_DEBUG::watchDir ${p}`); + + const subscriptionPromise = parcelWatcher + .subscribe( + p, + (err, events) => { + if (err) { + logger?.error(`Watch error for ${p}: ${err.message}`); + return; + } + for (const event of events) { + const fileName = normalizePath(event.path); + logger?.debug( + `NODE_SYS_DEBUG::watchDir:callback dir=${p} changedPath=${fileName} type=${event.type}`, + ); + + // Map @parcel/watcher event types to Stencil's event types + if (event.type === 'create') { + callback(fileName, 'fileAdd'); + } else if (event.type === 'update') { + callback(fileName, 'fileUpdate'); + } else if (event.type === 'delete') { + callback(fileName, 'fileDelete'); + } + } + }, + { + ignore: ['.git', 'node_modules', '.stencil', 'dist', 'www', '.cache'], + }, + ) + .catch((err) => { + // Directory may not exist yet - this is expected during initial builds + logger?.debug(`Watch subscribe failed for ${p}: ${err.message}`); + return null; + }); + + activeSubscriptions.set(p, subscriptionPromise as Promise); + + const close = () => { + const sub = activeSubscriptions.get(p); + if (sub) { + activeSubscriptions.delete(p); + sub.then((s) => s?.unsubscribe()).catch(() => {}); + } + }; + + sys.addDestroy(close); + + return { + close() { + sys.removeDestroy(close); + close(); + }, + }; + }; + + /** + * Watch an individual file for changes using @parcel/watcher. + * Watches the parent directory and filters events for the specific file. + * + * @param filePath - the file path to watch + * @param callback - the callback function when the file changes + * @returns a file watcher with a close method + */ + sys.watchFile = ( + filePath: string, + callback: CompilerFileWatcherCallback, + ): CompilerFileWatcher => { + logger?.debug(`NODE_SYS_DEBUG::watchFile ${filePath}`); + + const normalizedPath = normalizePath(filePath); + const dirPath = path.dirname(filePath); + + const subscriptionPromise = parcelWatcher + .subscribe( + dirPath, + (err, events) => { + if (err) { + logger?.error(`Watch error for ${filePath}: ${err.message}`); + return; + } + for (const event of events) { + const eventPath = normalizePath(event.path); + // Only process events for the specific file we're watching + if (eventPath === normalizedPath) { + logger?.debug( + `NODE_SYS_DEBUG::watchFile:callback file=${filePath} type=${event.type}`, + ); + + if (event.type === 'create') { + callback(eventPath, 'fileAdd'); + sys.events.emit('fileAdd', eventPath); + } else if (event.type === 'update') { + callback(eventPath, 'fileUpdate'); + sys.events.emit('fileUpdate', eventPath); + } else if (event.type === 'delete') { + callback(eventPath, 'fileDelete'); + sys.events.emit('fileDelete', eventPath); + } + } + } + }, + { + ignore: ['.git', 'node_modules'], + }, + ) + .catch((err) => { + // Directory may not exist yet - this is expected for files being watched before creation + logger?.debug(`Watch subscribe failed for ${filePath}: ${err.message}`); + return null; + }); + + const subscriptionKey = `file:${filePath}`; + activeSubscriptions.set( + subscriptionKey, + subscriptionPromise as Promise, + ); + + const close = () => { + const sub = activeSubscriptions.get(subscriptionKey); + if (sub) { + activeSubscriptions.delete(subscriptionKey); + sub.then((s) => s?.unsubscribe()).catch(() => {}); + } + }; + + sys.addDestroy(close); + + return { + close() { + sys.removeDestroy(close); + close(); + }, + }; + }; + }, + stat(p) { + return new Promise((resolve) => { + fs.stat(p, (err, fsStat) => { + if (err) { + resolve({ + isDirectory: false, + isFile: false, + isSymbolicLink: false, + size: 0, + mtimeMs: 0, + error: err, + }); + } else { + resolve({ + isDirectory: fsStat.isDirectory(), + isFile: fsStat.isFile(), + isSymbolicLink: fsStat.isSymbolicLink(), + size: fsStat.size, + mtimeMs: fsStat.mtimeMs, + error: null, + }); + } + }); + }); + }, + statSync(p) { + try { + const fsStat = fs.statSync(p); + return { + isDirectory: fsStat.isDirectory(), + isFile: fsStat.isFile(), + isSymbolicLink: fsStat.isSymbolicLink(), + size: fsStat.size, + mtimeMs: fsStat.mtimeMs, + error: null, + }; + } catch (e) { + return { + isDirectory: false, + isFile: false, + isSymbolicLink: false, + size: 0, + mtimeMs: 0, + error: e, + }; + } + }, + tmpDirSync() { + return tmpdir(); + }, + writeFile(p, content) { + return new Promise((resolve) => { + fs.writeFile(p, content, (err) => { + resolve({ path: p, error: err }); + }); + }); + }, + writeFileSync(p, content) { + const results: CompilerSystemWriteFileResults = { + path: p, + error: null, + }; + try { + fs.writeFileSync(p, content); + } catch (e) { + results.error = e; + } + return results; + }, + generateContentHash(content, length) { + let hash = createHash('sha1').update(content).digest('hex').toLowerCase(); + if (typeof length === 'number') { + hash = hash.slice(0, length); + } + return Promise.resolve(hash); + }, + generateFileHash(filePath, length) { + return new Promise((resolve, reject) => { + const h = createHash('sha1'); + fs.createReadStream(filePath) + .on('error', (err) => reject(err)) + .on('data', (data) => h.update(data)) + .on('end', () => { + let hash = h.digest('hex').toLowerCase(); + if (typeof length === 'number') { + hash = hash.slice(0, length); + } + resolve(hash); + }); + }); + }, + copy: nodeCopyTasks, + details: { + cpuModel: + (Array.isArray(sysCpus) && sysCpus.length > 0 ? sysCpus[0] && sysCpus[0].model : '') || '', + freemem() { + return freemem(); + }, + platform: + osPlatform === 'darwin' || osPlatform === 'linux' + ? osPlatform + : osPlatform === 'win32' + ? 'windows' + : '', + release: release(), + totalmem: totalmem(), + }, + }; + + const nodeResolve = new NodeResolveModule(); + + sys.lazyRequire = new NodeLazyRequire(nodeResolve, { + '@types/jest': { minVersion: '24.9.1', recommendedVersion: '29', maxVersion: '29.0.0' }, + jest: { minVersion: '24.9.0', recommendedVersion: '29', maxVersion: '29.0.0' }, + 'jest-cli': { minVersion: '24.9.0', recommendedVersion: '29', maxVersion: '29.0.0' }, + puppeteer: { minVersion: '10.0.0', recommendedVersion: '20' }, + 'puppeteer-core': { minVersion: '10.0.0', recommendedVersion: '20' }, + 'workbox-build': { minVersion: '4.3.1', recommendedVersion: '4.3.1' }, + }); + + prcs.on('SIGINT', runInterruptsCallbacks); + prcs.on('exit', runInterruptsCallbacks); + + return sys; +} diff --git a/src/sys/node/node-worker-controller.ts b/packages/core/src/sys/node/node-worker-controller.ts similarity index 98% rename from src/sys/node/node-worker-controller.ts rename to packages/core/src/sys/node/node-worker-controller.ts index eafdce994db..f12c15c5664 100755 --- a/src/sys/node/node-worker-controller.ts +++ b/packages/core/src/sys/node/node-worker-controller.ts @@ -1,8 +1,8 @@ -import { TASK_CANCELED_MSG } from '@utils'; import { EventEmitter } from 'events'; import { cpus } from 'os'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; +import { TASK_CANCELED_MSG } from '../../utils'; import { NodeWorkerMain } from './node-worker-main'; /** diff --git a/src/sys/node/node-worker-main.ts b/packages/core/src/sys/node/node-worker-main.ts similarity index 95% rename from src/sys/node/node-worker-main.ts rename to packages/core/src/sys/node/node-worker-main.ts index 17e1e7d18bc..7b08d8b4b6f 100644 --- a/src/sys/node/node-worker-main.ts +++ b/packages/core/src/sys/node/node-worker-main.ts @@ -1,8 +1,8 @@ -import { TASK_CANCELED_MSG } from '@utils'; import * as cp from 'child_process'; import { EventEmitter } from 'events'; +import type * as d from '@stencil/core'; -import type * as d from '../../declarations'; +import { TASK_CANCELED_MSG } from '../../utils'; /** * A class that holds a reference to a node worker sub-process within the main @@ -56,7 +56,7 @@ export class NodeWorkerMain extends EventEmitter { this.childProcess.stderr.setEncoding('utf8'); this.childProcess.stderr.on('data', (data) => { - console.log(data); + console.error(data); }); this.childProcess.on('message', this.receiveFromWorker.bind(this)); @@ -104,7 +104,7 @@ export class NodeWorkerMain extends EventEmitter { } }); - if (!success || /^win/.test(process.platform)) { + if (!success || process.platform.startsWith('win')) { this.processQueue = false; } } diff --git a/src/sys/node/node-worker-thread.ts b/packages/core/src/sys/node/node-worker-thread.ts similarity index 97% rename from src/sys/node/node-worker-thread.ts rename to packages/core/src/sys/node/node-worker-thread.ts index c1e1f19edab..07c940ab95f 100755 --- a/src/sys/node/node-worker-thread.ts +++ b/packages/core/src/sys/node/node-worker-thread.ts @@ -1,4 +1,4 @@ -import type * as d from '../../declarations'; +import type * as d from '@stencil/core'; /** * Initialize a worker thread, setting up various machinery for managing diff --git a/src/sys/node/worker.ts b/packages/core/src/sys/node/worker.ts similarity index 84% rename from src/sys/node/worker.ts rename to packages/core/src/sys/node/worker.ts index 5917f9f36a5..4cec27771ad 100644 --- a/src/sys/node/worker.ts +++ b/packages/core/src/sys/node/worker.ts @@ -1,7 +1,6 @@ -import * as coreCompiler from '@stencil/core/compiler'; -import * as nodeApi from '@sys-api-node'; - +import * as coreCompiler from '../../compiler'; import { initNodeWorkerThread } from './node-worker-thread'; +import * as nodeApi from './'; // this module is the entry point for the node.js workers that we create using // `child_process.fork`. They receive messages from the main thread and diff --git a/packages/core/src/sys/readme.md b/packages/core/src/sys/readme.md new file mode 100644 index 00000000000..772bf034640 --- /dev/null +++ b/packages/core/src/sys/readme.md @@ -0,0 +1,41 @@ +# sys + +System abstraction layer for Node.js APIs. + +## Overview + +This directory provides abstractions over Node.js file system, path, and other system APIs. It allows the compiler to work with different underlying implementations. + +## Historical Context + +In earlier versions of Stencil, this abstraction supported: + +- Node.js (primary) +- In-browser compilation (deprecated in v5) +- Different file system backends + +## Current Status (v5) + +With v5 targeting Node.js 18+ only and removing in-browser compilation, much of this abstraction layer is being simplified. The goal is to use Node.js APIs directly where possible. + +## Directory Structure + +| Directory | Purpose | +| --------- | -------------------------------- | +| `node/` | Node.js-specific implementations | + +## Key Interfaces + +- `CompilerSystem` - File system, path utilities, and platform detection +- `CompilerFileSystem` - File read/write operations +- `Logger` - Logging abstraction + +## Migration Notes + +As part of v5 modernization, code should prefer: + +- Direct `node:fs` imports over `sys.readFile()` +- Direct `node:path` imports over `sys.path` +- Standard Node.js patterns over abstractions + +The abstraction remains for cases where the interface is genuinely useful, but unnecessary indirection is being removed. \ No newline at end of file diff --git a/packages/core/src/testing/app-data.ts b/packages/core/src/testing/app-data.ts new file mode 100644 index 00000000000..72d85709902 --- /dev/null +++ b/packages/core/src/testing/app-data.ts @@ -0,0 +1,82 @@ +import type { BuildConditionals } from '@stencil/core'; + +/** + * Testing-specific build conditionals. + * + * Key differences from production defaults: + * - `lazyLoad: true` - Required for `getElement()` to work correctly + * - `isTesting: true` - Enables testing-specific behavior + * - `isDev: true` - Enables dev-mode checks + */ +export const BUILD: BuildConditionals = { + allRenderFn: false, + element: true, + event: true, + hasRenderFn: true, + hostListener: true, + hostListenerTargetWindow: true, + hostListenerTargetDocument: true, + hostListenerTargetBody: true, + hostListenerTarget: true, + member: true, + method: true, + mode: true, + observeAttribute: true, + prop: true, + propMutable: true, + reflect: true, + scoped: true, + shadowDom: true, + slot: true, + cssAnnotations: false, + state: true, + style: true, + formAssociated: false, + svg: true, + updatable: true, + vdomAttribute: true, + vdomXlink: true, + vdomClass: true, + vdomFunctional: true, + vdomKey: true, + vdomListener: true, + vdomRef: true, + vdomPropOrAttr: true, + vdomRender: true, + vdomStyle: true, + vdomText: true, + propChangeCallback: true, + taskQueue: true, + hotModuleReplacement: false, + isDebug: false, + isDev: true, + isTesting: true, + hydrateServerSide: false, + hydrateClientSide: false, + lifecycleDOMEvents: false, + lazyLoad: true, // Critical for getElement() to work + profile: false, + slotRelocation: true, + lightDomPatches: false, + slotChildNodes: false, + slotCloneNode: false, + slotDomMutations: false, + slotTextContent: false, + hydratedAttribute: false, + hydratedClass: true, + invisiblePrehydration: true, + propBoolean: true, + propNumber: true, + propString: true, + constructableCSS: true, + devTools: false, + shadowDelegatesFocus: true, + shadowSlotAssignmentManual: false, + initializeNextTick: false, + asyncLoading: true, + asyncQueue: false, +}; + +export const Env = {}; + +export const NAMESPACE = 'app' as string; diff --git a/packages/core/src/testing/create-test-compiler.ts b/packages/core/src/testing/create-test-compiler.ts new file mode 100644 index 00000000000..037905009bf --- /dev/null +++ b/packages/core/src/testing/create-test-compiler.ts @@ -0,0 +1,256 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type * as d from '@stencil/core'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +import { createCompiler } from '../compiler/compiler'; +import { loadConfig } from '../compiler/config/load-config'; +import { mockCompilerSystem } from './mocks'; + +const TESTING_TSCONFIG = path.resolve(__dirname, 'fixtures/tsconfig.testing.json'); + +/** + * Options for creating a test compiler + */ +export interface CreateTestCompilerOptions { + /** + * Additional configuration overrides for the test compiler + */ + config?: Partial; + /** + * Path to a tsconfig.json file. Defaults to {@link TESTING_TSCONFIG}. + * To add custom options, create a file that `extends` the base fixture: + * `{ "extends": "./path/to/tsconfig.testing.json", "compilerOptions": { ... } }` + */ + tsconfig?: string; + /** + * Pre-validated compiler setup from {@link prepareTestCompiler}. When + * provided, the expensive `loadConfig` step is skipped and a fresh compiler + * is created directly from the cached validated config. + */ + setup?: PreparedTestCompiler; +} + +/** + * Result of creating a test compiler + */ +export interface TestCompilerResult { + /** + * The compiler instance ready for testing + */ + compiler: d.Compiler; + /** + * The validated configuration used to create the compiler + */ + config: d.ValidatedConfig; + /** + * The compiler system instance + */ + sys: d.CompilerSystem; +} + +/** + * A pre-validated compiler configuration that can be reused across multiple + * `createTestCompiler` calls within the same test suite to avoid repeating + * the expensive `loadConfig` step. + * + * Obtain via {@link prepareTestCompiler} in a `beforeAll` block, then pass as + * `options.setup` to {@link createTestCompiler} in each `beforeEach`. + */ +export interface PreparedTestCompiler { + /** @internal */ + _validatedConfig: d.ValidatedConfig; + /** @internal */ + _tsconfigPath: string; +} + +/** + * Builds a patched sys that falls through to real disk for TypeScript lib files. + * + * @returns A compiler system instance with patched readFileSync, readFile, accessSync, and statSync methods + */ +const createPatchedSys = (): d.CompilerSystem => { + const sys = mockCompilerSystem(); + + // The in-memory sys has no disk access, but TypeScript needs to read its own + // lib files (lib.es2022.d.ts etc.) from disk. Wrap reads to fall through to + // real node:fs for anything not already in memory. + const originalReadFileSync = sys.readFileSync.bind(sys); + sys.readFileSync = (p: string) => { + const mem = originalReadFileSync(p); + if (mem !== undefined) return mem; + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return undefined; + } + }; + + const originalReadFile = sys.readFile.bind(sys); + sys.readFile = async (p: string) => { + const mem = await originalReadFile(p); + if (mem !== undefined) return mem; + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return undefined; + } + }; + + const originalAccessSync = sys.accessSync.bind(sys); + sys.accessSync = (p: string) => { + if (originalAccessSync(p)) return true; + try { + fs.accessSync(p); + return true; + } catch { + return false; + } + }; + + const originalStatSync = sys.statSync.bind(sys); + sys.statSync = (p: string) => { + const mem = originalStatSync(p); + if (!mem.error) return mem; + try { + const s = fs.statSync(p); + return { + isDirectory: s.isDirectory(), + isFile: s.isFile(), + isSymbolicLink: s.isSymbolicLink(), + size: s.size, + error: null, + }; + } catch { + return mem; + } + }; + + // Point getCompilerExecutingPath at the real built compiler so that + // coreResolvePlugin can compute an absolute path to dist/runtime/. + sys.getCompilerExecutingPath = () => path.resolve(__dirname, '../../dist/compiler/index.mjs'); + + return sys; +}; + +/** + * Runs the expensive one-time setup for a test compiler suite: patching the + * sys, reading and validating the tsconfig. Use this in a `beforeAll` block + * when a describe block contains multiple tests that each need a fresh + * compiler, to avoid repeating `loadConfig` on every test. + * + * @param options - Configuration options for preparing the test compiler + * @returns A {@link PreparedTestCompiler} that can be passed to {@link createTestCompiler} + * + * @example + * ```ts + * let setup: PreparedTestCompiler; + * beforeAll(async () => { setup = await prepareTestCompiler(); }); + * beforeEach(async () => { + * const { compiler } = await createTestCompiler({ setup }); + * }); + * ``` + */ +export const prepareTestCompiler = async ( + options: Omit = {}, +): Promise => { + const sys = createPatchedSys(); + const tsconfigPath = options.tsconfig ?? TESTING_TSCONFIG; + + const userConfig: d.Config = { + // @ts-expect-error - devMode is not publicly exposed, just chill + devMode: true, + sourceMap: false, + enableCache: false, + minifyJs: false, + minifyCss: false, + namespace: 'Testing', + tsconfig: tsconfigPath, + ...options.config, + }; + + const { config: validatedConfig } = await loadConfig({ + sys, + config: userConfig, + initTsConfig: false, + }); + + return { _validatedConfig: validatedConfig, _tsconfigPath: tsconfigPath }; +}; + +/** + * Creates a test compiler instance with a hybrid filesystem (reads from disk, writes to memory). + * This utility handles the common setup pattern for compiler tests. + * + * When multiple tests in the same suite need independent compiler instances, + * pass a {@link PreparedTestCompiler} from {@link prepareTestCompiler} as + * `options.setup` to skip the expensive `loadConfig` step on each test. + * + * @param options - Configuration options for the test compiler + * @returns An object with the compiler, validated config, and system instance + * + * @example + * ```ts + * const { compiler, config } = await createTestCompiler({ + * config: { minifyCss: true } + * }); + * await compiler.fs.writeFile('/src/index.html', ''); + * const result = await compiler.build(); + * ``` + */ +export const createTestCompiler = async ( + options: CreateTestCompilerOptions = {}, +): Promise => { + let validatedConfig: d.ValidatedConfig; + let tsconfigPath: string; + + if (options.setup) { + // Reuse the pre-validated config, but give this compiler a fresh sys so + // its in-memory filesystem is isolated from other tests in the suite. + const freshSys = createPatchedSys(); + validatedConfig = { ...options.setup._validatedConfig, sys: freshSys }; + tsconfigPath = options.setup._tsconfigPath; + } else { + const sys = createPatchedSys(); + tsconfigPath = options.tsconfig ?? TESTING_TSCONFIG; + + const userConfig: d.Config = { + // @ts-expect-error - devMode is not publicly exposed, just chill + devMode: true, + sourceMap: false, + enableCache: false, + minifyJs: false, + minifyCss: false, + namespace: 'Testing', + tsconfig: tsconfigPath, + ...options.config, + }; + + const { config } = await loadConfig({ sys, config: userConfig, initTsConfig: false }); + validatedConfig = config; + } + + const compiler = await createCompiler(validatedConfig); + + // Overlay the fixture tsconfig in inMemoryFs with `include` pointing at srcDir. + // TypeScript reads via the patched inMemoryFs and discovers virtual source files; + // rolldown reads the real fixture from disk and is unaffected. + const tsconfigObj = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8')); + await compiler.fs.writeFile( + tsconfigPath, + JSON.stringify({ ...tsconfigObj, include: [validatedConfig.srcDir] }), + ); + + // Pre-create components.d.ts so the first build skips the two-pass bootstrap + // (components.d.ts absent → generate → needsRebuild). generateAppTypes will + // overwrite this stub with correct content during the normal build flow. + await compiler.fs.writeFile(path.join(validatedConfig.srcDir, 'components.d.ts'), ''); + + return { + compiler, + config: validatedConfig, + sys: validatedConfig.sys, + }; +}; diff --git a/packages/core/src/testing/fixtures/tsconfig.testing.json b/packages/core/src/testing/fixtures/tsconfig.testing.json new file mode 100644 index 00000000000..bc280189774 --- /dev/null +++ b/packages/core/src/testing/fixtures/tsconfig.testing.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "allowJs": true, + "declaration": false, + "experimentalDecorators": true, + "lib": ["es2022", "dom"], + "module": "ESNext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "target": "ES2022", + "jsx": "react", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + } +} diff --git a/packages/core/src/testing/index.ts b/packages/core/src/testing/index.ts new file mode 100644 index 00000000000..da437be4284 --- /dev/null +++ b/packages/core/src/testing/index.ts @@ -0,0 +1,48 @@ +export { + mockBuildCtx, + mockCompilerCtx, + mockCompilerSystem, + mockComponentMeta, + mockConfig, + mockDocument, + mockLoadConfigInit, + mockLogger, + mockModule, + mockValidatedConfig, + mockWindow, +} from './mocks'; +export { + createTestCompiler, + prepareTestCompiler, + type CreateTestCompilerOptions, + type PreparedTestCompiler, + type TestCompilerResult, +} from './create-test-compiler'; +export { newSpecPage } from './spec-page'; +export { setupConsoleMocker, shuffleArray } from './testing-utils'; +export { createTestingSystem } from './testing-sys'; +export { + registerInstance, + getHostRef, + registerHost, + setErrorHandler, + writeTask, + readTask, + Build, + Env, + setMode, + getMode, +} from './platform'; +export { + h, + Host, + createEvent, + getElement, + Fragment, + getAssetPath, + setAssetPath, + forceUpdate, + Mixin, + getRenderingRef, +} from '../runtime'; +export type { SpecPage } from '@stencil/core'; diff --git a/packages/core/src/testing/mocks.ts b/packages/core/src/testing/mocks.ts new file mode 100644 index 00000000000..3472ddae232 --- /dev/null +++ b/packages/core/src/testing/mocks.ts @@ -0,0 +1,376 @@ +import path from 'node:path'; +import { MockWindow } from '@stencil/mock-doc'; +import type * as d from '@stencil/core'; + +import { createWorkerContext } from '../compiler'; +import { BuildContext } from '../compiler/build/build-ctx'; +import { Cache as CompilerCache } from '../compiler/cache'; +import { buildEvents } from '../compiler/events'; +import { createInMemoryFs } from '../compiler/sys/in-memory-fs'; +import { noop } from '../utils'; +import { TestingLogger } from './testing-logger'; +import { createTestingSystem, TestingSystem } from './testing-sys'; + +/** + * Generates a stub {@link d.ComponentCompilerMeta}. This function uses sensible defaults for the initial stub. However, + * any field in the object may be overridden via the `overrides` argument. + * @param overrides a partial implementation of `ComponentCompilerMeta`. Any provided fields will override the + * defaults provided by this function. + * @returns the stubbed `ComponentCompilerMeta` + */ +export const mockComponentMeta = ( + overrides: Partial = {}, +): d.ComponentCompilerMeta => ({ + assetsDirs: [], + attachInternalsMemberName: null, + attachInternalsCustomStates: [], + componentClassName: 'StubCmp', + dependencies: [], + dependents: [], + deserializers: [], + directDependencies: [], + directDependents: [], + docs: { text: 'docs', tags: [] }, + doesExtend: false, + elementRef: '', + encapsulation: 'none', + events: [], + excludeFromCollection: false, + formAssociated: false, + hasAttribute: false, + hasAttributeChangedCallbackFn: false, + hasComponentDidLoadFn: false, + hasComponentDidRenderFn: false, + hasComponentDidUpdateFn: false, + hasComponentShouldUpdateFn: false, + hasComponentWillLoadFn: false, + hasComponentWillRenderFn: false, + hasComponentWillUpdateFn: false, + hasConnectedCallbackFn: false, + hasDeserializer: false, + hasDisconnectedCallbackFn: false, + hasElement: false, + hasEvent: false, + hasLifecycle: false, + hasListener: false, + hasListenerTarget: false, + hasListenerTargetBody: false, + hasListenerTargetDocument: false, + hasListenerTargetWindow: false, + hasMember: false, + hasMethod: false, + hasMode: false, + hasModernPropertyDecls: false, + hasPatchAll: false, + hasPatchChildren: false, + hasPatchClone: false, + hasPatchInsert: false, + hasProp: false, + hasPropBoolean: false, + hasPropMutable: false, + hasPropNumber: false, + hasPropString: false, + hasReflect: false, + hasRenderFn: false, + hasSerializer: false, + hasSlot: false, + hasState: false, + hasStyle: false, + hasVdomAttribute: false, + hasVdomClass: false, + hasVdomFunctional: false, + hasVdomKey: false, + hasVdomListener: false, + hasVdomPropOrAttr: false, + hasVdomRef: false, + hasVdomRender: false, + hasVdomStyle: false, + hasVdomText: false, + hasVdomXlink: false, + hasWatchCallback: false, + htmlAttrNames: [], + htmlParts: [], + htmlTagNames: [], + internal: false, + isCollectionDependency: false, + isPlain: false, + isUpdateable: false, + jsFilePath: '/some/stubbed/path/my-component.js', + listeners: [], + methods: [], + patches: null, + potentialCmpRefs: [], + properties: [], + serializers: [], + shadowDelegatesFocus: false, + shadowMode: null, + slotAssignment: null, + sourceFilePath: '/some/stubbed/path/my-component.tsx', + sourceMapPath: '/some/stubbed/path/my-component.js.map', + states: [], + styleDocs: [], + styles: [], + globalStyles: [], + tagName: 'stub-cmp', + virtualProperties: [], + watchers: [], + ...overrides, +}); + +/** + * Creates a mock instance of an internal, validated Stencil configuration object + * the caller + * @param overrides a partial implementation of `ValidatedConfig`. Any provided fields will override the defaults + * provided by this function. + * @returns the mock Stencil configuration + */ +export function mockValidatedConfig(overrides: Partial = {}): d.ValidatedConfig { + const baseConfig = mockConfig(overrides); + const rootDir = path.resolve('/'); + + return { + ...baseConfig, + cacheDir: '.stencil', + devMode: true, + devServer: {}, + extras: {}, + fsNamespace: 'testing', + hydratedFlag: null, + logLevel: 'info', + logger: mockLogger(), + minifyCss: false, + minifyJs: false, + namespace: 'Testing', + outputTargets: baseConfig.outputTargets ?? [], + packageJsonFilePath: path.join(rootDir, 'package.json'), + rootDir, + sourceMap: true, + srcDir: '/src', + srcIndexHtml: 'src/index.html', + suppressReservedPublicNameWarnings: false, + sys: createTestingSystem(), + transformAliasedImportPaths: true, + rolldownConfig: {}, + ...overrides, + }; +} + +/** + * Creates a mock instance of a Stencil configuration entity. The mocked configuration has no guarantees around the + * types/validity of its data. + * @param overrides a partial implementation of `UnvalidatedConfig`. Any provided fields will override the defaults + * provided by this function. + * @returns the mock Stencil configuration + */ +export function mockConfig(overrides: Partial = {}): d.UnvalidatedConfig { + const rootDir = path.resolve('/'); + + let { sys } = overrides; + if (!sys) { + sys = createTestingSystem(); + } + sys.getCurrentDirectory = () => rootDir; + + return { + _isTesting: true, + buildAppCore: false, + bundles: null, + devMode: true, + enableCache: false, + extras: {}, + globalScript: null, + logger: new TestingLogger(), + maxConcurrentWorkers: 0, + minifyCss: false, + minifyJs: false, + namespace: 'Testing', + nodeResolve: {}, + outputTargets: null, + rolldownPlugins: { + before: [], + after: [], + }, + rootDir, + sourceMap: true, + suppressReservedPublicNameWarnings: false, + sys, + validateTypes: false, + ...overrides, + }; +} + +/** + * Creates a configuration object used to bootstrap a Stencil task invocation + * + * Several fields are intentionally undefined for this entity. While it would be trivial to stub them out, this mock + * generation function operates under the assumption that entities like loggers and compiler system abstractions will + * be shared by multiple entities in a test suite, who should provide those entities to this function + * + * @param overrides the properties on the default entity to manually override + * @returns the default configuration initialization object, with any overrides applied + */ +export const mockLoadConfigInit = (overrides?: Partial): d.LoadConfigInit => { + const defaults: d.LoadConfigInit = { + config: {}, + configPath: undefined, + initTsConfig: true, + logger: undefined, + sys: undefined, + }; + + return { ...defaults, ...overrides }; +}; + +export function mockCompilerCtx(config?: d.ValidatedConfig) { + const innerConfig = config || mockValidatedConfig(); + const compilerCtx: d.CompilerCtx = { + version: 1, + activeBuildId: 0, + activeDirsAdded: [], + activeDirsDeleted: [], + activeFilesAdded: [], + activeFilesDeleted: [], + activeFilesUpdated: [], + addWatchDir: noop, + addWatchFile: noop, + globalStyleCache: new Map(), + changedFiles: new Set(), + changedModules: new Set(), + collections: [], + compilerOptions: null, + cache: null, + cssModuleImports: new Map(), + events: buildEvents(), + fs: null, + hasSuccessfulBuild: false, + isActivelyBuilding: false, + lastBuildResults: null, + moduleMap: new Map(), + nodeMap: new WeakMap(), + reset: noop, + resolvedCollections: new Set(), + rolldownCache: new Map(), + rolldownCacheSsr: null, + rolldownCacheLazy: null, + rolldownCacheNative: null, + transpileCache: new Map(), + prevStylesMap: new Map(), + styleModeNames: new Set(), + worker: createWorkerContext(innerConfig.sys), + cssTransformCache: new Map(), + }; + + Object.defineProperty(compilerCtx, 'fs', { + get() { + if (this._fs == null) { + this._fs = createInMemoryFs(innerConfig.sys); + } + return this._fs; + }, + }); + + Object.defineProperty(compilerCtx, 'cache', { + get() { + if (this._cache == null) { + this._cache = mockCache(innerConfig, compilerCtx); + } + return this._cache; + }, + }); + + return compilerCtx; +} + +export function mockBuildCtx(config?: d.ValidatedConfig, compilerCtx?: d.CompilerCtx): d.BuildCtx { + const validatedConfig = config || mockValidatedConfig(); + const validatedCompilerCtx = compilerCtx || mockCompilerCtx(validatedConfig); + + const buildCtx = new BuildContext(validatedConfig, validatedCompilerCtx); + return buildCtx as d.BuildCtx; +} + +function mockCache(config: d.ValidatedConfig, compilerCtx: d.CompilerCtx) { + config.enableCache = true; + const cache = new CompilerCache(config, compilerCtx.fs); + cache.initCacheDir(); + return cache as d.Cache; +} + +export function mockLogger() { + return new TestingLogger(); +} + +/** + * Create a {@link d.CompilerSystem} entity for testing the compiler. + * + * This function acts as a thin wrapper around a {@link TestingSystem} entity creation. It exists to provide a logical + * place in the codebase where we might expect Stencil engineers to reach for when attempting to mock a + * {@link d.CompilerSystem} base type. Should there prove to be usage of both this function and the one it wraps, + * reconsider if this wrapper is necessary. + * + * @returns a System instance for testing purposes. + */ +export function mockCompilerSystem(): TestingSystem { + return createTestingSystem(); +} + +export function mockDocument(html: string | null = null) { + const win = new MockWindow(html); + return win.document as Document; +} + +export function mockWindow(html?: string) { + const win = new MockWindow(html); + return win as any as Window; +} + +/** + * This gives you a mock Module, an interface which is the internal compiler + * representation of a module. It includes a bunch of information necessary for + * compilation, this mock basically sets sane defaults for all those values. + * + * @param mod is an override module that you can supply to set particular values + * @returns a module object ready to use in tests! + */ +export const mockModule = (mod: Partial = {}): d.Module => ({ + cmps: [], + isExtended: false, + isMixin: false, + hasExportableMixins: false, + coreRuntimeApis: [], + outputTargetCoreRuntimeApis: {}, + collectionName: '', + dtsFilePath: '', + excludeFromCollection: false, + externalImports: [], + htmlAttrNames: [], + htmlTagNames: [], + htmlParts: [], + isCollectionDependency: false, + isLegacy: false, + jsFilePath: '', + localImports: [], + functionalComponentDeps: [], + originalImports: [], + originalCollectionComponentPath: '', + potentialCmpRefs: [], + sourceFilePath: '', + staticSourceFile: '', + staticSourceFileText: '', + sourceMapPath: '', + sourceMapFileText: '', + + // build features + hasVdomAttribute: false, + hasVdomClass: false, + hasVdomFunctional: false, + hasVdomKey: false, + hasVdomListener: false, + hasVdomPropOrAttr: false, + hasVdomRef: false, + hasVdomRender: false, + hasVdomStyle: false, + hasVdomText: false, + hasVdomXlink: false, + ...mod, +}); diff --git a/packages/core/src/testing/platform/index.ts b/packages/core/src/testing/platform/index.ts new file mode 100644 index 00000000000..c1d19dee876 --- /dev/null +++ b/packages/core/src/testing/platform/index.ts @@ -0,0 +1,50 @@ +import { modeResolutionChain } from './testing-constants'; +export { Build } from './testing-build'; +export { modeResolutionChain, styles } from './testing-constants'; +export { getHostRef, registerHost, registerInstance } from './testing-host-ref'; +export { + consoleDevError, + consoleDevInfo, + consoleDevWarn, + consoleError, + setErrorHandler, +} from './testing-log'; +export { + isMemberInElement, + plt, + registerComponents, + registerModule, + resetPlatform, + setPlatformHelpers, + startAutoApplyChanges, + stopAutoApplyChanges, + supportsConstructableStylesheets, + supportsListenerOptions, + supportsMutableAdoptedStyleSheets, +} from './testing-platform'; +export { + flushAll, + flushLoadModule, + flushQueue, + loadModule, + nextTick, + readTask, + writeTask, +} from './testing-task-queue'; +export { win } from './testing-window'; +export { Env } from 'virtual:app-data'; +export * from '../../runtime'; + +// Testing-specific setMode that clears previous handlers first +// This shadows the runtime's setMode so each test gets a clean slate +export const setMode = (handler: (elm: any) => string | undefined | null) => { + modeResolutionChain.length = 0; + modeResolutionChain.push(handler); +}; + +export const setScopedSsr = (scoped?: boolean) => { + scopedSSR = scoped; +}; +export const needsScopedSSR = () => scopedSSR; + +let scopedSSR = false; diff --git a/src/testing/platform/load-module.ts b/packages/core/src/testing/platform/load-module.ts similarity index 100% rename from src/testing/platform/load-module.ts rename to packages/core/src/testing/platform/load-module.ts diff --git a/packages/core/src/testing/platform/testing-build.ts b/packages/core/src/testing/platform/testing-build.ts new file mode 100644 index 00000000000..0bc717c519c --- /dev/null +++ b/packages/core/src/testing/platform/testing-build.ts @@ -0,0 +1,8 @@ +import type * as d from '@stencil/core'; + +export const Build: d.UserBuildConditionals = { + isDev: true, + isBrowser: false, + isServer: true, + isTesting: true, +}; diff --git a/src/testing/platform/testing-constants.ts b/packages/core/src/testing/platform/testing-constants.ts similarity index 94% rename from src/testing/platform/testing-constants.ts rename to packages/core/src/testing/platform/testing-constants.ts index 7f974ca86d9..b0edeb5c2e0 100644 --- a/src/testing/platform/testing-constants.ts +++ b/packages/core/src/testing/platform/testing-constants.ts @@ -1,4 +1,4 @@ -import type * as d from '@stencil/core/internal'; +import type * as d from '@stencil/core'; import { QueuedLoadModule } from './load-module'; diff --git a/src/testing/platform/testing-host-ref.ts b/packages/core/src/testing/platform/testing-host-ref.ts similarity index 86% rename from src/testing/platform/testing-host-ref.ts rename to packages/core/src/testing/platform/testing-host-ref.ts index 74fea69a9dc..ce411d051c6 100644 --- a/src/testing/platform/testing-host-ref.ts +++ b/packages/core/src/testing/platform/testing-host-ref.ts @@ -1,6 +1,9 @@ -import type * as d from '@stencil/core/internal'; +import { BUILD } from 'virtual:app-data'; +import type * as d from '@stencil/core'; + import { createEvent } from '../../runtime/event-emitter'; -import { EVENT_FLAGS } from '@utils'; +import { CMP_FLAGS, EVENT_FLAGS } from '../../utils'; +import { reWireGetterSetter } from '../../utils/es2022-rewire-class-members'; /** * Retrieve the data structure tracking the component by its runtime reference @@ -8,8 +11,8 @@ import { EVENT_FLAGS } from '@utils'; * @returns the corresponding Stencil reference data structure, or undefined if one cannot be found */ export const getHostRef = (elm: d.RuntimeRef | undefined): d.HostRef | undefined => { - if (elm.__stencil__getHostRef) { - return elm.__stencil__getHostRef(); + if (elm.__s_ghr) { + return elm.__s_ghr(); } return undefined; @@ -29,15 +32,24 @@ export const registerInstance = (lazyInstance: any, hostRef: d.HostRef | null | if (hostRef == null) { const Cstr = lazyInstance.constructor as d.ComponentTestingConstructor; - const tagName = Cstr.COMPILER_META && Cstr.COMPILER_META.tagName ? Cstr.COMPILER_META.tagName : 'div'; + const tagName = + Cstr.COMPILER_META && Cstr.COMPILER_META.tagName ? Cstr.COMPILER_META.tagName : 'div'; const elm = document.createElement(tagName); registerHost(elm, { $flags$: 0, $tagName$: tagName }); hostRef = getHostRef(elm); } - lazyInstance.__stencil__getHostRef = () => hostRef; + lazyInstance.__s_ghr = () => hostRef; hostRef.$lazyInstance$ = lazyInstance; + // Re-wire getters/setters for ES2022+ class fields + if ( + hostRef.$cmpMeta$?.$flags$ & CMP_FLAGS.hasModernPropertyDecls && + (BUILD.state || BUILD.prop) + ) { + reWireGetterSetter(lazyInstance, hostRef); + } + // Create EventEmitters for all events from the component and its parent classes/mixins // This is necessary to support events defined in mixins that may not have been included // in the component's compiled constructor @@ -106,5 +118,5 @@ export const registerHost = (elm: d.HostElement, cmpMeta: d.ComponentRuntimeMeta hostRef.$onReadyPromise$ = new Promise((r) => (hostRef.$onReadyResolve$ = r)); elm['s-p'] = []; elm['s-rc'] = []; - elm.__stencil__getHostRef = () => hostRef; + elm.__s_ghr = () => hostRef; }; diff --git a/src/testing/platform/testing-log.ts b/packages/core/src/testing/platform/testing-log.ts similarity index 80% rename from src/testing/platform/testing-log.ts rename to packages/core/src/testing/platform/testing-log.ts index c1a2711dddf..8b118991dbe 100644 --- a/src/testing/platform/testing-log.ts +++ b/packages/core/src/testing/platform/testing-log.ts @@ -1,5 +1,5 @@ -import type * as d from '../../declarations'; import { caughtErrors } from './testing-constants'; +import type * as d from '../../declarations'; let customError: d.ErrorHandler | undefined; @@ -7,7 +7,8 @@ const defaultConsoleError = (e: any) => { caughtErrors.push(e); }; -export const consoleError: d.ErrorHandler = (e: any, el?: any) => (customError || defaultConsoleError)(e, el); +export const consoleError: d.ErrorHandler = (e: any, el?: any) => + (customError || defaultConsoleError)(e, el); export const consoleDevError = (...e: any[]) => { caughtErrors.push(new Error(e.join(', '))); @@ -15,7 +16,9 @@ export const consoleDevError = (...e: any[]) => { export const consoleDevWarn = (...args: any[]) => { // log warnings so we can spy on them when testing - const params = args.filter((a) => typeof a === 'string' || typeof a === 'number' || typeof a === 'boolean'); + const params = args.filter( + (a) => typeof a === 'string' || typeof a === 'number' || typeof a === 'boolean', + ); console.warn(...params); }; diff --git a/src/testing/platform/testing-platform.ts b/packages/core/src/testing/platform/testing-platform.ts similarity index 88% rename from src/testing/platform/testing-platform.ts rename to packages/core/src/testing/platform/testing-platform.ts index 08cbfb2f0ce..e1b0f518a5a 100644 --- a/src/testing/platform/testing-platform.ts +++ b/packages/core/src/testing/platform/testing-platform.ts @@ -1,11 +1,9 @@ -import type * as d from '@stencil/core/internal'; +import type * as d from '@stencil/core'; import { cstrs, moduleLoaded, styles } from './testing-constants'; import { flushAll, resetTaskQueue } from './testing-task-queue'; import { win } from './testing-window'; -export let supportsShadow = true; - export const plt: d.PlatformRuntime = { $flags$: 0, $resourcesUrl$: '', @@ -30,17 +28,6 @@ export const supportsListenerOptions = true; export const supportsConstructableStylesheets = false; export const supportsMutableAdoptedStyleSheets = false; -/** - * Helper function to programmatically set shadow DOM support in testing scenarios. - * - * This function modifies the global {@link supportsShadow} variable. - * - * @param supports `true` if shadow DOM is supported, `false` otherwise - */ -export const setSupportsShadowDom = (supports: boolean): void => { - supportsShadow = supports; -}; - /** * Resets global testing variables and collections, so that a new set of tests can be started with a "clean slate". * @@ -64,8 +51,6 @@ export function resetPlatform(defaults: Partial = {}) { plt.$orgLocNodes$ = undefined; } - win.location.href = plt.$resourcesUrl$ = `http://testing.stenciljs.com/`; - resetTaskQueue(); stopAutoApplyChanges(); diff --git a/src/testing/platform/testing-task-queue.ts b/packages/core/src/testing/platform/testing-task-queue.ts similarity index 94% rename from src/testing/platform/testing-task-queue.ts rename to packages/core/src/testing/platform/testing-task-queue.ts index 6b5478a6a19..34d3ca9fc4a 100644 --- a/src/testing/platform/testing-task-queue.ts +++ b/packages/core/src/testing/platform/testing-task-queue.ts @@ -1,4 +1,4 @@ -import type * as d from '@stencil/core/internal'; +import type * as d from '@stencil/core'; import { QueuedLoadModule } from './load-module'; import { @@ -39,7 +39,7 @@ export const nextTick = (cb: Function): void => { * Any callbacks that are added to `queuedTasks` while this function is running are scheduled to be flushed on the * next tick. */ -export function flushTicks(): Promise { +function flushTicks(): Promise { return new Promise((resolve, reject) => { function drain() { try { @@ -142,7 +142,13 @@ export function flushQueue(): Promise { } export async function flushAll(): Promise { - while (queuedTicks.length + queuedLoadModules.length + queuedWriteTasks.length + queuedReadTasks.length > 0) { + while ( + queuedTicks.length + + queuedLoadModules.length + + queuedWriteTasks.length + + queuedReadTasks.length > + 0 + ) { await flushTicks(); await flushLoadModule(); await flushQueue(); @@ -167,7 +173,11 @@ export async function flushAll(): Promise { * @param _hmrVersionId an unused parameter denoting the current hot-module reloading version * @returns A promise that loads the component onto `queuedLoadModules` */ -export function loadModule(cmpMeta: d.ComponentRuntimeMeta, _hostRef: d.HostRef, _hmrVersionId?: string): Promise { +export function loadModule( + cmpMeta: d.ComponentRuntimeMeta, + _hostRef: d.HostRef, + _hmrVersionId?: string, +): Promise { return new Promise((resolve) => { queuedLoadModules.push({ bundleId: cmpMeta.$lazyBundleId$, diff --git a/packages/core/src/testing/platform/testing-window.ts b/packages/core/src/testing/platform/testing-window.ts new file mode 100644 index 00000000000..574f185e797 --- /dev/null +++ b/packages/core/src/testing/platform/testing-window.ts @@ -0,0 +1,10 @@ +import { setupGlobal } from '@stencil/mock-doc'; + +// When running in vitest with a custom environment (like 'stencil'), the environment +// sets up the window before any test code imports. Try and use use that + +const isVitestEnvironment = typeof process !== 'undefined' && process.env?.VITEST; +const existingWindow = typeof globalThis !== 'undefined' && globalThis.window; + +export const win = + isVitestEnvironment && existingWindow ? existingWindow : (setupGlobal(global) as Window); diff --git a/packages/core/src/testing/reset-build-conditionals.ts b/packages/core/src/testing/reset-build-conditionals.ts new file mode 100644 index 00000000000..7fcfa4da6bf --- /dev/null +++ b/packages/core/src/testing/reset-build-conditionals.ts @@ -0,0 +1,56 @@ +import type * as d from '@stencil/core'; + +/** + * Reset build conditionals used for testing to a known "good state". + * + * This function does not return a value, but rather mutates its argument in place. + * Certain values are set to `true` or `false` for testing purpose (see this function's implementation for the full + * list). Build conditional options _not_ in that list that are set to `true` when this function is invoked will remain + * set to `true`. + * + * @param b the build conditionals to reset. + */ +export function resetBuildConditionals(b: d.BuildConditionals) { + Object.keys(b).forEach((key) => { + (b as any)[key] = true; + }); + + b.isDev = true; + b.isTesting = true; + b.isDebug = false; + b.lazyLoad = true; + b.member = true; + b.reflect = true; + b.scoped = true; + b.shadowDom = true; + b.slotRelocation = true; + b.asyncLoading = true; + b.svg = true; + b.updatable = true; + b.vdomAttribute = true; + b.vdomClass = true; + b.vdomFunctional = true; + b.vdomKey = true; + b.vdomPropOrAttr = true; + b.vdomRef = true; + b.vdomListener = true; + b.vdomStyle = true; + b.vdomText = true; + b.vdomXlink = true; + b.allRenderFn = false; + b.devTools = false; + b.hydrateClientSide = false; + b.hydrateServerSide = false; + b.cssAnnotations = false; + b.style = false; + b.hydratedAttribute = false; + b.hydratedClass = true; + b.invisiblePrehydration = true; + b.staticHydrationStyles = false; + b.lightDomPatches = false; + b.slotChildNodes = false; + b.slotCloneNode = false; + b.slotDomMutations = false; + b.slotTextContent = false; + b.hotModuleReplacement = false; +} diff --git a/src/testing/spec-page.ts b/packages/core/src/testing/spec-page.ts similarity index 80% rename from src/testing/spec-page.ts rename to packages/core/src/testing/spec-page.ts index fe42c30f588..355f15ae447 100644 --- a/src/testing/spec-page.ts +++ b/packages/core/src/testing/spec-page.ts @@ -1,4 +1,4 @@ -import { BUILD } from '@app-data'; +import { BUILD } from 'virtual:app-data'; import type { ComponentCompilerMeta, ComponentRuntimeMeta, @@ -7,7 +7,11 @@ import type { LazyBundlesRuntimeData, NewSpecPageOptions, SpecPage, -} from '@stencil/core/internal'; +} from '@stencil/core'; + +import { getBuildFeatures } from '../compiler/app-core/app-data'; +import { formatLazyBundleRuntimeMeta } from '../utils'; +import { CMP_FLAGS } from '../utils/constants'; import { bootstrapLazy, flushAll, @@ -19,15 +23,11 @@ import { registerModule, renderVdom, resetPlatform, - setSupportsShadowDom, startAutoApplyChanges, styles, win, writeTask, -} from '@stencil/core/internal/testing'; -import { formatLazyBundleRuntimeMeta } from '@utils'; - -import { getBuildFeatures } from '../compiler/app-core/app-data'; +} from './platform'; import { resetBuildConditionals } from './reset-build-conditionals'; /** @@ -59,14 +59,8 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise { } if (opts.hydrateServerSide) { opts.includeAnnotations = true; - setSupportsShadowDom(false); } else { opts.includeAnnotations = !!opts.includeAnnotations; - if (opts.supportsShadowDom === false) { - setSupportsShadowDom(false); - } else { - setSupportsShadowDom(true); - } } BUILD.cssAnnotations = opts.includeAnnotations; @@ -90,41 +84,43 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise { flushQueue: flushQueue, }; - const lazyBundles: LazyBundlesRuntimeData = opts.components.map((Cstr: ComponentTestingConstructor) => { - /** - * just pass through functional components that don't have styles nor any other metadata - */ - if (Cstr.COMPILER_META == null) { + const lazyBundles: LazyBundlesRuntimeData = opts.components.map( + (Cstr: ComponentTestingConstructor) => { /** - * the bundleId can be arbitrary, but must be unique + * just pass through functional components that don't have styles nor any other metadata */ - const arbitraryBundleId = `fc.${generateRandBundleId()}`; - return formatLazyBundleRuntimeMeta(arbitraryBundleId, []); - } + if (Cstr.COMPILER_META == null) { + /** + * the bundleId can be arbitrary, but must be unique + */ + const arbitraryBundleId = `fc.${generateRandBundleId()}`; + return formatLazyBundleRuntimeMeta(arbitraryBundleId, []); + } - cmpTags.add(Cstr.COMPILER_META.tagName); - Cstr.isProxied = false; - - proxyComponentLifeCycles(Cstr); - - const bundleId = `${Cstr.COMPILER_META.tagName}.${generateRandBundleId()}`; - const stylesMeta = Cstr.COMPILER_META.styles; - if (Array.isArray(stylesMeta)) { - if (stylesMeta.length > 1) { - const styles: any = {}; - stylesMeta.forEach((style) => { - styles[style.modeName] = style.styleStr; - }); - Cstr.style = styles; - } else if (stylesMeta.length === 1) { - Cstr.style = stylesMeta[0].styleStr; + cmpTags.add(Cstr.COMPILER_META.tagName); + Cstr.isProxied = false; + + proxyComponentLifeCycles(Cstr); + + const bundleId = `${Cstr.COMPILER_META.tagName}.${generateRandBundleId()}`; + const stylesMeta = Cstr.COMPILER_META.styles; + if (Array.isArray(stylesMeta)) { + if (stylesMeta.length > 1) { + const styleModes: any = {}; + stylesMeta.forEach((style) => { + styleModes[style.modeName] = style.styleStr; + }); + Cstr.style = styleModes; + } else if (stylesMeta.length === 1) { + Cstr.style = stylesMeta[0].styleStr; + } } - } - registerModule(bundleId, Cstr); + registerModule(bundleId, Cstr); - const lazyBundleRuntimeMeta = formatLazyBundleRuntimeMeta(bundleId, [Cstr.COMPILER_META]); - return lazyBundleRuntimeMeta; - }); + const lazyBundleRuntimeMeta = formatLazyBundleRuntimeMeta(bundleId, [Cstr.COMPILER_META]); + return lazyBundleRuntimeMeta; + }, + ); const cmpCompilerMeta = opts.components .filter((Cstr) => Cstr.COMPILER_META != null) @@ -147,10 +143,7 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise { BUILD.hydrateServerSide = true; BUILD.hydrateClientSide = false; } - BUILD.cloneNodeFix = false; - // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field - BUILD.shadowDomShim = false; - BUILD.attachStyles = !!opts.attachStyles; + BUILD.slotCloneNode = false; if (typeof opts.url === 'string') { page.win.location.href = opts.url; @@ -167,19 +160,29 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise { if (typeof opts.cookie === 'string') { try { page.doc.cookie = opts.cookie; - } catch (e) {} + } catch {} } if (typeof opts.referrer === 'string') { try { (page.doc as any).referrer = opts.referrer; - } catch (e) {} + } catch {} } if (typeof opts.userAgent === 'string') { try { (page.win.navigator as any).userAgent = opts.userAgent; - } catch (e) {} + } catch {} + } + + if (opts.hydrateServerSide && opts.serializeShadowRoot === 'scoped') { + for (const [, cmps] of lazyBundles) { + for (const cmp of cmps) { + if (cmp[0] & CMP_FLAGS.shadowDomEncapsulation) { + cmp[0] |= CMP_FLAGS.shadowNeedsScopedCss; + } + } + } } bootstrapLazy(lazyBundles); @@ -239,7 +242,9 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise { if (opts.autoApplyChanges) { startAutoApplyChanges(); page.waitForChanges = () => { - console.error('waitForChanges() cannot be used manually if the "startAutoApplyChanges" option is enabled'); + console.error( + 'waitForChanges() cannot be used manually if the "startAutoApplyChanges" option is enabled', + ); return Promise.resolve(); }; } @@ -268,6 +273,7 @@ function proxyComponentLifeCycles(Cstr: ComponentTestingConstructor): void { // the class should be in a known 'good' state to proxy functions if (typeof Cstr.prototype?.componentWillLoad === 'function') { Cstr.prototype.__componentWillLoad = Cstr.prototype.componentWillLoad; + /** @returns the result from the original componentWillLoad */ Cstr.prototype.componentWillLoad = function () { const result = this.__componentWillLoad(); if (result != null && typeof result.then === 'function') { @@ -281,6 +287,7 @@ function proxyComponentLifeCycles(Cstr: ComponentTestingConstructor): void { if (typeof Cstr.prototype?.componentWillUpdate === 'function') { Cstr.prototype.__componentWillUpdate = Cstr.prototype.componentWillUpdate; + /** @returns the result from the original componentWillUpdate */ Cstr.prototype.componentWillUpdate = function () { const result = this.__componentWillUpdate(); if (result != null && typeof result.then === 'function') { @@ -294,6 +301,7 @@ function proxyComponentLifeCycles(Cstr: ComponentTestingConstructor): void { if (typeof Cstr.prototype?.componentWillRender === 'function') { Cstr.prototype.__componentWillRender = Cstr.prototype.componentWillRender; + /** @returns the result from the original componentWillRender */ Cstr.prototype.componentWillRender = function () { const result = this.__componentWillRender(); if (result != null && typeof result.then === 'function') { diff --git a/src/testing/testing-logger.ts b/packages/core/src/testing/testing-logger.ts similarity index 98% rename from src/testing/testing-logger.ts rename to packages/core/src/testing/testing-logger.ts index 779d3210b7d..786c4532de1 100644 --- a/src/testing/testing-logger.ts +++ b/packages/core/src/testing/testing-logger.ts @@ -1,4 +1,4 @@ -import type { Diagnostic, Logger, LoggerTimeSpan, LogLevel } from '@stencil/core/internal'; +import type { Diagnostic, Logger, LoggerTimeSpan, LogLevel } from '@stencil/core'; export class TestingLogger implements Logger { private isEnabled = false; diff --git a/src/testing/testing-sys.ts b/packages/core/src/testing/testing-sys.ts similarity index 97% rename from src/testing/testing-sys.ts rename to packages/core/src/testing/testing-sys.ts index eabc111c279..55ca39808ef 100644 --- a/src/testing/testing-sys.ts +++ b/packages/core/src/testing/testing-sys.ts @@ -1,6 +1,6 @@ -import type { CompilerSystem } from '@stencil/core/internal'; import { createHash } from 'crypto'; import path from 'path'; +import type { CompilerSystem } from '@stencil/core'; import { createSystem } from '../compiler/sys/stencil-sys'; diff --git a/packages/core/src/testing/testing-utils.ts b/packages/core/src/testing/testing-utils.ts new file mode 100644 index 00000000000..969b17719ed --- /dev/null +++ b/packages/core/src/testing/testing-utils.ts @@ -0,0 +1,178 @@ +import { afterAll, Mock, vi } from 'vitest'; + +import { InMemoryFileSystem } from '../compiler/sys/in-memory-fs'; + +/** + * Shuffle an array using Fisher-Yates algorithm + * http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array + * + * @param array - the array to shuffle + * @returns the shuffled array + */ +export function shuffleArray(array: any[]) { + let currentIndex = array.length; + let temporaryValue: any; + let randomIndex: number; + + // While there remain elements to shuffle... + while (0 !== currentIndex) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; +} + +/** + * Utility for silencing `console` functions in tests. + * + * When this function is first called it grabs a reference to the `log`, + * `error`, and `warn` functions on `console` and then returns a per-test setup + * function which sets up a fresh set of mocks (via `jest.fn()`) and then + * assigns them to each of these functions. This setup function will return a + * reference to each of the three mock functions so tests can make assertions + * about their calls and so on. + * + * Because references to the original `.log`, `.error`, and `.warn` functions + * exist in closure within the function, it can use an `afterAll` call to clean + * up after itself and ensure that the original implementations are restored + * after the test suite finishes. + * + * An example of using this to silence log statements in a single test could look + * like this: + * + * ```ts + * describe("my-test-suite", () => { + * const { setupConsoleMocks, teardownConsoleMocks } = setupConsoleMocker() + * + * it("should log a message", () => { + * const { logMock } = setupConsoleMocks(); + * myFunctionWhichLogs(foo, bar); + * expect(logMock).toBeCalledWith('my log message'); + * teardownConsoleMocks(); + * }) + * }) + * ``` + * + * @returns a per-test mock setup function + */ +export function setupConsoleMocker(): ConsoleMocker { + const originalLog = console.log; + const originalWarn = console.warn; + const originalError = console.error; + + /** + * Function to tear down console mocks where you're done with them! Ideally + * this would be called right after the assertion you're looking to make in + * your test. + */ + function teardownConsoleMocks() { + console.log = originalLog; + console.warn = originalWarn; + console.error = originalError; + } + + // this is insurance! + afterAll(() => { + teardownConsoleMocks(); + }); + + function setupConsoleMocks() { + const logMock = vi.fn(); + const warnMock = vi.fn(); + const errorMock = vi.fn(); + + console.log = logMock; + console.warn = warnMock; + console.error = errorMock; + + return { + logMock, + warnMock, + errorMock, + }; + } + return { setupConsoleMocks, teardownConsoleMocks }; +} + +interface ConsoleMocker { + setupConsoleMocks: () => { + logMock: Mock; + warnMock: Mock; + errorMock: Mock; + }; + teardownConsoleMocks: () => void; +} + +/** + * the callback that `withSilentWarn` expects to receive. Basically receives a mock + * as its argument and returns a `Promise`, the value of which is returned by `withSilentWarn` + * as well. + */ +type SilentWarnFunc = (mock: Mock) => Promise; + +/** + * Wrap a single callback with a silent `console.warn`. The callback passed in + * receives the mocking function as an argument, so you can easily make assertions + * that it is called if necessary. + * + * @param cb a callback which `withSilentWarn` will call after replacing `console.warn` + * with a mock. + * @returns a Promise wrapping the return value of the callback + */ +export async function withSilentWarn(cb: SilentWarnFunc): Promise { + const realWarn = console.warn; + const warnMock = vi.fn(); + console.warn = warnMock; + const retVal = await cb(warnMock); + console.warn = realWarn; + return retVal; +} + +/** + * Testing utility to validate the existence of some provided file paths using a specific file system + * + * @param fs the file system to use to validate the existence of some files + * @param filePaths the paths to validate + * @throws when one or more of the provided file paths cannot be found + */ +export function expectFilesExist(fs: InMemoryFileSystem, filePaths: string[]): void { + const notFoundFiles: ReadonlyArray = filePaths.filter( + (filePath: string) => !fs.statSync(filePath).exists, + ); + + if (notFoundFiles.length > 0) { + throw new Error( + `The following files were expected, but could not be found:\n${notFoundFiles + .map((result: string) => '-' + result) + .join('\n')}`, + ); + } +} + +/** + * Testing utility to validate the non-existence of some provided file paths using a specific file system + * + * @param fs the file system to use to validate the non-existence of some files + * @param filePaths the paths to validate + * @throws when one or more of the provided file paths is found + */ +export function expectFilesDoNotExist(fs: InMemoryFileSystem, filePaths: string[]): void { + const existentFiles: ReadonlyArray = filePaths.filter( + (filePath: string) => fs.statSync(filePath).exists, + ); + + if (existentFiles.length > 0) { + throw new Error( + `The following files were expected to not exist, but do:\n${existentFiles + .map((result: string) => '-' + result) + .join('\n')}`, + ); + } +} diff --git a/packages/core/src/testing/vitest-stencil-plugin.ts b/packages/core/src/testing/vitest-stencil-plugin.ts new file mode 100644 index 00000000000..4d62bcf6e37 --- /dev/null +++ b/packages/core/src/testing/vitest-stencil-plugin.ts @@ -0,0 +1,71 @@ +import { transpileSync } from '@stencil/core/compiler'; + +interface VitePlugin { + name: string; + enforce?: 'pre' | 'post'; + transform?: (code: string, id: string) => { code: string; map?: any } | null; +} + +/** + * A Vite plugin that transforms Stencil components for use in Vitest. + * This replaces the Jest preprocessor functionality. + * + * @returns a Vite plugin configuration object + */ +export function stencilVitestPlugin(): VitePlugin { + return { + name: 'stencil-vitest-transform', + enforce: 'pre', + + transform(code, id) { + // Only transform .tsx files that contain Stencil decorators + if (!id.endsWith('.tsx')) { + return null; + } + + // Quick check for Stencil component patterns + const hasStencilDecorator = + code.includes('@Component') || + code.includes('@Prop') || + code.includes('@State') || + code.includes('@Event') || + code.includes('@Method') || + code.includes('@Watch') || + code.includes('@Listen'); + + if (!hasStencilDecorator) { + return null; + } + + const result = transpileSync(code, { + file: id, + componentExport: null, + componentMetadata: 'compilerstatic', + coreImportPath: '@stencil/core/testing', + currentDirectory: process.cwd(), + module: 'esm', + proxy: null, + sourceMap: false, + style: null, + styleImportData: 'queryparams', + target: 'es2022', + // Don't rewrite import paths - let Vite handle resolution via aliases + transformAliasedImportPaths: false, + }); + + const hasErrors = result.diagnostics?.some((d) => d.level === 'error'); + if (hasErrors) { + const messages = result.diagnostics + .filter((d) => d.level === 'error') + .map((d) => d.messageText) + .join('\n'); + throw new Error(`Stencil transform error in ${id}:\n${messages}`); + } + + return { + code: result.code, + map: result.map, + }; + }, + }; +} diff --git a/src/dev-server/test/Diagnostic.stub.ts b/packages/core/src/utils/_test_/fixtures/Diagnostic.stub.ts similarity index 93% rename from src/dev-server/test/Diagnostic.stub.ts rename to packages/core/src/utils/_test_/fixtures/Diagnostic.stub.ts index 783fe29cfd8..0a1ab63c3a9 100644 --- a/src/dev-server/test/Diagnostic.stub.ts +++ b/packages/core/src/utils/_test_/fixtures/Diagnostic.stub.ts @@ -1,4 +1,4 @@ -import * as d from '@stencil/core/declarations'; +import * as d from '@stencil/core'; /** * Generates a stub {@link d.Diagnostic}. This function uses sensible defaults for the initial stub. However, diff --git a/packages/core/src/utils/_test_/helpers.spec.ts b/packages/core/src/utils/_test_/helpers.spec.ts new file mode 100644 index 00000000000..c0c5711912c --- /dev/null +++ b/packages/core/src/utils/_test_/helpers.spec.ts @@ -0,0 +1,180 @@ +import { expect, describe, it } from '@stencil/vitest'; + +import { + dashToPascalCase, + escapeWithPattern, + isDef, + mergeIntoWith, + toCamelCase, + toDashCase, +} from '../helpers'; + +describe('util helpers', () => { + describe('dashToPascalCase', () => { + it('my-3d-component => My3dComponent', () => { + expect(dashToPascalCase('my-3d-component')).toBe('My3dComponent'); + }); + + it('madison-wisconsin => MadisonWisconsin', () => { + expect(dashToPascalCase('madison-wisconsin')).toBe('MadisonWisconsin'); + }); + + it('wisconsin => Wisconsin', () => { + expect(dashToPascalCase('wisconsin')).toBe('Wisconsin'); + }); + }); + + describe('toCamelCase', () => { + it.each([ + ['my-3d-component', 'my3dComponent'], + ['madison-wisconsin', 'madisonWisconsin'], + ['wisconsin', 'wisconsin'], + ])('%s => %s', (input: string, exp: string) => { + expect(toCamelCase(input)).toBe(exp); + }); + }); + + describe('toDashCase', () => { + it('My3dComponent => my-3d-component', () => { + expect(toDashCase('My3dComponent')).toBe('my-3d-component'); + }); + + it('MadisonWisconsin => madison-wisconsin', () => { + expect(toDashCase('MadisonWisconsin')).toBe('madison-wisconsin'); + }); + + it('madisonWisconsin => madison-wisconsin', () => { + expect(toDashCase('madisonWisconsin')).toBe('madison-wisconsin'); + }); + + it('Wisconsin => wisconsin', () => { + expect(toDashCase('Wisconsin')).toBe('wisconsin'); + }); + + it('wisconsin => wisconsin', () => { + expect(toDashCase('wisconsin')).toBe('wisconsin'); + }); + }); + + describe('isDef', () => { + it('number', () => { + expect(isDef(88)).toBe(true); + }); + + it('string', () => { + expect(isDef('str')).toBe(true); + }); + + it('object', () => { + expect(isDef({})).toBe(true); + }); + + it('array', () => { + expect(isDef([])).toBe(true); + }); + + it('false', () => { + expect(isDef(false)).toBe(true); + }); + + it('true', () => { + expect(isDef(true)).toBe(true); + }); + + it('undefined', () => { + expect(isDef(undefined)).toBe(false); + }); + + it('null', () => { + expect(isDef(null)).toBe(false); + }); + }); + + describe('mergeIntoWith', () => { + it('should do nothing if all elements already present', () => { + const target = [1, 2, 3]; + mergeIntoWith(target, [1, 2, 3], (x) => x); + expect(target).toEqual([1, 2, 3]); + }); + + it('should add new items', () => { + const target = [1, 2, 3]; + mergeIntoWith(target, [1, 2, 3, 4, 5], (x) => x); + expect(target).toEqual([1, 2, 3, 4, 5]); + }); + + it('should merge in objects using the predicate', () => { + const target = [{ id: 'foo' }, { id: 'bar' }, { id: 'boz' }, { id: 'baz' }]; + mergeIntoWith(target, [{ id: 'foo' }, { id: 'fab' }, { id: 'fib' }], (x) => x.id); + expect(target).toEqual([ + { id: 'foo' }, + { id: 'bar' }, + { id: 'boz' }, + { id: 'baz' }, + { id: 'fab' }, + { id: 'fib' }, + ]); + }); + }); + + describe('escapeWithPattern', () => { + it('replaces all occurrences of a string pattern by default', () => { + const text = 'foo/bar foo/bar foo/bar'; + const pattern = '/'; + const replacement = '\\/'; + expect(escapeWithPattern(text, pattern, replacement)).toBe('foo\\/bar foo\\/bar foo\\/bar'); + }); + + it('replaces only first occurrence if replaceAll is false', () => { + const text = 'foo/bar foo/bar foo/bar'; + const pattern = '/'; + const replacement = '\\/'; + expect(escapeWithPattern(text, pattern, replacement, false)).toBe( + 'foo\\/bar foo/bar foo/bar', + ); + }); + + it('replaces all occurrences using a RegExp pattern with no g flag by default', () => { + const text = 'a+b+c+a+b+c'; + const pattern = /\+/; // no 'g' flag + const replacement = '-'; + expect(escapeWithPattern(text, pattern, replacement)).toBe('a-b-c-a-b-c'); + }); + + it('replaces only first occurrence if replaceAll is false with RegExp', () => { + const text = 'a+b+c+a+b+c'; + const pattern = /\+/; + const replacement = '-'; + expect(escapeWithPattern(text, pattern, replacement, false)).toBe('a-b+c+a+b+c'); + }); + + it('respects the g flag if RegExp already has it and replaceAll true', () => { + const text = 'x*y*z*x*y*z'; + const pattern = /\*/g; + const replacement = '-'; + expect(escapeWithPattern(text, pattern, replacement, true)).toBe('x-y-z-x-y-z'); + }); + + it('removes the g flag if replaceAll is false', () => { + const text = 'x*y*z*x*y*z'; + const pattern = /\*/g; + const replacement = '-'; + expect(escapeWithPattern(text, pattern, replacement, false)).toBe('x-y*z*x*y*z'); + }); + + it('escapes special RegExp chars in string pattern', () => { + const text = 'foo.*+?^${}()|[]\\bar'; + const pattern = '.*+?^${}()|[]\\'; + const replacement = '-ESCAPED-'; + expect(escapeWithPattern(text, pattern, replacement)).toBe('foo-ESCAPED-bar'); + }); + + it('works with empty string input', () => { + expect(escapeWithPattern('', 'a', 'b')).toBe(''); + }); + + it('works with empty replacement', () => { + expect(escapeWithPattern('abcabc', 'a', '')).toBe('bcbc'); + }); + }); +}); diff --git a/src/utils/test/is-root-path.spec.ts b/packages/core/src/utils/_test_/is-root-path.spec.ts similarity index 94% rename from src/utils/test/is-root-path.spec.ts rename to packages/core/src/utils/_test_/is-root-path.spec.ts index bd61510f20d..8dbce44f80b 100644 --- a/src/utils/test/is-root-path.spec.ts +++ b/packages/core/src/utils/_test_/is-root-path.spec.ts @@ -1,3 +1,5 @@ +import { expect, describe, it } from '@stencil/vitest'; + import { isRootPath } from '../is-root-path'; describe('isRootPath', () => { diff --git a/src/utils/test/message-utils.spec.ts b/packages/core/src/utils/_test_/message-utils.spec.ts similarity index 98% rename from src/utils/test/message-utils.spec.ts rename to packages/core/src/utils/_test_/message-utils.spec.ts index bb08200664d..c0bf5f836a7 100644 --- a/src/utils/test/message-utils.spec.ts +++ b/packages/core/src/utils/_test_/message-utils.spec.ts @@ -1,4 +1,6 @@ -import type * as d from '../../declarations'; +import { expect, describe, it, beforeEach } from '@stencil/vitest'; +import type * as d from '@stencil/core'; + import { catchError } from '../message-utils'; describe('message-utils', () => { diff --git a/src/utils/test/output-target.spec.ts b/packages/core/src/utils/_test_/output-target.spec.ts similarity index 80% rename from src/utils/test/output-target.spec.ts rename to packages/core/src/utils/_test_/output-target.spec.ts index cae9556060b..d9d994181bf 100644 --- a/src/utils/test/output-target.spec.ts +++ b/packages/core/src/utils/_test_/output-target.spec.ts @@ -1,20 +1,23 @@ -import type * as d from '../../declarations'; -import type { EligiblePrimaryPackageOutputTarget } from '../../declarations'; -import { DIST_TYPES, VALID_CONFIG_OUTPUT_TARGETS } from '../constants'; +import { expect, describe, it, vi } from '@stencil/vitest'; +import type * as d from '@stencil/core'; + +import { VALID_CONFIG_OUTPUT_TARGETS } from '../constants'; import { filterExcludedComponents, - isEligiblePrimaryPackageOutputTarget, isValidConfigOutputTarget, shouldExcludeComponent, } from '../output-target'; describe('output-utils tests', () => { describe('isValidConfigOutputTarget', () => { - it.each(VALID_CONFIG_OUTPUT_TARGETS)('should return true for valid output type "%s"', (outputTargetType) => { - expect(isValidConfigOutputTarget(outputTargetType)).toBe(true); - }); + it.each(VALID_CONFIG_OUTPUT_TARGETS)( + 'should return true for valid output type "%s"', + (outputTargetType) => { + expect(isValidConfigOutputTarget(outputTargetType)).toBe(true); + }, + ); - it.each(['', 'my-target-that-i-made-up', DIST_TYPES])( + it.each(['', 'my-target-that-i-made-up', 'invalid-output-type'])( 'should return false for invalid config output type "%s"', (outputTargetType) => { expect(isValidConfigOutputTarget(outputTargetType)).toBe(false); @@ -22,35 +25,6 @@ describe('output-utils tests', () => { ); }); - describe('isEligiblePrimaryPackageOutputTarget', () => { - it.each<(typeof VALID_CONFIG_OUTPUT_TARGETS)[number]>([ - 'copy', - 'custom', - 'dist-hydrate-script', - 'www', - 'stats', - 'docs-json', - 'docs-readme', - 'docs-vscode', - 'docs-custom', - ])('should return false for $type', (outputTarget) => { - const res = isEligiblePrimaryPackageOutputTarget({ type: outputTarget } as any); - - expect(res).toBe(false); - }); - - it.each([ - 'dist', - 'dist-collection', - 'dist-custom-elements', - 'dist-types', - ])('should return true for `$type`', (outputTarget) => { - const res = isEligiblePrimaryPackageOutputTarget({ type: outputTarget } as any); - - expect(res).toBe(true); - }); - }); - describe('shouldExcludeComponent', () => { it('should return false when no patterns provided', () => { expect(shouldExcludeComponent('my-component', undefined)).toBe(false); @@ -102,7 +76,7 @@ describe('output-utils tests', () => { return { excludeComponents: excludePatterns, logger: { - debug: jest.fn(), + debug: vi.fn(), }, } as any as d.ValidatedConfig; }; @@ -131,7 +105,7 @@ describe('output-utils tests', () => { const config = { ...createMockConfig(['demo-widget']), devMode: false, - logger: { debug: jest.fn(), info: jest.fn() }, + logger: { debug: vi.fn(), info: vi.fn() }, } as any; const result = filterExcludedComponents(components, config); @@ -152,7 +126,7 @@ describe('output-utils tests', () => { const config = { ...createMockConfig(['demo-*']), devMode: false, - logger: { debug: jest.fn(), info: jest.fn() }, + logger: { debug: vi.fn(), info: vi.fn() }, } as any; const result = filterExcludedComponents(components, config); @@ -173,7 +147,7 @@ describe('output-utils tests', () => { const config = { ...createMockConfig(['demo-*', '*-test', 'specific-exclude']), devMode: false, - logger: { debug: jest.fn(), info: jest.fn() }, + logger: { debug: vi.fn(), info: vi.fn() }, } as any; const result = filterExcludedComponents(components, config); @@ -188,12 +162,14 @@ describe('output-utils tests', () => { const config = { ...createMockConfig(['demo-*']), devMode: false, - logger: { debug: jest.fn(), info: jest.fn() }, + logger: { debug: vi.fn(), info: vi.fn() }, } as any; filterExcludedComponents(components, config); - expect(config.logger.debug).toHaveBeenCalledWith('Excluding component from build: demo-widget'); + expect(config.logger.debug).toHaveBeenCalledWith( + 'Excluding component from build: demo-widget', + ); }); it('should return empty array when all components are excluded', () => { @@ -201,7 +177,7 @@ describe('output-utils tests', () => { const config = { ...createMockConfig(['demo-*']), devMode: false, - logger: { debug: jest.fn(), info: jest.fn() }, + logger: { debug: vi.fn(), info: vi.fn() }, } as any; const result = filterExcludedComponents(components, config); @@ -220,8 +196,8 @@ describe('output-utils tests', () => { ...createMockConfig(['demo-*']), devMode: false, logger: { - debug: jest.fn(), - info: jest.fn(), + debug: vi.fn(), + info: vi.fn(), }, } as any as d.ValidatedConfig; @@ -238,14 +214,16 @@ describe('output-utils tests', () => { ...createMockConfig(['demo-widget']), devMode: false, logger: { - debug: jest.fn(), - info: jest.fn(), + debug: vi.fn(), + info: vi.fn(), }, } as any as d.ValidatedConfig; filterExcludedComponents(components, config); - expect(config.logger.info).toHaveBeenCalledWith('Excluding 1 component from production build: demo-widget'); + expect(config.logger.info).toHaveBeenCalledWith( + 'Excluding 1 component from production build: demo-widget', + ); }); it('should not exclude components in dev mode', () => { @@ -254,8 +232,8 @@ describe('output-utils tests', () => { ...createMockConfig(['demo-widget']), devMode: true, logger: { - debug: jest.fn(), - info: jest.fn(), + debug: vi.fn(), + info: vi.fn(), }, } as any as d.ValidatedConfig; @@ -275,8 +253,8 @@ describe('output-utils tests', () => { ...createMockConfig(['demo-*']), devMode: false, logger: { - debug: jest.fn(), - info: jest.fn(), + debug: vi.fn(), + info: vi.fn(), }, } as any as d.ValidatedConfig; diff --git a/src/utils/test/path.spec.ts b/packages/core/src/utils/_test_/path.spec.ts similarity index 99% rename from src/utils/test/path.spec.ts rename to packages/core/src/utils/_test_/path.spec.ts index a0697b75945..e3ca7aa6167 100644 --- a/src/utils/test/path.spec.ts +++ b/packages/core/src/utils/_test_/path.spec.ts @@ -1,3 +1,5 @@ +import { expect, describe, it } from '@stencil/vitest'; + import { join, normalize, normalizeFsPathQuery, normalizePath, relative, resolve } from '../path'; describe('normalizePath', () => { diff --git a/src/utils/test/query-nonce-meta-tag-content.spec.ts b/packages/core/src/utils/_test_/query-nonce-meta-tag-content.spec.ts similarity index 93% rename from src/utils/test/query-nonce-meta-tag-content.spec.ts rename to packages/core/src/utils/_test_/query-nonce-meta-tag-content.spec.ts index 820a3585655..785e74e0498 100644 --- a/src/utils/test/query-nonce-meta-tag-content.spec.ts +++ b/packages/core/src/utils/_test_/query-nonce-meta-tag-content.spec.ts @@ -1,6 +1,16 @@ +// @vitest-environment stencil + +import { expect, describe, it } from '@stencil/vitest'; + import { queryNonceMetaTagContent } from '../query-nonce-meta-tag-content'; describe('queryNonceMetaTagContent', () => { + it('should return `undefined` if the tag does not exist', () => { + const nonce = queryNonceMetaTagContent(document); + + expect(nonce).toEqual(undefined); + }); + it('should return the nonce value if the tag exists', () => { const meta = document.createElement('meta'); meta.setAttribute('name', 'csp-nonce'); @@ -12,12 +22,6 @@ describe('queryNonceMetaTagContent', () => { expect(nonce).toEqual('1234'); }); - it('should return `undefined` if the tag does not exist', () => { - const nonce = queryNonceMetaTagContent(document); - - expect(nonce).toEqual(undefined); - }); - it('should return `undefined` if the document does not have a head element', () => { const head = document.querySelector('head'); head?.remove(); diff --git a/src/utils/test/regular-expression.spec.ts b/packages/core/src/utils/_test_/regular-expression.spec.ts similarity index 83% rename from src/utils/test/regular-expression.spec.ts rename to packages/core/src/utils/_test_/regular-expression.spec.ts index d2b8a112f2c..7e05ebb74d2 100644 --- a/src/utils/test/regular-expression.spec.ts +++ b/packages/core/src/utils/_test_/regular-expression.spec.ts @@ -1,10 +1,13 @@ +import { expect, describe, it } from '@stencil/vitest'; + import { escapeRegExpSpecialCharacters } from '../regular-expression'; describe('regular expression utils', () => { describe('escapeRegExpSpecialCharacters', () => { it('should escape all special characters', () => { const text = 'This is a string with special characters: $ ^ * + ? . ( ) | { } [ ]'; - const expected = 'This is a string with special characters: \\$ \\^ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]'; + const expected = + 'This is a string with special characters: \\$ \\^ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]'; const result = escapeRegExpSpecialCharacters(text); expect(result).toEqual(expected); }); diff --git a/src/utils/test/result.spec.ts b/packages/core/src/utils/_test_/result.spec.ts similarity index 95% rename from src/utils/test/result.spec.ts rename to packages/core/src/utils/_test_/result.spec.ts index a1f7f5a7e03..7af7a97faf4 100644 --- a/src/utils/test/result.spec.ts +++ b/packages/core/src/utils/_test_/result.spec.ts @@ -1,3 +1,5 @@ +import { expect, describe, it } from '@stencil/vitest'; + import * as result from '../result'; describe('Result type and utility functions', () => { diff --git a/src/utils/test/scope-css.spec.ts b/packages/core/src/utils/_test_/scope-css.spec.ts similarity index 97% rename from src/utils/test/scope-css.spec.ts rename to packages/core/src/utils/_test_/scope-css.spec.ts index bf9f4a21b9e..75a8e0161ef 100644 --- a/src/utils/test/scope-css.spec.ts +++ b/packages/core/src/utils/_test_/scope-css.spec.ts @@ -1,8 +1,8 @@ +import { expect, describe, it } from '@stencil/vitest'; /** * Tests modified from Angular shadow_css tests * https://github.com/angular/angular/blob/0f5c70d563b6943623a5940036a52fe077ad3fac/packages/compiler/test/shadow_css_spec.ts */ - /** * @license * Copyright Google Inc. All Rights Reserved. @@ -23,7 +23,7 @@ describe('scopeCSS', function () { .replace(/:\s/g, ':') .replace(/'/g, '"') .replace(/ }/g, '}') - .replace(/url\((\"|\s)(.+)(\"|\s)\)(\s*)/g, (...match: string[]) => `url("${match[2]}")`) + .replace(/url\(("|\s)(.+)("|\s)\)(\s*)/g, (...match: string[]) => `url("${match[2]}")`) .replace(/\[(.+)=([^"\]]+)\]/g, (...match: string[]) => `[${match[1]}="${match[2]}"]`); } @@ -79,7 +79,8 @@ describe('scopeCSS', function () { it('should handle media rules', () => { const css = '@media screen and (max-width:800px, max-height:100%) {div {font-size:50px;}}'; - const expected = '@media screen and (max-width:800px, max-height:100%) {div.a {font-size:50px;}}'; + const expected = + '@media screen and (max-width:800px, max-height:100%) {div.a {font-size:50px;}}'; expect(s(css, 'a')).toEqual(expected); }); @@ -121,7 +122,7 @@ describe('scopeCSS', function () { it('should perform relative fast', () => { const now = Date.now(); scopeCss(exampleComponentCss, 'a', true); - expect(Date.now() - now).toBeLessThan(200); + expect(Date.now() - now).toBeLessThan(250); }); it('should handle complicated selectors', () => { @@ -220,11 +221,15 @@ describe('scopeCSS', function () { }); it('should not replace the selector in a `@supports` rule', () => { - expect(s('@supports selector(:host-context(.class1)) {:host-context(.class1) {color: red; }}', 'a')).toEqual( + expect( + s( + '@supports selector(:host-context(.class1)) {:host-context(.class1) {color: red; }}', + 'a', + ), + ).toEqual( '@supports selector(:host-context(.class1)) {.class1.a-h, .class1 .a-h {color:red;}}', ); }); - ``; }); describe('::slotted', () => { @@ -274,7 +279,9 @@ describe('scopeCSS', function () { it('should combine parent selector', () => { const r = s('div{} .a .b .c ::slotted(*) {}', 'sc-ion-tag'); - expect(r).toEqual('div.sc-ion-tag{} .a .b .c.sc-ion-tag-s > *, .a .b .c .sc-ion-tag-s > * {}'); + expect(r).toEqual( + 'div.sc-ion-tag{} .a .b .c.sc-ion-tag-s > *, .a .b .c .sc-ion-tag-s > * {}', + ); }); it('same selectors', () => { @@ -284,7 +291,9 @@ describe('scopeCSS', function () { it('should combine parent selector when comma', () => { const r = s('.a .b, .c ::slotted(*) {}', 'sc-ion-tag'); - expect(r).toEqual('.a.sc-ion-tag .b.sc-ion-tag, .c.sc-ion-tag-s > *, .c .sc-ion-tag-s > * {}'); + expect(r).toEqual( + '.a.sc-ion-tag .b.sc-ion-tag, .c.sc-ion-tag-s > *, .c .sc-ion-tag-s > * {}', + ); }); it('should not replace the selector in a `@supports` rule', () => { @@ -339,8 +348,12 @@ describe('scopeCSS', function () { }); it('should keep sourceMappingURL comments', () => { - expect(s('b {c}/*# sourceMappingURL=data:x */', 'a')).toEqual('b.a {c}/*# sourceMappingURL=data:x */'); - expect(s('b {c}/* #sourceMappingURL=data:x */', 'a')).toEqual('b.a {c}/* #sourceMappingURL=data:x */'); + expect(s('b {c}/*# sourceMappingURL=data:x */', 'a')).toEqual( + 'b.a {c}/*# sourceMappingURL=data:x */', + ); + expect(s('b {c}/* #sourceMappingURL=data:x */', 'a')).toEqual( + 'b.a {c}/* #sourceMappingURL=data:x */', + ); }); describe('expands ::part selectors', () => { @@ -349,14 +362,20 @@ describe('scopeCSS', function () { expect(r).toEqual('.sc-ion-tag::part(part), .sc-ion-tag [part~="part"] {}'); }); it('complex', () => { - const r = s(':where(something something-else) > .something::part(part part2):hover::after {}', 'sc-ion-tag'); + const r = s( + ':where(something something-else) > .something::part(part part2):hover::after {}', + 'sc-ion-tag', + ); expect(r).toEqual( '.sc-ion-tag:where(something something-else) > .something.sc-ion-tag::part(part part2):hover::after, .sc-ion-tag:where(something something-else) > .something.sc-ion-tag [part~="part"][part~="part2"]:hover::after {}', ); }); it('ignores already processed', () => { - const r = s('.something::part(part part2), .something [part~="part"][part~="part2"] {}', 'sc-ion-tag'); + const r = s( + '.something::part(part part2), .something [part~="part"][part~="part2"] {}', + 'sc-ion-tag', + ); expect(r).toEqual( '.something.sc-ion-tag::part(part part2), .something.sc-ion-tag [part~="part"][part~="part2"] {}', ); diff --git a/src/utils/test/sourcemaps.spec.ts b/packages/core/src/utils/_test_/sourcemaps.spec.ts similarity index 87% rename from src/utils/test/sourcemaps.spec.ts rename to packages/core/src/utils/_test_/sourcemaps.spec.ts index 85c871cdbeb..588fc8943b4 100644 --- a/src/utils/test/sourcemaps.spec.ts +++ b/packages/core/src/utils/_test_/sourcemaps.spec.ts @@ -1,29 +1,30 @@ +import { expect, describe, it } from '@stencil/vitest'; +import type * as d from '@stencil/core'; +import type { SourceMap as RolldownSourceMap } from 'rolldown'; + import { getInlineSourceMappingUrlLinker, getSourceMappingUrlForEndOfFile, getSourceMappingUrlLinker, - rollupToStencilSourceMap, -} from '@utils'; -import { SourceMap as RollupSourceMap } from 'rollup'; - -import type * as d from '../../declarations'; + rolldownToStencilSourceMap, +} from '../index'; describe('sourcemaps', () => { - describe('rollupToStencilSourceMap', () => { + describe('rolldownToStencilSourceMap', () => { it('returns null if the given sourcemap is null', () => { - expect(rollupToStencilSourceMap(null)).toBeNull(); + expect(rolldownToStencilSourceMap(null)).toBeNull(); }); it('returns null if the given sourcemap is undefined', () => { - expect(rollupToStencilSourceMap(undefined)).toBeNull(); + expect(rolldownToStencilSourceMap(undefined)).toBeNull(); }); it('returns null if the given sourcemap has no file', () => { - expect(rollupToStencilSourceMap({ sourcesContent: [] } as RollupSourceMap)).toBeNull(); + expect(rolldownToStencilSourceMap({ sourcesContent: [] } as RolldownSourceMap)).toBeNull(); }); - it('transforms a rollup sourcemap to a stencil sourcemap', () => { - const rollupSourceMap: RollupSourceMap = { + it('transforms a rolldown sourcemap to a stencil sourcemap', () => { + const rolldownSourceMap: RolldownSourceMap = { file: 'index.js', mappings: ';;AAAA;AAC9D,GAAG,CAAC,CAAC;AACL;;;;', names: ['bootstrapLazy'], @@ -36,7 +37,7 @@ describe('sourcemaps', () => { toUrl: () => 'stub', }; - const stencilSourceMap = rollupToStencilSourceMap(rollupSourceMap); + const stencilSourceMap = rolldownToStencilSourceMap(rolldownSourceMap); const expectedSourceMap: d.SourceMap = { file: 'index.js', @@ -90,7 +91,9 @@ describe('sourcemaps', () => { }); it('encodes multiple disallowed characters at once', () => { - expect(getSourceMappingUrlLinker('!some-(pkg)*')).toBe('//# sourceMappingURL=%21some-%28pkg%29%2a'); + expect(getSourceMappingUrlLinker('!some-(pkg)*')).toBe( + '//# sourceMappingURL=%21some-%28pkg%29%2a', + ); }); }); @@ -219,43 +222,63 @@ describe('sourcemaps', () => { describe('getSourceMappingUrlLinkerWithNewline', () => { it('returns a correctly formatted url', () => { - expect(getSourceMappingUrlForEndOfFile('some-pkg')).toBe('\n//# sourceMappingURL=some-pkg.map'); + expect(getSourceMappingUrlForEndOfFile('some-pkg')).toBe( + '\n//# sourceMappingURL=some-pkg.map', + ); }); it('handles question marks in URLs', () => { - expect(getSourceMappingUrlForEndOfFile('some-pkg?')).toBe('\n//# sourceMappingURL=some-pkg%3F.map'); + expect(getSourceMappingUrlForEndOfFile('some-pkg?')).toBe( + '\n//# sourceMappingURL=some-pkg%3F.map', + ); }); it('handles equal signs in URLs', () => { - expect(getSourceMappingUrlForEndOfFile('some-pkg=')).toBe('\n//# sourceMappingURL=some-pkg%3D.map'); + expect(getSourceMappingUrlForEndOfFile('some-pkg=')).toBe( + '\n//# sourceMappingURL=some-pkg%3D.map', + ); }); it('handles ampersands in URLs', () => { - expect(getSourceMappingUrlForEndOfFile('some-pkg&')).toBe('\n//# sourceMappingURL=some-pkg%26.map'); + expect(getSourceMappingUrlForEndOfFile('some-pkg&')).toBe( + '\n//# sourceMappingURL=some-pkg%26.map', + ); }); it('handles slashes in URLs', () => { - expect(getSourceMappingUrlForEndOfFile('some-pkg/')).toBe('\n//# sourceMappingURL=some-pkg%2F.map'); + expect(getSourceMappingUrlForEndOfFile('some-pkg/')).toBe( + '\n//# sourceMappingURL=some-pkg%2F.map', + ); }); it('handles exclamation points in URLs', () => { - expect(getSourceMappingUrlForEndOfFile('some-pkg!')).toBe('\n//# sourceMappingURL=some-pkg%21.map'); + expect(getSourceMappingUrlForEndOfFile('some-pkg!')).toBe( + '\n//# sourceMappingURL=some-pkg%21.map', + ); }); it('handles single quotes in URLs', () => { - expect(getSourceMappingUrlForEndOfFile("some-'pkg'")).toBe('\n//# sourceMappingURL=some-%27pkg%27.map'); + expect(getSourceMappingUrlForEndOfFile("some-'pkg'")).toBe( + '\n//# sourceMappingURL=some-%27pkg%27.map', + ); }); it('handles parenthesis in URLs', () => { - expect(getSourceMappingUrlForEndOfFile('some-(pkg)')).toBe('\n//# sourceMappingURL=some-%28pkg%29.map'); + expect(getSourceMappingUrlForEndOfFile('some-(pkg)')).toBe( + '\n//# sourceMappingURL=some-%28pkg%29.map', + ); }); it('handles asterisks in URLs', () => { - expect(getSourceMappingUrlForEndOfFile('some-pkg*')).toBe('\n//# sourceMappingURL=some-pkg%2a.map'); + expect(getSourceMappingUrlForEndOfFile('some-pkg*')).toBe( + '\n//# sourceMappingURL=some-pkg%2a.map', + ); }); it('encodes multiple disallowed characters at once', () => { - expect(getSourceMappingUrlForEndOfFile('!some-(pkg)*')).toBe('\n//# sourceMappingURL=%21some-%28pkg%29%2a.map'); + expect(getSourceMappingUrlForEndOfFile('!some-(pkg)*')).toBe( + '\n//# sourceMappingURL=%21some-%28pkg%29%2a.map', + ); }); }); }); diff --git a/src/utils/test/url-paths.spec.ts b/packages/core/src/utils/_test_/url-paths.spec.ts similarity index 79% rename from src/utils/test/url-paths.spec.ts rename to packages/core/src/utils/_test_/url-paths.spec.ts index 9e05855616b..1a019047ee4 100644 --- a/src/utils/test/url-paths.spec.ts +++ b/packages/core/src/utils/_test_/url-paths.spec.ts @@ -1,3 +1,5 @@ +import { expect, describe, it } from '@stencil/vitest'; + import { isRemoteUrl } from '../url-paths'; describe('url-paths', () => { @@ -16,9 +18,12 @@ describe('url-paths', () => { }, ); - it.each(['C:/file.txt', 'C:\\file.txt', '/User/file.txt'])("returns false for file paths '%s'", (fileName) => { - expect(isRemoteUrl(fileName)).toBe(false); - }); + it.each(['C:/file.txt', 'C:\\file.txt', '/User/file.txt'])( + "returns false for file paths '%s'", + (fileName) => { + expect(isRemoteUrl(fileName)).toBe(false); + }, + ); it('returns false if the provided url is an empty string', () => { expect(isRemoteUrl('')).toBe(false); diff --git a/packages/core/src/utils/_test_/util.spec.ts b/packages/core/src/utils/_test_/util.spec.ts new file mode 100644 index 00000000000..2e92adb7bc8 --- /dev/null +++ b/packages/core/src/utils/_test_/util.spec.ts @@ -0,0 +1,377 @@ +import { mockBuildCtx, mockValidatedConfig } from '@stencil/core/testing'; +import { expect, describe, it, beforeEach } from '@stencil/vitest'; +import type * as d from '@stencil/core'; + +import * as util from '../index'; +import { getTextDocs } from '../util'; +import { stubDiagnostic } from './fixtures/Diagnostic.stub'; + +describe('util', () => { + describe('generatePreamble', () => { + it('generates a comment with a single line preamble', () => { + const testConfig = mockValidatedConfig({ preamble: 'I am Stencil' }); + + const result = util.generatePreamble(testConfig); + + expect(result).toBe(`/*! + * I am Stencil + */`); + }); + + it('generates a comment with a multi-line preamble', () => { + const testConfig = mockValidatedConfig({ preamble: 'I am Stencil\nHear me roar' }); + + const result = util.generatePreamble(testConfig); + + expect(result).toBe(`/*! + * I am Stencil + * Hear me roar + */`); + }); + + it('returns an empty string if no preamble is provided', () => { + const testConfig = mockValidatedConfig(); + + const result = util.generatePreamble(testConfig); + + expect(result).toBe(''); + }); + + it('returns an empty string a null preamble is provided', () => { + const testConfig = mockValidatedConfig({ preamble: undefined }); + + const result = util.generatePreamble(testConfig); + + expect(result).toBe(''); + }); + + it('returns an empty string if an empty preamble is provided', () => { + const testConfig = mockValidatedConfig({ preamble: '' }); + + const result = util.generatePreamble(testConfig); + + expect(result).toBe(''); + }); + }); + + describe('hasDependency', () => { + let buildCtx: d.BuildCtx; + + beforeEach(() => { + buildCtx = mockBuildCtx(); + }); + + it("returns false when the packageJson field isn't set on the build context", () => { + // this cast is done to specifically test the case where this is not the + // expected type + buildCtx.packageJson = null as unknown as d.PackageJsonData; + + expect(util.hasDependency(buildCtx, 'a-non-existent-pkg')).toBe(false); + }); + + it('returns false if a project has no dependencies', () => { + buildCtx.packageJson.dependencies = undefined; + + expect(util.hasDependency(buildCtx, 'a-non-existent-pkg')).toBe(false); + }); + + it('returns false if a project has an empty list of dependencies', () => { + buildCtx.packageJson.dependencies = {}; + + expect(util.hasDependency(buildCtx, 'a-non-existent-pkg')).toBe(false); + }); + + it("returns false for '@stencil/core'", () => { + buildCtx.packageJson.dependencies = { '@stencil/core': '2.0.0' }; + + expect(util.hasDependency(buildCtx, '@stencil/core')).toBe(false); + }); + + it('returns true for a dependency match', () => { + buildCtx.packageJson.dependencies = { + 'existent-pkg1': '1.0.0', + 'existent-pkg2': '2.0.0', + 'existent-pkg3': '3.0.0', + }; + + expect(util.hasDependency(buildCtx, 'existent-pkg2')).toBe(true); + }); + + it('is case sensitive in its lookup', () => { + buildCtx.packageJson.dependencies = { + 'existent-pkg1': '1.0.0', + 'existent-pkg2': '2.0.0', + 'existent-pkg3': '3.0.0', + }; + + expect(util.hasDependency(buildCtx, 'EXISTENT-PKG2')).toBe(false); + }); + }); + + describe('isDtsFile', () => { + it('should return true for .d.ts files', () => { + expect(util.isDtsFile('.d.ts')).toEqual(true); + expect(util.isDtsFile('foo.d.ts')).toEqual(true); + expect(util.isDtsFile('foo/bar.d.ts')).toEqual(true); + }); + + it('should return false for all other file types', () => { + expect(util.isDtsFile('.k.ts')).toEqual(false); + expect(util.isDtsFile('foo.d.doc')).toEqual(false); + expect(util.isDtsFile('foo/bar.txt')).toEqual(false); + expect(util.isDtsFile('foo.spec.ts')).toEqual(false); + }); + + it('should be case insensitive', () => { + expect(util.isDtsFile('foo/bar.D.tS')).toEqual(true); + }); + }); + + it('createJsVarName', () => { + expect( + util.createJsVarName('./scoped-style-import.css?tag=my-button&encapsulation=scoped'), + ).toBe('scopedStyleImportCss'); + expect(util.createJsVarName('./scoped-style-import.css#hash')).toBe('scopedStyleImportCss'); + expect(util.createJsVarName('./scoped-style-import.css&data')).toBe('scopedStyleImportCss'); + expect(util.createJsVarName('./scoped-style-import.css=data')).toBe('scopedStyleImportCss'); + expect(util.createJsVarName('@ionic/core')).toBe('ionicCore'); + expect(util.createJsVarName('@ionic\\core')).toBe('ionicCore'); + expect(util.createJsVarName('88mph')).toBe('_88mph'); + expect(util.createJsVarName('Doc.brown&')).toBe('docBrown'); + expect(util.createJsVarName(' Doc! Brown? ')).toBe('docBrown'); + expect(util.createJsVarName('doc---Brown')).toBe('docBrown'); + expect(util.createJsVarName('doc-brown')).toBe('docBrown'); + expect(util.createJsVarName('DocBrown')).toBe('docBrown'); + expect(util.createJsVarName('Doc')).toBe('doc'); + expect(util.createJsVarName('doc')).toBe('doc'); + expect(util.createJsVarName('AB')).toBe('aB'); + expect(util.createJsVarName('Ab')).toBe('ab'); + expect(util.createJsVarName('a')).toBe('a'); + expect(util.createJsVarName('A')).toBe('a'); + expect(util.createJsVarName(' ')).toBe(''); + expect(util.createJsVarName('')).toBe(''); + }); + + describe('parsePackageJson', () => { + const mockPackageJsonPath = '/mock/path/package.json'; + + it('returns a parse error if parsing cannot complete', () => { + // improperly formatted JSON - note the lack of ':' + const diagnostic = util.parsePackageJson('{ "someJson" "value"}', mockPackageJsonPath); + + const expectedDiagnostic: d.Diagnostic = stubDiagnostic({ + absFilePath: mockPackageJsonPath, + header: 'Error Parsing JSON', + messageText: expect.stringMatching(/.*in JSON at position 13/), + type: 'build', + }); + + expect(diagnostic).toEqual({ + diagnostic: expectedDiagnostic, + data: null, + filePath: mockPackageJsonPath, + }); + }); + + it('returns the parsed data from the provided json', () => { + const diagnostic = util.parsePackageJson('{ "someJson": "value"}', mockPackageJsonPath); + + expect(diagnostic).toEqual({ + diagnostic: null, + data: { + someJson: 'value', + }, + filePath: mockPackageJsonPath, + }); + }); + }); + + describe('addDocBlock', () => { + let str: string; + let docs: d.CompilerJsDoc; + + beforeEach(() => { + str = 'interface Foo extends Components.Foo, HTMLStencilElement {'; + docs = { + tags: [{ name: 'deprecated', text: 'only for testing' }], + text: 'Lorem ipsum', + }; + }); + + it('adds a doc block to the string', () => { + expect(util.addDocBlock(str, docs)).toEqual(`/** + * Lorem ipsum + * @deprecated only for testing + */ +interface Foo extends Components.Foo, HTMLStencilElement {`); + }); + + it('indents the doc block correctly', () => { + str = ' ' + str; + expect(util.addDocBlock(str, docs, 4)).toEqual(` /** + * Lorem ipsum + * @deprecated only for testing + */ + interface Foo extends Components.Foo, HTMLStencilElement {`); + }); + + it('excludes the @internal tag', () => { + docs.tags.push({ name: 'internal' }); + expect(util.addDocBlock(str, docs).includes('@internal')).toBeFalsy(); + }); + + it('excludes empty lines', () => { + docs.text = ''; + str = ' ' + str; + expect(util.addDocBlock(str, docs, 4)).toEqual(` /** + * @deprecated only for testing + */ + interface Foo extends Components.Foo, HTMLStencilElement {`); + }); + + it.each<[d.CompilerJsDoc | undefined]>([[undefined], [{ tags: [], text: '' }]])( + 'does not add a doc block when docs are empty (%j)', + (documents) => { + expect(util.addDocBlock(str, documents)).toEqual(str); + }, + ); + }); + + describe('isTsFile', () => { + it.each(['.ts', 'foo.ts', 'foo.bar.ts', 'foo/bar.ts'])( + 'returns true for a file ending with .ts (%s)', + (fileName) => { + expect(util.isTsFile(fileName)).toEqual(true); + }, + ); + + it.each(['.tsx', 'foo.tsx', 'foo.bar.tsx', 'foo/bar.tsx'])( + 'returns false for a file ending with .tsx (%s)', + (fileName) => { + expect(util.isTsFile(fileName)).toEqual(false); + }, + ); + + it.each(['foo.js', 'foo.doc', 'foo.css', 'foo.html'])( + 'returns false for other a file with another extension (%s)', + (fileName) => { + expect(util.isTsFile(fileName)).toEqual(false); + }, + ); + + it('returns false for .d.ts and .d.tsx files', () => { + expect(util.isTsFile('foo/bar.d.ts')).toEqual(false); + expect(util.isTsFile('foo/bar.d.tsx')).toEqual(false); + }); + + it('returns true for a file named "spec.ts"', () => { + expect(util.isTsFile('spec.ts')).toEqual(true); + }); + + it('returns true for a file named "d.ts"', () => { + expect(util.isTsFile('d.ts')).toEqual(true); + }); + + it.each(['foo.tS', 'foo.Ts', 'foo.TS'])( + 'returns true for non-lowercase extensions (%s)', + (fileName) => { + expect(util.isTsFile(fileName)).toEqual(true); + }, + ); + }); + + describe('isJsFile', () => { + it.each(['.js', 'foo.js', 'foo.bar.js', 'foo/bar.js'])( + 'returns true for a file ending with .js (%s)', + (fileName) => { + expect(util.isJsFile(fileName)).toEqual(true); + }, + ); + + it.each(['.jsx', 'foo.txt', 'foo/bar.css', 'foo.bar.html'])( + 'returns false for other a file with another extension (%s)', + (fileName) => { + expect(util.isJsFile(fileName)).toEqual(false); + }, + ); + + it('returns true for a file named "spec.js"', () => { + expect(util.isJsFile('spec.js')).toEqual(true); + }); + + it.each(['foo.jS', 'foo.Js', 'foo.JS'])( + 'returns true for non-lowercase extensions (%s)', + (fileName) => { + expect(util.isJsFile(fileName)).toEqual(true); + }, + ); + }); + + describe('getTextDocs', () => { + let docs: d.CompilerJsDoc; + + beforeEach(() => { + docs = { + tags: [], + text: '', + }; + }); + it('returns empty string for null or undefined', () => { + expect(getTextDocs(null)).toBe(''); + expect(getTextDocs(undefined)).toBe(''); + }); + + it('returns only main text if no tags', () => { + docs = { + text: 'Some doc text.', + tags: [], + }; + expect(getTextDocs(docs)).toBe('Some doc text.'); + }); + + it('replaces line breaks in main text with spaces', () => { + docs = { + text: 'Line 1\nLine 2\r\nLine 3', + tags: [], + }; + expect(getTextDocs(docs)).toBe('Line 1 Line 2 Line 3'); + }); + + it('filters out internal tags and escapes "*/" in text and tags', () => { + docs = { + text: 'This text ends with */ sequence.', + tags: [ + { name: 'internal', text: 'should be removed' }, + { name: 'default', text: "'*/*'" }, + { name: 'deprecated', text: 'Use something else' }, + ], + }; + + const result = getTextDocs(docs); + + // Should replace "*/" with "*\/" (single backslash) + expect(result).toContain('This text ends with *\\/ sequence.'); + + // @internal tag filtered out + expect(result).not.toContain('@internal'); + + // @default and @deprecated tags included and escaped + expect(result).toContain("@default '*\\/*'"); + expect(result).toContain('@deprecated Use something else'); + + // Main text and tags separated by new lines + const lines = result.split('\n'); + expect(lines[0]).toBe('This text ends with *\\/ sequence.'); + expect(lines).toContain("@default '*\\/*'"); + expect(lines).toContain('@deprecated Use something else'); + }); + + it('trims the result', () => { + docs = { + text: ' Some text with spaces ', + tags: [], + }; + expect(getTextDocs(docs)).toBe('Some text with spaces'); + }); + }); +}); diff --git a/src/utils/test/validation.spec.ts b/packages/core/src/utils/_test_/validation.spec.ts similarity index 79% rename from src/utils/test/validation.spec.ts rename to packages/core/src/utils/_test_/validation.spec.ts index cadd6368b03..fa66bfa3f08 100644 --- a/src/utils/test/validation.spec.ts +++ b/packages/core/src/utils/_test_/validation.spec.ts @@ -1,3 +1,5 @@ +import { expect, describe, it } from '@stencil/vitest'; + import { validateComponentTag } from '../validation'; describe('validation', () => { @@ -24,15 +26,20 @@ describe('validation', () => { }); it('should error on comma', () => { - expect(validateComponentTag('my-tag,your-tag')).toBe('"my-tag,your-tag" tag cannot be used for multiple tags'); - }); - - it.each(['你-好', 'my-@component', '!@#$!@#4-ohno'])('should error on any invalid characters', (funkyTag) => { - expect(validateComponentTag(funkyTag)).toBe( - `"${funkyTag}" tag contains invalid characters: ${funkyTag.replace(/\w|-/g, '')}`, + expect(validateComponentTag('my-tag,your-tag')).toBe( + '"my-tag,your-tag" tag cannot be used for multiple tags', ); }); + it.each(['你-好', 'my-@component', '!@#$!@#4-ohno'])( + 'should error on any invalid characters', + (funkyTag) => { + expect(validateComponentTag(funkyTag)).toBe( + `"${funkyTag}" tag contains invalid characters: ${funkyTag.replace(/\w|-/g, '')}`, + ); + }, + ); + it('should error if no dash', () => { expect(validateComponentTag('dashless')).toBe( '"dashless" tag must contain a dash (-) to work as a valid web component', diff --git a/packages/core/src/utils/byte-size.d.ts b/packages/core/src/utils/byte-size.d.ts new file mode 100644 index 00000000000..b857129de2d --- /dev/null +++ b/packages/core/src/utils/byte-size.d.ts @@ -0,0 +1,7 @@ +/** + * Used to learn the size of a string in bytes. + * + * @param str The string to measure + * @returns number + */ +export declare const byteSize: (str: string) => number; diff --git a/src/utils/byte-size.ts b/packages/core/src/utils/byte-size.ts similarity index 100% rename from src/utils/byte-size.ts rename to packages/core/src/utils/byte-size.ts diff --git a/packages/core/src/utils/compiler-exports.ts b/packages/core/src/utils/compiler-exports.ts new file mode 100644 index 00000000000..4d758e883c7 --- /dev/null +++ b/packages/core/src/utils/compiler-exports.ts @@ -0,0 +1,24 @@ +/** + * Compiler utilities exported for CLI and other tools + * + * Excludes runtime-dependent utilities like shadow-root, style + */ +export * from './byte-size'; +export * from './constants'; +export * from './format-component-runtime-meta'; +export * from './helpers'; +export * from './is-glob'; +export * from './is-root-path'; +export * from './logger/logger-rolldown'; +export * from './logger/logger-typescript'; +export * from './logger/logger-utils'; +export * from './message-utils'; +export * from './output-target'; +export * from './path'; +export * from './query-nonce-meta-tag-content'; +export * from './regular-expression'; +export * as result from './result'; +export * from './sourcemaps'; +export * from './url-paths'; +export * from './util'; +export * from './validation'; diff --git a/packages/core/src/utils/constants.ts b/packages/core/src/utils/constants.ts new file mode 100644 index 00000000000..bf9a603c08d --- /dev/null +++ b/packages/core/src/utils/constants.ts @@ -0,0 +1,348 @@ +// MEMBER_FLAGS base values +const MF_String = 1 << 0; +const MF_Number = 1 << 1; +const MF_Boolean = 1 << 2; +const MF_Any = 1 << 3; +const MF_Unknown = 1 << 4; +const MF_State = 1 << 5; + +export const MEMBER_FLAGS = { + String: MF_String, + Number: MF_Number, + Boolean: MF_Boolean, + Any: MF_Any, + Unknown: MF_Unknown, + + State: MF_State, + Method: 1 << 6, + Event: 1 << 7, + Element: 1 << 8, + + ReflectAttr: 1 << 9, + Mutable: 1 << 10, + + Getter: 1 << 11, + Setter: 1 << 12, + + Prop: MF_String | MF_Number | MF_Boolean | MF_Any | MF_Unknown, + HasAttribute: MF_String | MF_Number | MF_Boolean | MF_Any | MF_Unknown, + PropLike: MF_String | MF_Number | MF_Boolean | MF_Any | MF_Unknown | MF_State, +} as const; + +export const WATCH_FLAGS = { + Immediate: 1 << 0, +} as const; + +export const EVENT_FLAGS = { + Cancellable: 1 << 0, + Composed: 1 << 1, + Bubbles: 1 << 2, +} as const; + +export const LISTENER_FLAGS = { + Passive: 1 << 0, + Capture: 1 << 1, + + TargetDocument: 1 << 2, + TargetWindow: 1 << 3, + TargetBody: 1 << 4, +} as const; + +export const HOST_FLAGS = { + hasConnected: 1 << 0, + hasRendered: 1 << 1, + isWaitingForChildren: 1 << 2, + isConstructingInstance: 1 << 3, + isQueuedForUpdate: 1 << 4, + hasInitializedComponent: 1 << 5, + hasLoadedComponent: 1 << 6, + isWatchReady: 1 << 7, + isListenReady: 1 << 8, + needsRerender: 1 << 9, + + // DEV ONLY + devOnRender: 1 << 10, + devOnDidLoad: 1 << 11, +} as const; + +// CMP_FLAGS base values +const CF_scopedCssEncapsulation = 1 << 1; + +/** + * A set of flags used for bitwise calculations against {@link ComponentRuntimeMeta#$flags$}. + * + * These flags should only be used in conjunction with {@link ComponentRuntimeMeta#$flags$}. + * They should _not_ be used for calculations against other fields/numbers + */ +export const CMP_FLAGS = { + /** + * Used to determine if a component is using the shadow DOM. + * e.g. `shadow: true | {}` is set on the `@Component()` decorator + */ + shadowDomEncapsulation: 1 << 0, + /** + * Used to determine if a component is using scoped stylesheets + * e.g. `scoped: true` is set on the `@Component()` decorator + */ + scopedCssEncapsulation: CF_scopedCssEncapsulation, + /** + * Used to determine if a component does not use the shadow DOM _and_ has `` tags in its markup. + */ + hasSlotRelocation: 1 << 2, + // bit 3 (1 << 3) is reserved — was needsShadowDomShim, removed in v5 + /** + * Determines if `delegatesFocus` is enabled for a component that uses the shadow DOM. + * e.g. `shadow: { delegatesFocus: true }` is set on the `@Component()` decorator + */ + shadowDelegatesFocus: 1 << 4, + /** + * Determines if `mode` is set on the `@Component()` decorator + */ + hasMode: 1 << 5, + /** + * Determines if styles must be scoped — i.e. the component uses scoped stylesheets. + */ + needsScopedEncapsulation: CF_scopedCssEncapsulation, + /** + * Determines if a component is form-associated or not. This is set based on + * options passed to the `@Component` decorator. + */ + formAssociated: 1 << 6, + + /** + * Determines if a `shadow: true` component needs + * to have its styles scoped during SSR as opposed to using DSD. + */ + shadowNeedsScopedCss: 1 << 7, + + /** + * Determines if a component has a `` in its template. + */ + hasSlot: 1 << 8, + + /** + * Determines if a component uses modern class property declarations. + */ + hasModernPropertyDecls: 1 << 9, + + /** + * Determines if `slotAssignment` is set to `'manual'` for a component that uses the shadow DOM. + * e.g. `shadow: { slotAssignment: 'manual' }` is set on the `@Component()` decorator + */ + shadowSlotAssignmentManual: 1 << 10, + + /** + * Determines if the shadow DOM mode is 'closed'. + * e.g. `encapsulation: { type: 'shadow', mode: 'closed' }` is set on the `@Component()` decorator + */ + shadowModeClosed: 1 << 11, + + /** + * Determines if the component should patch child node accessors for slot handling. + * e.g. `encapsulation: { type: 'none', patches: ['children'] }` + */ + patchChildren: 1 << 12, + + /** + * Determines if the component should patch cloneNode() for slot handling. + * e.g. `encapsulation: { type: 'none', patches: ['clone'] }` + */ + patchClone: 1 << 13, + + /** + * Determines if the component should patch appendChild/insertBefore for slot handling. + * e.g. `encapsulation: { type: 'none', patches: ['insert'] }` + */ + patchInsert: 1 << 14, + + /** + * Determines if the component should apply all slot patches. + * e.g. `encapsulation: { type: 'none', patches: ['all'] }` + * Equivalent to the global `experimentalSlotFixes` config option. + */ + patchAll: 1 << 15, +} as const; + +/** + * Default style mode id + */ +export const DEFAULT_STYLE_MODE = '$'; + +/** + * Namespaces + */ +export const SVG_NS = 'http://www.w3.org/2000/svg'; +export const HTML_NS = 'http://www.w3.org/1999/xhtml'; +export const XLINK_NS = 'http://www.w3.org/1999/xlink'; + +/** + * File names and value + */ +export const COLLECTION_MANIFEST_FILE_NAME = 'collection-manifest.json'; +export const COLLECTION_APP_DATA_FILE_NAME = 'app-data.js'; + +/** + * Constant for the 'copy' output target + */ +export const COPY = 'copy'; +/** + * Constant for the 'custom' output target + */ +export const CUSTOM = 'custom'; + +// ==================== Output Target Constants (v5) ==================== + +/** + * Constant for the 'loader-bundle' output target + * (formerly 'dist' in v4) + */ +export const LOADER_BUNDLE = 'loader-bundle'; + +/** + * Constant for the 'standalone' output target + * (formerly 'dist-custom-elements' in v4) + */ +export const STANDALONE = 'standalone'; + +/** + * Constant for the 'ssr' output target + * (formerly 'dist-hydrate-script' in v4) + */ +export const SSR = 'ssr'; + +/** + * Constant for the 'ssr-wasm' output target. + * Compiles the SSR script to a WASM binary via javy for use in any WASM-capable host + * (PHP, Java, Ruby, Go, etc.) without requiring a JavaScript runtime. + */ +export const SSR_WASM = 'ssr-wasm'; + +/** + * Constant for the 'collection' output target + * (formerly 'dist-collection' sub-output in v4) + * + * Contains transpiled source + metadata for downstream Stencil projects + * to re-compile/bundle. + */ +export const STENCIL_REBUNDLE = 'collection'; + +/** + * Constant for the 'types' output target + * (formerly 'dist-types' sub-output in v4) + */ +export const TYPES = 'types'; + +/** + * Constant for the 'global-style' output target + * Outputs global styles to a unified location available to all distributions. + */ +export const GLOBAL_STYLE = 'global-style'; + +/** + * Constant for the 'assets' output target + * Copies component assetsDirs to a unified location available to all distributions. + */ +export const ASSETS = 'assets'; + +// ==================== Internal Output Targets ==================== + +/** + * Internal constant for the 'dist-lazy' output target + * (used by loader-bundle and www) + */ +export const DIST_LAZY = 'dist-lazy'; + +/** + * Constant for the 'docs-custom' output target + */ +export const DOCS_CUSTOM = 'docs-custom'; +/** + * Constant for the 'docs-json' output target + */ +export const DOCS_JSON = 'docs-json'; +/** + * Constant for the 'docs-readme' output target + */ +export const DOCS_README = 'docs-readme'; +/** + * Constant for the 'docs-vscode' output target + */ +export const DOCS_VSCODE = 'docs-vscode'; +/** + * Constant for the 'docs-custom-elements-manifest' output target + */ +export const DOCS_CUSTOM_ELEMENTS_MANIFEST = 'docs-custom-elements-manifest'; +/** + * Constant for the 'stats' output target + */ +export const STATS = 'stats'; +/** + * Constant for the 'www' output target + */ +export const WWW = 'www'; + +/** + * Valid output targets to specify in a Stencil config. + * + * Note that some internal output targets (e.g. `DIST_LAZY`, `DIST_GLOBAL_STYLES`) + * are programmatically created by the compiler and are not user-configurable. + * + * In v5, `TYPES` and `STENCIL_REBUNDLE` are auto-generated in production builds unless explicitly configured. + */ +export const VALID_CONFIG_OUTPUT_TARGETS = [ + // DISTRIBUTION + WWW, + LOADER_BUNDLE, + STANDALONE, + SSR, + SSR_WASM, + STENCIL_REBUNDLE, + TYPES, + GLOBAL_STYLE, + ASSETS, + // DEPRECATED DISTRIBUTION TARGETS (< v5) + 'dist', + 'dist-custom-elements', + 'dist-hydrate-script', + 'dist-collection', + 'dist-types', + + // DOCS + DOCS_JSON, + DOCS_README, + DOCS_VSCODE, + DOCS_CUSTOM, + DOCS_CUSTOM_ELEMENTS_MANIFEST, + + // MISC + COPY, + CUSTOM, + STATS, +] as const; + +export const GENERATED_DTS = 'components.d.ts'; + +/** + * DOM Node types + * + * See https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType + * + * Note: this is a duplicate of the `NODE_TYPES` enum in mock-doc, it's + * copied over here so that we do not need to introduce a dependency on the + * mock-doc bundle in the runtime. See + * https://github.com/stenciljs/core/pull/5705 for more details. + */ +export const NODE_TYPES = { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12, +} as const; diff --git a/src/utils/es2022-rewire-class-members.ts b/packages/core/src/utils/es2022-rewire-class-members.ts similarity index 79% rename from src/utils/es2022-rewire-class-members.ts rename to packages/core/src/utils/es2022-rewire-class-members.ts index d03f0eb03d2..f08bb833021 100644 --- a/src/utils/es2022-rewire-class-members.ts +++ b/packages/core/src/utils/es2022-rewire-class-members.ts @@ -1,7 +1,7 @@ -import { BUILD } from '@app-data'; -import { MEMBER_FLAGS } from '@utils/constants'; +import { BUILD } from 'virtual:app-data'; +import type * as d from '@stencil/core'; -import type * as d from '../declarations'; +import { MEMBER_FLAGS } from './constants'; import { getPropertyDescriptor } from './get-prop-descriptor'; /** @@ -38,7 +38,10 @@ export const reWireGetterSetter = (instance: any, hostRef: d.HostRef) => { const members = Object.entries(cmpMeta.$members$ ?? {}); members.map(([memberName, [memberFlags]]) => { - if ((BUILD.state || BUILD.prop) && (memberFlags & MEMBER_FLAGS.Prop || memberFlags & MEMBER_FLAGS.State)) { + if ( + (BUILD.state || BUILD.prop) && + (memberFlags & MEMBER_FLAGS.Prop || memberFlags & MEMBER_FLAGS.State) + ) { const ogValue = instance[memberName]; // Get the original Stencil prototype `get` / `set` @@ -62,7 +65,10 @@ export const reWireGetterSetter = (instance: any, hostRef: d.HostRef) => { if (hostRef.$instanceValues$.has(memberName)) { instance[memberName] = hostRef.$instanceValues$.get(memberName); - } else if (ogValue !== undefined) { + } else if (ogValue !== undefined && instance[memberName] !== ogValue) { + // Only set if the value actually differs after re-wiring. + // This avoids triggering setters unnecessarily when the getter + // already returns the same value (e.g., custom getter/setter props). instance[memberName] = ogValue; } } diff --git a/src/utils/format-component-runtime-meta.ts b/packages/core/src/utils/format-component-runtime-meta.ts similarity index 93% rename from src/utils/format-component-runtime-meta.ts rename to packages/core/src/utils/format-component-runtime-meta.ts index 99f6d25b11d..24a10ff706b 100644 --- a/src/utils/format-component-runtime-meta.ts +++ b/packages/core/src/utils/format-component-runtime-meta.ts @@ -1,4 +1,5 @@ -import type * as d from '../declarations'; +import type * as d from '@stencil/core'; + import { CMP_FLAGS, LISTENER_FLAGS, MEMBER_FLAGS, WATCH_FLAGS } from './constants'; export const formatLazyBundleRuntimeMeta = ( @@ -29,6 +30,9 @@ export const formatComponentRuntimeMeta = ( if (compilerMeta.slotAssignment === 'manual') { flags |= CMP_FLAGS.shadowSlotAssignmentManual; } + if (compilerMeta.shadowMode === 'closed') { + flags |= CMP_FLAGS.shadowModeClosed; + } } else if (compilerMeta.encapsulation === 'scoped') { flags |= CMP_FLAGS.scopedCssEncapsulation; } @@ -48,6 +52,22 @@ export const formatComponentRuntimeMeta = ( flags |= CMP_FLAGS.hasModernPropertyDecls; } + // Per-component patches for slot handling (non-shadow DOM only) + if (compilerMeta.patches) { + if (compilerMeta.patches.all) { + flags |= CMP_FLAGS.patchAll; + } + if (compilerMeta.patches.children) { + flags |= CMP_FLAGS.patchChildren; + } + if (compilerMeta.patches.clone) { + flags |= CMP_FLAGS.patchClone; + } + if (compilerMeta.patches.insert) { + flags |= CMP_FLAGS.patchInsert; + } + } + const members = formatComponentRuntimeMembers(compilerMeta, includeMethods); const hostListeners = formatHostListeners(compilerMeta); const watchers = formatComponentRuntimeReactiveHandlers(compilerMeta, 'watchers'); @@ -244,9 +264,6 @@ const computeListenerFlags = (listener: d.ComponentCompilerListener) => { case 'body': flags |= LISTENER_FLAGS.TargetBody; break; - case 'parent' as any: - flags |= LISTENER_FLAGS.TargetParent; - break; } return flags; }; diff --git a/src/utils/get-prop-descriptor.ts b/packages/core/src/utils/get-prop-descriptor.ts similarity index 100% rename from src/utils/get-prop-descriptor.ts rename to packages/core/src/utils/get-prop-descriptor.ts diff --git a/packages/core/src/utils/helpers.ts b/packages/core/src/utils/helpers.ts new file mode 100644 index 00000000000..66ff18650c6 --- /dev/null +++ b/packages/core/src/utils/helpers.ts @@ -0,0 +1,246 @@ +/** + * Check if a value is defined (not null and not undefined). + * + * @param v - the value to check + * @returns true if the value is defined + */ +export const isDef = (v: any) => v != null && v !== undefined; + +/** + * Convert a string from PascalCase to dash-case + * + * @param str the string to convert + * @returns a converted string + */ +export const toDashCase = (str: string): string => + str + .replace(/([A-Z0-9])/g, (match) => ` ${match[0]}`) + .trim() + .split(' ') + .join('-') + .toLowerCase(); + +/** + * Convert a string from dash-case / kebab-case to PascalCase (or CamelCase, + * or whatever you call it!) + * + * @param str a string to convert + * @returns a converted string + */ +export const dashToPascalCase = (str: string): string => + str + .toLowerCase() + .split('-') + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join(''); + +/** + * Convert a string to 'camelCase' + * + * @param str the string to convert + * @returns the converted string + */ +export const toCamelCase = (str: string) => { + const pascalCase = dashToPascalCase(str); + return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1); +}; + +/** + * Capitalize the first letter of a string + * + * @param str the string to capitalize + * @returns a capitalized string + */ +export const toTitleCase = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); + +/** + * Escapes all occurrences of a specified pattern in a string. + * This function replaces all matches of a given pattern in the input text with a specified replacement string. + * It can handle both string and regular expression patterns and allows toggling between global and single-match replacements. + * + * @param text - The input string to process. + * @param pattern - The pattern to search for in the input string. Can be a regular expression or a string. + * @param replacement - The string to replace each match with. + * @param replaceAll - Whether to replace all occurrences (true) or just the first occurrence (false). Defaults to true. + * @returns The processed string with the replacements applied. + */ +export const escapeWithPattern = ( + text: string, + pattern: RegExp | string, + replacement: string, + replaceAll: boolean = true, +): string => { + let regex: RegExp; + + if (typeof pattern === 'string') { + const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + regex = new RegExp(escaped, replaceAll ? 'g' : ''); + } else { + const flags = pattern.flags; + const hasG = flags.includes('g'); + const newFlags = replaceAll + ? hasG + ? flags + : flags + 'g' + : hasG + ? flags.replace(/g/g, '') + : flags; + regex = new RegExp(pattern.source, newFlags); + } + + return text.replace(regex, replacement); +}; + +/** + * This is just a no-op, don't expect it to do anything. + */ +export const noop = (): any => { + /* noop*/ +}; + +/** + * Check whether a value is a 'complex type', defined here as an object or a + * function. + * + * @param o the value to check + * @returns whether it's a complex type or not + */ +export const isComplexType = (o: unknown): boolean => { + // https://jsperf.com/typeof-fn-object/5 + o = typeof o; + return o === 'object' || o === 'function'; +}; + +/** + * Sort an array without mutating it in-place (as `Array.prototype.sort` + * unfortunately does) + * + * @param array the array you'd like to sort + * @param prop a function for deriving sortable values (strings or numbers) + * from array members + * @returns a new array of all items `x` in `array` ordered by `prop(x)` + */ +export const sortBy = (array: T[], prop: (item: T) => string | number): T[] => { + return array.slice().sort((a, b) => { + const nameA = prop(a); + const nameB = prop(b); + if (nameA < nameB) return -1; + if (nameA > nameB) return 1; + return 0; + }); +}; + +/** + * A polyfill of sorts for `Array.prototype.flat` which will return the result + * of calling that method if present and, if not, return an equivalent based on + * `Array.prototype.reduce`. + * + * @param array the array to flatten (one level) + * @returns a flattened array + */ +export const flatOne = (array: T[][]): T[] => { + if (array.flat) { + return array.flat(1); + } + return array.reduce((result, item) => { + result.push(...item); + return result; + }, [] as T[]); +}; + +/** + * Deduplicate an array, retaining items at the earliest position in which + * they appear. + * + * So `unique([1,3,2,1,1,4])` would be `[1,3,2,4]`. + * + * @param array the array to deduplicate + * @param predicate an optional function used to generate the key used to + * determine uniqueness + * @returns a new, deduplicated array + */ +export const unique = (array: T[], predicate: (item: T) => K = (i) => i as any): T[] => { + const set = new Set(); + return array.filter((item) => { + const key = predicate(item); + if (key == null) { + return true; + } + if (set.has(key)) { + return false; + } + set.add(key); + return true; + }); +}; + +/** + * Merge elements of an array into an existing array, using a predicate to + * determine uniqueness and only adding elements when they are not present in + * the first array. + * + * **Note**: this mutates the target array! This is intentional to avoid + * unnecessary array allocation, but be sure that it's what you want! + * + * @param target the target array, to which new unique items should be added + * @param newItems a list of new items, some (or all!) of which may be added + * @param mergeWith a predicate function which reduces the items in `target` + * and `newItems` to a value which can be equated with `===` for the purposes + * of determining uniqueness + */ +export function mergeIntoWith(target: T1[], newItems: T1[], mergeWith: (item: T1) => T2) { + for (const item of newItems) { + const maybeItem = target.find((existingItem) => mergeWith(existingItem) === mergeWith(item)); + if (!maybeItem) { + // this is a new item that isn't present in `target` yet + target.push(item); + } + } +} + +/** + * A utility for building an object from an iterable very similar to + * `Object.fromEntries` + * + * @param entries an iterable object holding entries (key-value tuples) to + * plop into a new object + * @returns an object containing those entries + */ +export const fromEntries = (entries: IterableIterator<[string, V]>) => { + const object: Record = {}; + for (const [key, value] of entries) { + object[key] = value; + } + return object; +}; + +/** + * Based on a given object, create a new object which has only the specified + * key-value pairs included in it. + * + * @param obj the object from which to take values + * @param keys a set of keys to take + * @returns an object mapping `key` to `obj[key]` if `obj[key]` is truthy for + * every `key` in `keys` + */ +export const pluck = (obj: { [key: string]: any }, keys: string[]) => { + return keys.reduce( + (final, key) => { + if (obj[key]) { + final[key] = obj[key]; + } + return final; + }, + {} as { [key: string]: any }, + ); +}; + +const isDefined = (v: any): v is NonNullable => v !== null && v !== undefined; +export const isBoolean = (v: any): v is boolean => typeof v === 'boolean'; +export const isFunction = (v: any): v is Function => typeof v === 'function'; +export const isNumber = (v: any): v is number => typeof v === 'number'; +export const isObject = (val: object): val is object => + val != null && typeof val === 'object' && Array.isArray(val) === false; +export const isString = (v: any): v is string => typeof v === 'string'; +export const isIterable = (v: any): v is Iterable => + isDefined(v) && isFunction(v[Symbol.iterator]); diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 00000000000..f9b3ee83085 --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1,20 @@ +export * from './byte-size'; +export * from './constants'; +export * from './format-component-runtime-meta'; +export * from './helpers'; +export * from './is-glob'; +export * from './is-root-path'; +export * from './logger/logger-rolldown'; +export * from './logger/logger-typescript'; +export * from './logger/logger-utils'; +export * from './message-utils'; +export * from './output-target'; +export * from './path'; +export * from './query-nonce-meta-tag-content'; +export * from './regular-expression'; +export * as result from './result'; +export * from './shadow-root'; +export * from './sourcemaps'; +export * from './url-paths'; +export * from './util'; +export * from './validation'; diff --git a/src/utils/is-glob.ts b/packages/core/src/utils/is-glob.ts similarity index 88% rename from src/utils/is-glob.ts rename to packages/core/src/utils/is-glob.ts index 0a56ae8de99..6a4faf868b2 100644 --- a/src/utils/is-glob.ts +++ b/packages/core/src/utils/is-glob.ts @@ -7,7 +7,8 @@ export const isGlob = (str: string): boolean => { const chars: Record = { '{': '}', '(': ')', '[': ']' }; /* eslint-disable-next-line max-len */ - const regex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; + const regex = + /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; if (str === '') { return false; diff --git a/src/utils/is-root-path.ts b/packages/core/src/utils/is-root-path.ts similarity index 100% rename from src/utils/is-root-path.ts rename to packages/core/src/utils/is-root-path.ts diff --git a/packages/core/src/utils/logger/logger-rolldown.ts b/packages/core/src/utils/logger/logger-rolldown.ts new file mode 100644 index 00000000000..29edf401bd0 --- /dev/null +++ b/packages/core/src/utils/logger/logger-rolldown.ts @@ -0,0 +1,195 @@ +import type * as d from '@stencil/core'; +import type { RolldownError } from 'rolldown'; + +import { isString, toTitleCase } from '../helpers'; +import { buildWarn } from '../message-utils'; +import { splitLineBreaks } from './logger-utils'; + +export const isRolldownError = (e: unknown): e is RolldownError => + typeof e === 'object' && e !== null && 'message' in e; + +export const loadRolldownDiagnostics = ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + buildCtx: d.BuildCtx, + rolldownError: RolldownError, +) => { + const formattedCode = formatErrorCode(rolldownError.code); + + const diagnostic: d.Diagnostic = { + level: 'error', + type: 'bundling', + language: 'javascript', + code: rolldownError.code, + header: `Rolldown${formattedCode.length > 0 ? ': ' + formattedCode : ''}`, + messageText: formattedCode, + relFilePath: undefined, + absFilePath: undefined, + lines: [], + }; + + if (config.logLevel === 'debug' && rolldownError.stack) { + diagnostic.messageText = rolldownError.stack; + } else if (rolldownError.message) { + diagnostic.messageText = rolldownError.message; + } + + if (rolldownError.plugin) { + diagnostic.messageText += ` (plugin: ${rolldownError.plugin}${rolldownError.hook ? `, ${rolldownError.hook}` : ''})`; + } + + const loc = rolldownError.loc; + if (loc != null) { + const srcFile = loc.file || rolldownError.id; + if (isString(srcFile)) { + try { + const sourceText = compilerCtx.fs.readFileSync(srcFile); + if (sourceText) { + diagnostic.absFilePath = srcFile; + + try { + const srcLines = splitLineBreaks(sourceText); + + const errorLine = { + lineIndex: loc.line - 1, + lineNumber: loc.line, + text: srcLines[loc.line - 1], + errorCharStart: loc.column, + errorLength: 0, + } satisfies d.PrintLine; + + diagnostic.lineNumber = errorLine.lineNumber; + diagnostic.columnNumber = errorLine.errorCharStart; + + const highlightLine = errorLine.text?.slice(loc.column) ?? ''; + for (let i = 0; i < highlightLine.length; i++) { + if (charBreak.has(highlightLine.charAt(i))) { + break; + } + errorLine.errorLength++; + } + + diagnostic.lines.push(errorLine); + + if (errorLine.errorLength === 0 && errorLine.errorCharStart > 0) { + errorLine.errorLength = 1; + errorLine.errorCharStart--; + } + + if (errorLine.lineIndex > 0) { + const previousLine: d.PrintLine = { + lineIndex: errorLine.lineIndex - 1, + lineNumber: errorLine.lineNumber - 1, + text: srcLines[errorLine.lineIndex - 1], + errorCharStart: -1, + errorLength: -1, + }; + + diagnostic.lines.unshift(previousLine); + } + + if (errorLine.lineIndex + 1 < srcLines.length) { + const nextLine: d.PrintLine = { + lineIndex: errorLine.lineIndex + 1, + lineNumber: errorLine.lineNumber + 1, + text: srcLines[errorLine.lineIndex + 1], + errorCharStart: -1, + errorLength: -1, + }; + + diagnostic.lines.push(nextLine); + } + } catch { + diagnostic.messageText += `\nError parsing: ${diagnostic.absFilePath}, line: ${loc.line}, column: ${loc.column}`; + diagnostic.debugText = sourceText; + } + } else if (typeof rolldownError.frame === 'string') { + diagnostic.messageText += '\n' + rolldownError.frame; + } + } catch {} + } + } + + buildCtx.diagnostics.push(diagnostic); +}; + +export const createOnWarnFn = (diagnostics: d.Diagnostic[], bundleModulesFiles?: d.Module[]) => { + const previousWarns = new Set(); + + return function onWarningMessage(warning: { + code?: string; + importer?: string; + message?: string; + }) { + if ( + warning == null || + (warning.code && ignoreWarnCodes.has(warning.code)) || + (warning.message && previousWarns.has(warning.message)) + ) { + return; + } + + if (warning.message) { + previousWarns.add(warning.message); + } + + let label = ''; + if (bundleModulesFiles) { + label = bundleModulesFiles + .reduce((cmps, m) => { + cmps.push(...m.cmps); + return cmps; + }, [] as d.ComponentCompilerMeta[]) + .join(', ') + .trim(); + + if (label.length) { + label += ': '; + } + } + + const diagnostic = buildWarn(diagnostics); + diagnostic.header = `Bundling Warning ${warning.code}`; + diagnostic.messageText = label + (warning.message || warning); + }; +}; + +const ignoreWarnCodes = new Set([ + 'THIS_IS_UNDEFINED', + 'NON_EXISTENT_EXPORT', + 'CIRCULAR_DEPENDENCY', + 'EMPTY_BUNDLE', + 'UNUSED_EXTERNAL_IMPORT', +]); + +const charBreak = new Set([ + ' ', + '=', + '.', + ',', + '?', + ':', + ';', + '(', + ')', + '{', + '}', + '[', + ']', + '|', + `'`, + `"`, + '`', +]); + +const formatErrorCode = (errorCode: any) => { + if (typeof errorCode === 'string') { + return errorCode + .split('_') + .map((c) => { + return toTitleCase(c.toLowerCase()); + }) + .join(' '); + } + return (errorCode || '').trim(); +}; diff --git a/src/utils/logger/logger-typescript.ts b/packages/core/src/utils/logger/logger-typescript.ts similarity index 96% rename from src/utils/logger/logger-typescript.ts rename to packages/core/src/utils/logger/logger-typescript.ts index 3d822779031..393c67c6bc8 100644 --- a/src/utils/logger/logger-typescript.ts +++ b/packages/core/src/utils/logger/logger-typescript.ts @@ -1,6 +1,6 @@ +import type * as d from '@stencil/core'; import type { Diagnostic, DiagnosticMessageChain, Node } from 'typescript'; -import type * as d from '../../declarations'; import { isIterable } from '../helpers'; import { normalizePath } from '../path'; import { splitLineBreaks } from './logger-utils'; @@ -82,6 +82,12 @@ export const augmentDiagnosticWithNode = (d: d.Diagnostic, node: Node): d.Diagno * error reporting within a terminal. So, yeah, let's code it up, shall we? */ +/** + * Convert an array of TypeScript diagnostics to Stencil diagnostic format. + * + * @param tsDiagnostics - array of TypeScript diagnostic objects + * @returns array of Stencil diagnostic objects + */ export const loadTypeScriptDiagnostics = (tsDiagnostics: readonly Diagnostic[]) => { const diagnostics: d.Diagnostic[] = []; const maxErrors = Math.min(tsDiagnostics.length, 50); diff --git a/src/utils/logger/logger-utils.ts b/packages/core/src/utils/logger/logger-utils.ts similarity index 88% rename from src/utils/logger/logger-utils.ts rename to packages/core/src/utils/logger/logger-utils.ts index 10a67df7bd2..3a0376a22cf 100644 --- a/src/utils/logger/logger-utils.ts +++ b/packages/core/src/utils/logger/logger-utils.ts @@ -1,4 +1,4 @@ -import type * as d from '../../declarations'; +import type * as d from '@stencil/core'; /** * Iterate through a series of diagnostics to provide minor fix-ups for various edge cases, deduplicate messages, etc. @@ -6,7 +6,10 @@ import type * as d from '../../declarations'; * @param diagnostics the diagnostics to normalize * @returns the normalize documents */ -export const normalizeDiagnostics = (compilerCtx: d.CompilerCtx, diagnostics: d.Diagnostic[]): d.Diagnostic[] => { +export const normalizeDiagnostics = ( + compilerCtx: d.CompilerCtx, + diagnostics: d.Diagnostic[], +): d.Diagnostic[] => { const maxErrorsToNormalize = 25; const normalizedErrors: d.Diagnostic[] = []; const normalizedOthers: d.Diagnostic[] = []; @@ -15,7 +18,11 @@ export const normalizeDiagnostics = (compilerCtx: d.CompilerCtx, diagnostics: d. for (let i = 0; i < diagnostics.length; i++) { const diagnostic = normalizeDiagnostic(compilerCtx, diagnostics[i]); - const key = (diagnostic.absFilePath ?? '') + (diagnostic.code ?? '') + diagnostic.messageText + diagnostic.type; + const key = + (diagnostic.absFilePath ?? '') + + (diagnostic.code ?? '') + + diagnostic.messageText + + diagnostic.type; if (dups.has(key)) { continue; } @@ -39,11 +46,17 @@ export const normalizeDiagnostics = (compilerCtx: d.CompilerCtx, diagnostics: d. * @param diagnostic the diagnostic to normalize * @returns the altered diagnostic */ -const normalizeDiagnostic = (compilerCtx: d.CompilerCtx, diagnostic: d.Diagnostic): d.Diagnostic => { +const normalizeDiagnostic = ( + compilerCtx: d.CompilerCtx, + diagnostic: d.Diagnostic, +): d.Diagnostic => { if (diagnostic.messageText) { if (typeof (diagnostic.messageText).message === 'string') { diagnostic.messageText = (diagnostic.messageText).message; - } else if (typeof diagnostic.messageText === 'string' && diagnostic.messageText.indexOf('Error: ') === 0) { + } else if ( + typeof diagnostic.messageText === 'string' && + diagnostic.messageText.indexOf('Error: ') === 0 + ) { diagnostic.messageText = diagnostic.messageText.slice(7); } } @@ -101,7 +114,7 @@ const normalizeDiagnostic = (compilerCtx: d.CompilerCtx, diagnostic: d.Diagnosti break; } } - } catch (e) {} + } catch {} } } } diff --git a/packages/core/src/utils/message-utils.ts b/packages/core/src/utils/message-utils.ts new file mode 100644 index 00000000000..bdbc95891d9 --- /dev/null +++ b/packages/core/src/utils/message-utils.ts @@ -0,0 +1,232 @@ +import type * as d from '@stencil/core'; + +import { isString } from './helpers'; + +/** + * Builds a template `Diagnostic` entity for a build error. The created `Diagnostic` is returned, and have little + * detail attached to it regarding the specifics of the error - it is the responsibility of the caller of this method + * to attach the specifics of the error message. + * + * The created `Diagnostic` is pushed to the `diagnostics` argument as a side effect of calling this method. + * + * @param diagnostics the existing diagnostics that the created template `Diagnostic` should be added to + * @returns the created `Diagnostic` + */ +export const buildError = (diagnostics?: d.Diagnostic[]): d.Diagnostic => { + const diagnostic: d.Diagnostic = { + level: 'error', + type: 'build', + header: 'Build Error', + messageText: 'build error', + relFilePath: undefined, + absFilePath: undefined, + lines: [], + }; + + if (diagnostics) { + diagnostics.push(diagnostic); + } + + return diagnostic; +}; + +/** + * Builds a template `Diagnostic` entity for a build warning. The created `Diagnostic` is returned, and have little + * detail attached to it regarding the specifics of the warning - it is the responsibility of the caller of this method + * to attach the specifics of the warning message. + * + * The created `Diagnostic` is pushed to the `diagnostics` argument as a side effect of calling this method. + * + * @param diagnostics the existing diagnostics that the created template `Diagnostic` should be added to + * @returns the created `Diagnostic` + */ +export const buildWarn = (diagnostics: d.Diagnostic[]): d.Diagnostic => { + const diagnostic: d.Diagnostic = { + level: 'warn', + type: 'build', + header: 'Build Warn', + messageText: 'build warn', + lines: [], + }; + + diagnostics.push(diagnostic); + + return diagnostic; +}; + +/** + * Create a diagnostic message suited for representing an error in a JSON + * file. This includes information about the exact lines in the JSON file which + * caused the error and the path to the file. + * + * @param compilerCtx the current compiler context + * @param diagnostics a list of diagnostics used as a return param + * @param jsonFilePath the path to the JSON file where the error occurred + * @param msg the error message + * @param jsonField the key for the field which caused the error, used for finding + * the error line in the original JSON file. Only root-level keys (with minimal + * indentation, typically 2 spaces) are highlighted to avoid matching nested keys. + * @returns a reference to the newly-created diagnostic + */ +export const buildJsonFileError = ( + compilerCtx: d.CompilerCtx, + diagnostics: d.Diagnostic[], + jsonFilePath: string, + msg: string, + jsonField: string, +) => { + const err = buildError(diagnostics); + err.messageText = msg; + err.absFilePath = jsonFilePath; + + if (typeof jsonField === 'string') { + try { + const jsonStr = compilerCtx.fs.readFileSync(jsonFilePath); + const lines = jsonStr.replace(/\r/g, '\n').split('\n'); + + // Find matches that appear to be root-level JSON keys. + // In a standard pretty-printed JSON file, root-level keys have 2 spaces of indentation. + // We only highlight if we find a match at root level to avoid highlighting nested keys + // (e.g., highlighting "type": "git" inside repository when looking for the root "type" field). + let bestMatch: { lineIndex: number; charIndex: number; indentation: number } | null = null; + const ROOT_LEVEL_INDENTATION = 2; + + for (let i = 0; i < lines.length; i++) { + const txtLine = lines[i]; + const txtIndex = txtLine.indexOf(jsonField); + + if (txtIndex > -1) { + // Calculate indentation (number of leading whitespace chars) + const indentation = txtLine.search(/\S/); + + // For package.json and similar files, root-level keys are at 2 spaces indentation. + // Only consider this a match if it's at root level, or if no root match was found + // and this is the least-indented option. + if (indentation === ROOT_LEVEL_INDENTATION) { + // Found a root-level match - use it + bestMatch = { lineIndex: i, charIndex: txtIndex, indentation }; + break; // Root level found, no need to continue + } else if (bestMatch === null || indentation < bestMatch.indentation) { + // Track this as a fallback in case no root-level match exists + bestMatch = { lineIndex: i, charIndex: txtIndex, indentation }; + } + } + } + + // Only show line context if we found a root-level match (indentation === 2) + // This avoids highlighting nested keys when the root key doesn't exist + if (bestMatch !== null && bestMatch.indentation === ROOT_LEVEL_INDENTATION) { + const i = bestMatch.lineIndex; + const txtIndex = bestMatch.charIndex; + const txtLine = lines[i]; + + const warnLine: d.PrintLine = { + lineIndex: i, + lineNumber: i + 1, + text: txtLine, + errorCharStart: txtIndex, + errorLength: jsonField.length, + }; + err.lineNumber = warnLine.lineNumber; + err.columnNumber = txtIndex + 1; + err.lines.push(warnLine); + + if (i > 0) { + const beforeWarnLine: d.PrintLine = { + lineIndex: warnLine.lineIndex - 1, + lineNumber: warnLine.lineNumber - 1, + text: lines[i - 1], + errorCharStart: -1, + errorLength: -1, + }; + err.lines.unshift(beforeWarnLine); + } + + if (i < lines.length - 1) { + const afterWarnLine: d.PrintLine = { + lineIndex: warnLine.lineIndex + 1, + lineNumber: warnLine.lineNumber + 1, + text: lines[i + 1], + errorCharStart: -1, + errorLength: -1, + }; + err.lines.push(afterWarnLine); + } + } + } catch {} + } + + return err; +}; + +/** + * Builds a diagnostic from an `Error`, appends it to the `diagnostics` parameter, and returns the created diagnostic + * @param diagnostics the series of diagnostics the newly created diagnostics should be added to + * @param err the error to derive information from in generating the diagnostic + * @param msg an optional message to use in place of `err` to generate the diagnostic + * @returns the generated diagnostic + */ +export const catchError = ( + diagnostics: d.Diagnostic[], + err: Error | null | undefined, + msg?: string, +): d.Diagnostic => { + const diagnostic: d.Diagnostic = { + level: 'error', + type: 'build', + header: 'Build Error', + messageText: 'build error', + lines: [], + }; + + if (isString(msg)) { + diagnostic.messageText = msg.length ? msg : 'UNKNOWN ERROR'; + } else if (err != null) { + if (err.stack != null) { + diagnostic.messageText = err.stack.toString(); + } else { + if (err.message != null) { + diagnostic.messageText = err.message.length ? err.message : 'UNKNOWN ERROR'; + } else { + diagnostic.messageText = err.toString(); + } + } + } + + if (diagnostics != null && !shouldIgnoreError(diagnostic.messageText)) { + diagnostics.push(diagnostic); + } + + return diagnostic; +}; + +/** + * Determine if the provided diagnostics have any build errors + * @param diagnostics the diagnostics to inspect + * @returns true if any of the diagnostics in the list provided are errors that did not occur at runtime. false + * otherwise. + */ +export const hasError = (diagnostics: d.Diagnostic[]): boolean => { + if (diagnostics == null || diagnostics.length === 0) { + return false; + } + return diagnostics.some((d) => d.level === 'error' && d.type !== 'runtime'); +}; + +/** + * Determine if the provided diagnostics have any warnings + * @param diagnostics the diagnostics to inspect + * @returns true if any of the diagnostics in the list provided are warnings. false otherwise. + */ +export const hasWarning = (diagnostics: d.Diagnostic[]): boolean => { + if (diagnostics == null || diagnostics.length === 0) { + return false; + } + return diagnostics.some((d) => d.level === 'warn'); +}; + +export const shouldIgnoreError = (msg: any) => { + return msg === TASK_CANCELED_MSG; +}; + +export const TASK_CANCELED_MSG = `task canceled`; diff --git a/packages/core/src/utils/output-target.ts b/packages/core/src/utils/output-target.ts new file mode 100644 index 00000000000..fb1f50f6686 --- /dev/null +++ b/packages/core/src/utils/output-target.ts @@ -0,0 +1,258 @@ +import { basename, dirname, relative } from 'node:path'; +import picomatch from 'picomatch'; +import type * as d from '@stencil/core'; + +import { + COPY, + CUSTOM, + // v5 constants + LOADER_BUNDLE, + STANDALONE, + SSR, + SSR_WASM, + STENCIL_REBUNDLE, + TYPES, + GLOBAL_STYLE, + ASSETS, + // Internal output targets + DIST_LAZY, + // Docs + DOCS_CUSTOM, + DOCS_CUSTOM_ELEMENTS_MANIFEST, + DOCS_JSON, + DOCS_README, + DOCS_VSCODE, + // Other + GENERATED_DTS, + STATS, + VALID_CONFIG_OUTPUT_TARGETS, + WWW, +} from './constants'; +import { flatOne, sortBy } from './helpers'; +import { isGlob } from './is-glob'; +import { join, normalizePath } from './path'; + +/** + * Checks if a component tag name matches any of the exclude patterns. + * Supports glob patterns using minimatch. + * + * @param tagName The component's tag name to check + * @param excludePatterns Array of patterns to match against (supports globs) + * @returns true if the component should be excluded, false otherwise + */ +export const shouldExcludeComponent = ( + tagName: string, + excludePatterns: string[] | undefined, +): boolean => { + if (!excludePatterns || excludePatterns.length === 0) { + return false; + } + + return excludePatterns.some((pattern) => { + if (isGlob(pattern)) { + return picomatch.isMatch(tagName, pattern); + } + return pattern === tagName; + }); +}; + +export interface FilterComponentsResult { + components: d.ComponentCompilerMeta[]; + excludedComponents: d.ComponentCompilerMeta[]; +} + +/** + * Filters out components that match the excludeComponents patterns from the config. + * Only applies filtering to production builds (when devMode is false) - dev builds include all components. + * + * @param components Array of component metadata + * @param config The validated Stencil configuration + * @returns Object containing filtered components and excluded components + */ +export const filterExcludedComponents = ( + components: d.ComponentCompilerMeta[], + config: d.ValidatedConfig, +): FilterComponentsResult => { + // Only apply exclusion logic in production builds (devMode === false) + if (config.devMode) { + return { components, excludedComponents: [] }; + } + + const excludePatterns = config.excludeComponents; + + if (!excludePatterns || excludePatterns.length === 0) { + return { components, excludedComponents: [] }; + } + + const excludedComponents: d.ComponentCompilerMeta[] = []; + const excludedTags: string[] = []; + + const filtered = components.filter((cmp) => { + const shouldExclude = shouldExcludeComponent(cmp.tagName, excludePatterns); + + if (shouldExclude) { + excludedComponents.push(cmp); + excludedTags.push(cmp.tagName); + config.logger.debug(`Excluding component from build: ${cmp.tagName}`); + } + + return !shouldExclude; + }); + + // Log summary of excluded components for production builds + if (excludedTags.length > 0) { + const tagList = excludedTags.join(', '); + config.logger.info( + `Excluding ${excludedTags.length} component${excludedTags.length === 1 ? '' : 's'} from production build: ${tagList}`, + ); + } + + return { components: filtered, excludedComponents }; +}; + +export const relativeImport = ( + pathFrom: string, + pathTo: string, + ext?: string, + addPrefix = true, +) => { + let relativePath = relative(dirname(pathFrom), dirname(pathTo)); + if (addPrefix) { + if (relativePath === '') { + relativePath = '.'; + } else if (relativePath[0] !== '.') { + relativePath = './' + relativePath; + } + } + return normalizePath(`${relativePath}/${basename(pathTo, ext)}`); +}; + +export const getComponentsDtsSrcFilePath = (config: d.ValidatedConfig) => + join(config.srcDir, GENERATED_DTS); + +/** + * Helper to get an appropriate file path for `components.d.ts` for an output target. + * + * @param typesDir the directory where types are generated + * @returns a properly-formatted path + */ +export const getComponentsDtsTypesFilePath = (typesDir: string) => join(typesDir, GENERATED_DTS); + +// ==================== v5 Output Target Type Guards ==================== + +export const isOutputTargetLoaderBundle = (o: d.OutputTarget): o is d.OutputTargetLoaderBundle => + o.type === LOADER_BUNDLE; + +export const isOutputTargetStandalone = (o: d.OutputTarget): o is d.OutputTargetStandalone => + o.type === STANDALONE; + +export const isOutputTargetSsr = (o: d.OutputTarget): o is d.OutputTargetSsr => o.type === SSR; + +export const isOutputTargetSsrWasm = (o: d.OutputTarget): o is d.OutputTargetSsrWasm => + o.type === SSR_WASM; + +export const isOutputTargetCollection = (o: d.OutputTarget): o is d.OutputTargetCollection => + o.type === STENCIL_REBUNDLE; + +export const isOutputTargetTypes = (o: d.OutputTarget): o is d.OutputTargetTypes => + o.type === TYPES; + +export const isOutputTargetGlobalStyle = (o: d.OutputTarget): o is d.OutputTargetGlobalStyle => + o.type === GLOBAL_STYLE; + +export const isOutputTargetAssets = (o: d.OutputTarget): o is d.OutputTargetAssets => + o.type === ASSETS; + +// ==================== Other Output Target Type Guards ==================== + +export const isOutputTargetCopy = (o: d.OutputTarget): o is d.OutputTargetCopy => o.type === COPY; + +export const isOutputTargetDistLazy = (o: d.OutputTarget): o is d.OutputTargetDistLazy => + o.type === DIST_LAZY; + +export const isOutputTargetCustom = (o: d.OutputTarget): o is d.OutputTargetCustom => + o.type === CUSTOM; + +export const isOutputTargetDocs = ( + o: d.OutputTarget, +): o is + | d.OutputTargetDocsJson + | d.OutputTargetDocsReadme + | d.OutputTargetDocsVscode + | d.OutputTargetDocsCustom + | d.OutputTargetDocsCustomElementsManifest => + o.type === DOCS_README || + o.type === DOCS_JSON || + o.type === DOCS_CUSTOM || + o.type === DOCS_VSCODE || + o.type === DOCS_CUSTOM_ELEMENTS_MANIFEST; + +export const isOutputTargetDocsReadme = (o: d.OutputTarget): o is d.OutputTargetDocsReadme => + o.type === DOCS_README; + +export const isOutputTargetDocsJson = (o: d.OutputTarget): o is d.OutputTargetDocsJson => + o.type === DOCS_JSON; + +export const isOutputTargetDocsCustom = (o: d.OutputTarget): o is d.OutputTargetDocsCustom => + o.type === DOCS_CUSTOM; + +export const isOutputTargetDocsVscode = (o: d.OutputTarget): o is d.OutputTargetDocsVscode => + o.type === DOCS_VSCODE; + +export const isOutputTargetDocsCustomElementsManifest = ( + o: d.OutputTarget, +): o is d.OutputTargetDocsCustomElementsManifest => o.type === DOCS_CUSTOM_ELEMENTS_MANIFEST; + +export const isOutputTargetWww = (o: d.OutputTarget): o is d.OutputTargetWww => o.type === WWW; + +export const isOutputTargetStats = (o: d.OutputTarget): o is d.OutputTargetStats => + o.type === STATS; + +/** + * Retrieve the Stencil component compiler metadata from a collection of Stencil {@link d.Module}s + * @param moduleFiles the collection of `Module`s to retrieve the metadata from + * @returns the metadata, lexicographically sorted by the tag names of the components + */ +export const getComponentsFromModules = (moduleFiles: d.Module[]): d.ComponentCompilerMeta[] => + sortBy(flatOne(moduleFiles.map((m) => m.cmps)), (c: d.ComponentCompilerMeta) => c.tagName); + +// Given a ReadonlyArray of strings we can derive a union type from them +// by getting `typeof ARRAY[number]`, i.e. the type of all values returns +// by number keys. +type ValidConfigOutputTarget = (typeof VALID_CONFIG_OUTPUT_TARGETS)[number]; + +/** + * Check whether a given output target is a valid one to be set in a Stencil config + * + * @param targetType the type which we want to check + * @returns whether or not the targetType is a valid, configurable output target. + */ +export function isValidConfigOutputTarget( + targetType: string, +): targetType is ValidConfigOutputTarget { + // unfortunately `includes` is typed on `ReadonlyArray` as `(el: T): + // boolean` so a `string` cannot be passed to `includes` on a + // `ReadonlyArray` 😢 thus we `as any` + // + // see microsoft/TypeScript#31018 for some discussion of this + return VALID_CONFIG_OUTPUT_TARGETS.includes(targetType as any); +} + +/** + * Filter output targets based on devMode and their skipInDev setting. + * In dev mode, targets with `skipInDev: true` are filtered out. + * In prod mode, all targets are included. + * + * @param targets Array of output targets to filter + * @param devMode Whether we're in dev mode + * @returns Filtered array of active targets + */ +export const filterActiveTargets = ( + targets: T[], + devMode: boolean, +): T[] => { + if (!devMode) { + return targets; + } + return targets.filter((t) => !t.skipInDev); +}; diff --git a/src/utils/path.ts b/packages/core/src/utils/path.ts similarity index 97% rename from src/utils/path.ts rename to packages/core/src/utils/path.ts index caff7ae5980..6e68ebfe38f 100644 --- a/src/utils/path.ts +++ b/packages/core/src/utils/path.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import nodePath from 'node:path'; /** * Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar @@ -211,7 +211,7 @@ export function relative(from: string, to: string): string { * path. However, our algorithm does differ from that of Node's, as described in this function's JSDoc when a zero * length string is encountered. */ - return normalizePath(path.relative(from, to), false); + return normalizePath(nodePath.relative(from, to), false); } /** @@ -235,7 +235,7 @@ export function join(...paths: string[]): string { * Note that our algorithm does differ from Node's, as described in this function's JSDoc regarding trailing * slashes. */ - return normalizePath(path.join(...paths), false); + return normalizePath(nodePath.join(...paths), false); } /** @@ -253,7 +253,7 @@ export function resolve(...paths: string[]): string { * When normalizing, we should _not_ attempt to relativize the path returned by the native Node `resolve` method. When * calculating the path from each of the string-based parts, Node does not prepend './' to the calculated path. */ - return normalizePath(path.resolve(...paths), false); + return normalizePath(nodePath.resolve(...paths), false); } /** @@ -271,5 +271,5 @@ export function normalize(toNormalize: string): string { * When normalizing, we should _not_ attempt to relativize the path returned by the native Node `normalize` method. * When calculating the path from each of the string-based parts, Node does not prepend './' to the calculated path. */ - return normalizePath(path.normalize(toNormalize), false); + return normalizePath(nodePath.normalize(toNormalize), false); } diff --git a/src/utils/query-nonce-meta-tag-content.ts b/packages/core/src/utils/query-nonce-meta-tag-content.ts similarity index 100% rename from src/utils/query-nonce-meta-tag-content.ts rename to packages/core/src/utils/query-nonce-meta-tag-content.ts diff --git a/packages/core/src/utils/readme.md b/packages/core/src/utils/readme.md new file mode 100644 index 00000000000..534ae201e82 --- /dev/null +++ b/packages/core/src/utils/readme.md @@ -0,0 +1,60 @@ +# utils + +Shared utility functions used across the compiler and runtime. + +## Overview + +This directory contains helper functions, constants, and utilities that are used by multiple parts of Stencil. It's imported via the `@utils` alias. + +## Key Files + +| File | Purpose | +| --------------- | ------------------------------------------------------------ | +| `helpers.ts` | General-purpose utilities (type guards, string manipulation) | +| `constants.ts` | Shared constants and magic strings | +| `result.ts` | `Result` type for error handling | +| `validation.ts` | Input validation helpers | +| `path.ts` | Cross-platform path utilities | +| `shadow-css.ts` | CSS scoping for Shadow DOM emulation | +| `sourcemaps.ts` | Source map manipulation | + +## Categories + +### Type Utilities + +- Type guards (`isString`, `isFunction`, etc.) +- Type transformations + +### String Utilities + +- `toDashCase()`, `toCamelCase()` - case conversion +- `createJsVarName()` - safe JS identifier creation + +### Path Utilities + +- Normalization across platforms +- Relative path computation + +### Result Type + +Functional error handling without exceptions: + +```ts +import { result } from '@utils'; + +const res = result.ok(value); // Success +const err = result.err(error); // Failure +``` + +### Build Helpers + +- `format-component-runtime-meta.ts` - Serialize component metadata +- `output-target.ts` - Output target utilities + +## Usage + +```ts +import { isString, normalizePath } from '@utils'; +``` + +Note: Some utilities are compiler-only, some are runtime-safe. Check imports carefully when using in runtime code. \ No newline at end of file diff --git a/src/utils/regular-expression.ts b/packages/core/src/utils/regular-expression.ts similarity index 100% rename from src/utils/regular-expression.ts rename to packages/core/src/utils/regular-expression.ts diff --git a/src/utils/result.ts b/packages/core/src/utils/result.ts similarity index 96% rename from src/utils/result.ts rename to packages/core/src/utils/result.ts index 56593813f4d..80abdc52379 100644 --- a/src/utils/result.ts +++ b/packages/core/src/utils/result.ts @@ -6,7 +6,7 @@ * Using it could look something like this: * * ```ts - * import { result } from '@utils'; + * import { result } from './'; * * const mightFail = (input: number): Result => { * try { @@ -102,7 +102,10 @@ export const err = (value: T): Err => ({ * @returns a new `Result`, with the a new wrapped value (if `Ok`) or the * same (if `Err) */ -export function map(result: Result, fn: (t: T1) => Promise): Promise>; +export function map( + result: Result, + fn: (t: T1) => Promise, +): Promise>; export function map(result: Result, fn: (t: T1) => T2): Result; export function map( result: Result, diff --git a/src/utils/shadow-css.ts b/packages/core/src/utils/shadow-css.ts similarity index 90% rename from src/utils/shadow-css.ts rename to packages/core/src/utils/shadow-css.ts index 8dd7c0150dd..017ce5e994f 100644 --- a/src/utils/shadow-css.ts +++ b/packages/core/src/utils/shadow-css.ts @@ -74,10 +74,17 @@ const _polyfillHost = '-shadowcsshost'; const _polyfillSlotted = '-shadowcssslotted'; // note: :host-context pre-processed to -shadowcsshostcontext. const _polyfillHostContext = '-shadowcsscontext'; -const _parenSuffix = ')(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))?([^,{]*)'; -const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim'); -const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim'); -const _cssColonSlottedRe = new RegExp('(' + _polyfillSlotted + _parenSuffix, 'gim'); +const _parenSuffix = ')(?:\\(((?:\\([^)(]*\\)|[^)(]*)+?)\\))?([^,{]*)'; +// Lazy-initialize RegExps to avoid top-level side effects that prevent tree-shaking +let _cssColonHostRe: RegExp; +let _cssColonHostContextRe: RegExp; +let _cssColonSlottedRe: RegExp; +const getCssColonHostRe = () => + (_cssColonHostRe ??= new RegExp('(' + _polyfillHost + _parenSuffix, 'gim')); +const getCssColonHostContextRe = () => + (_cssColonHostContextRe ??= new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim')); +const getCssColonSlottedRe = () => + (_cssColonSlottedRe ??= new RegExp('(' + _polyfillSlotted + _parenSuffix, 'gim')); const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/; const _shadowDOMSelectorsRe = [/::shadow/g, /::content/g]; @@ -105,7 +112,7 @@ const _polyfillHostRe = /-shadowcsshost/gim; */ const createSupportsRuleRe = (selector: string) => { // We need to match any occurrence of the selector that's NOT inside @supports selector(...) - const safeSelector = escapeRegExpSpecialCharacters(selector); + const escapedSelector = escapeRegExpSpecialCharacters(selector); // This regex needs to: // 1. Skip selectors inside @supports selector(...) rule conditions @@ -117,9 +124,9 @@ const createSupportsRuleRe = (selector: string) => { return new RegExp( // First capture group: match any context before the selector that's not inside @supports selector() // Using negative lookahead to avoid matching inside @supports selector(...) condition - `(^|[^@]|@(?!supports\\s+selector\\s*\\([^{]*?${safeSelector}))` + + `(^|[^@]|@(?!supports\\s+selector\\s*\\([^{]*?${escapedSelector}))` + // Then match the selector - `(${safeSelector}\\b)`, + `(${escapedSelector}\\b)`, 'g', ); }; @@ -136,7 +143,7 @@ const extractCommentsWithHash = (input: string): string[] => { return input.match(_commentWithHashRe) || []; }; -const _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g; +const _ruleRe = /(\s*)([^;{}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g; const _curlyRe = /([{}])/g; const _selectorPartsRe = /(^.*?[^\\])??((:+)(.*)|$)/; const OPEN_CURLY = '{'; @@ -297,7 +304,7 @@ const colonHostPartReplacer = (host: string, part: string, suffix: string) => { }; const convertColonHost = (cssText: string) => { - return convertColonRule(cssText, _cssColonHostRe, colonHostPartReplacer); + return convertColonRule(cssText, getCssColonHostRe(), colonHostPartReplacer); }; const colonHostContextPartReplacer = (host: string, part: string, suffix: string) => { @@ -312,7 +319,7 @@ const convertColonSlotted = (cssText: string, slotScopeId: string) => { const slotClass = '.' + slotScopeId + ' > '; const selectors: { orgSelector: string; updatedSelector: string }[] = []; - cssText = cssText.replace(_cssColonSlottedRe, (...m: string[]) => { + cssText = cssText.replace(getCssColonSlottedRe(), (...m: string[]) => { if (m[2]) { const compound = m[2].trim(); const suffix = m[3]; @@ -350,7 +357,7 @@ const convertColonSlotted = (cssText: string, slotScopeId: string) => { }; const convertColonHostContext = (cssText: string) => { - return convertColonRule(cssText, _cssColonHostContextRe, colonHostContextPartReplacer); + return convertColonRule(cssText, getCssColonHostContextRe(), colonHostContextPartReplacer); }; const convertShadowDOMSelectors = (cssText: string) => { @@ -370,25 +377,36 @@ const selectorNeedsScoping = (selector: string, scopeSelector: string) => { }; const injectScopingSelector = (selector: string, scopingSelector: string) => { - return selector.replace(_selectorPartsRe, (_: string, before = '', _colonGroup: string, colon = '', after = '') => { - return before + scopingSelector + colon + after; - }); + return selector.replace( + _selectorPartsRe, + (_: string, before = '', _colonGroup: string, colon = '', after = '') => { + return before + scopingSelector + colon + after; + }, + ); }; -const applySimpleSelectorScope = (selector: string, scopeSelector: string, hostSelector: string) => { +const applySimpleSelectorScope = ( + selector: string, + scopeSelector: string, + hostSelector: string, +) => { // In Android browser, the lastIndex is not reset when the regex is used in String.replace() _polyfillHostRe.lastIndex = 0; if (_polyfillHostRe.test(selector)) { const replaceBy = `.${hostSelector}`; return selector - .replace(_polyfillHostNoCombinatorRe, (_, selector) => injectScopingSelector(selector, replaceBy)) + .replace(_polyfillHostNoCombinatorRe, (_, sel) => injectScopingSelector(sel, replaceBy)) .replace(_polyfillHostRe, replaceBy + ' '); } return scopeSelector + ' ' + selector; }; -const applyStrictSelectorScope = (selector: string, scopeSelector: string, hostSelector: string) => { +const applyStrictSelectorScope = ( + selector: string, + scopeSelector: string, + hostSelector: string, +) => { const isRe = /\[is=([^\]]*)\]/g; scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]); @@ -447,14 +465,20 @@ const applyStrictSelectorScope = (selector: string, scopeSelector: string, hostS } const part = selector.substring(startIndex); - shouldScope = !part.match(_safePartRe) && (shouldScope || part.indexOf(_polyfillHostNoCombinator) > -1); + shouldScope = + !part.match(_safePartRe) && (shouldScope || part.indexOf(_polyfillHostNoCombinator) > -1); scopedSelector += shouldScope ? _scopeSelectorPart(part) : part; // replace the placeholders with their original values return restoreSafeSelector(safeContent.placeholders, scopedSelector); }; -const scopeSelector = (selector: string, scopeSelectorText: string, hostSelector: string, slotSelector: string) => { +const scopeSelector = ( + selector: string, + scopeSelectorText: string, + hostSelector: string, + slotSelector: string, +) => { return selector .split(',') .map((shallowPart) => { @@ -489,7 +513,13 @@ const scopeSelectors = ( rule.selector.startsWith('@page') || rule.selector.startsWith('@document') ) { - content = scopeSelectors(rule.content, scopeSelectorText, hostSelector, slotSelector, commentOriginalSelector); + content = scopeSelectors( + rule.content, + scopeSelectorText, + hostSelector, + slotSelector, + commentOriginalSelector, + ); } const cssRule: CssRule = { @@ -647,7 +677,10 @@ export const scopeCss = (cssText: string, scopeId: string, commentOriginalSelect scoped.slottedSelectors.forEach((slottedSelector) => { // Use lookahead to ensure we only match complete selectors, not partial substrings // A selector ends at ',' (separator), '{' (declaration block), or end of string - const regex = new RegExp(escapeRegExpSpecialCharacters(slottedSelector.orgSelector) + '(?=\\s*[,{]|$)', 'g'); + const regex = new RegExp( + escapeRegExpSpecialCharacters(slottedSelector.orgSelector) + '(?=\\s*[,{]|$)', + 'g', + ); cssText = cssText.replace(regex, slottedSelector.updatedSelector); }); diff --git a/packages/core/src/utils/shadow-root.ts b/packages/core/src/utils/shadow-root.ts new file mode 100644 index 00000000000..a9e85521f18 --- /dev/null +++ b/packages/core/src/utils/shadow-root.ts @@ -0,0 +1,66 @@ +import { BUILD } from 'virtual:app-data'; +import { globalStyles } from 'virtual:app-globals'; +import { + supportsConstructableStylesheets, + supportsMutableAdoptedStyleSheets, +} from 'virtual:platform'; +import type * as d from '@stencil/core'; + +import { HYDRATED_STYLE_ID } from '../runtime/runtime-constants'; +import { CMP_FLAGS } from './constants'; +import { createStyleSheetIfNeededAndSupported } from './style'; + +let globalStyleSheet: CSSStyleSheet | null | undefined; + +// Constant scope ID for global styles to enable HMR tracking +const GLOBAL_STYLE_ID = 'sc-global'; + +export function createShadowRoot(this: HTMLElement, cmpMeta: d.ComponentRuntimeMeta) { + // Determine shadow root mode - 'closed' if flag is set, otherwise 'open' (default) + const isClosed = BUILD.shadowModeClosed && !!(cmpMeta.$flags$ & CMP_FLAGS.shadowModeClosed); + const opts: ShadowRootInit = { mode: isClosed ? 'closed' : 'open' }; + + if (BUILD.shadowDelegatesFocus) { + opts.delegatesFocus = !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDelegatesFocus); + } + + if (BUILD.shadowSlotAssignmentManual) { + const isManual = !!(cmpMeta.$flags$ & CMP_FLAGS.shadowSlotAssignmentManual); + if (isManual) { + opts.slotAssignment = 'manual'; + } + } + + const shadowRoot = this.attachShadow(opts); + + // For closed shadow roots, store the reference so Stencil can still access it internally. + // Note: element.shadowRoot will return null for closed shadow roots. + if (BUILD.shadowModeClosed && isClosed) { + (this as any).__shadowRoot = shadowRoot; + } + + // Initialize if undefined, set to CSSStyleSheet or null + if (globalStyleSheet === undefined) + globalStyleSheet = createStyleSheetIfNeededAndSupported(globalStyles) ?? null; + + // Use initialized global stylesheet if available + if (globalStyleSheet) { + if (supportsMutableAdoptedStyleSheets) { + shadowRoot.adoptedStyleSheets.push(globalStyleSheet); + } else { + shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, globalStyleSheet]; + } + } else if (globalStyles && !supportsConstructableStylesheets) { + // Fallback for dev mode: add global styles as + + + Stencil Dev Server Connector 5.0.0 + + + + diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json new file mode 100644 index 00000000000..bbe851e03bd --- /dev/null +++ b/packages/dev-server/package.json @@ -0,0 +1,62 @@ +{ + "name": "@stencil/dev-server", + "version": "5.0.0-alpha.5", + "description": "Development server for Stencil with DOM-based HMR", + "keywords": [ + "dev server", + "hmr", + "hot module replacement", + "stencil", + "web components" + ], + "homepage": "https://stenciljs.com/", + "license": "MIT", + "author": "StencilJs Contributors", + "repository": { + "type": "git", + "url": "git+https://github.com/stenciljs/core.git" + }, + "files": [ + "dist/", + "templates/", + "static/" + ], + "type": "module", + "main": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + }, + "./client": { + "types": "./dist/client/index.d.mts", + "import": "./dist/client/index.mjs" + } + }, + "scripts": { + "build": "tsdown", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "launch-editor": "^2.9.1", + "open": "^11.0.0", + "ws": "^8.0.0" + }, + "devDependencies": { + "@stencil/core": "workspace:*", + "@tsdown/css": "^0.21.6", + "@types/ws": "^8.0.0", + "tsdown": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:", + "vitest-environment-stencil": "catalog:" + }, + "peerDependencies": { + "@stencil/core": "^5.0.0-0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/dev-server/src/client/_test_/hmr.spec.ts b/packages/dev-server/src/client/_test_/hmr.spec.ts new file mode 100644 index 00000000000..61484248f8e --- /dev/null +++ b/packages/dev-server/src/client/_test_/hmr.spec.ts @@ -0,0 +1,216 @@ +/** + * @vitest-environment stencil + */ +import { describe, expect, it } from 'vitest'; + +import { hmrInlineStyles } from '../hmr/style'; +import { getHmrHref, updateCssUrlValue } from '../hmr/utils'; + +describe('updateCssUrlValue', () => { + const versionId = '1234'; + + it('should update url w/ existing qs', () => { + const fileName = 'img.png'; + const css = `background-image: url('img.png?what=ever&s-hmr=4321')`; + + const newCss = updateCssUrlValue(versionId, fileName, css); + expect(newCss).toBe(`background-image: url('img.png?what=ever&s-hmr=1234')`); + }); + + it('should update url w/ single quotes', () => { + const fileName = 'img.png'; + const css = `background: url('img.png')`; + + const newCss = updateCssUrlValue(versionId, fileName, css); + expect(newCss).toBe(`background: url('img.png?s-hmr=1234')`); + }); + + it('should update url w/ double quotes', () => { + const fileName = 'img.png'; + const css = 'background: url("img.png")'; + + const newCss = updateCssUrlValue(versionId, fileName, css); + expect(newCss).toBe('background: url("img.png?s-hmr=1234")'); + }); + + it('should update url w/ no quotes', () => { + const fileName = 'img.png'; + const css = 'background: url(img.png)'; + + const newCss = updateCssUrlValue(versionId, fileName, css); + expect(newCss).toBe('background: url(img.png?s-hmr=1234)'); + }); + + it('should not update for different file', () => { + const fileName = 'img.png'; + const css = 'background: url(hello.png)'; + + const newCss = updateCssUrlValue(versionId, fileName, css); + expect(newCss).toBe('background: url(hello.png)'); + }); + + it('should not get url', () => { + const fileName = 'img.png'; + const css = 'background: red'; + + const newCss = updateCssUrlValue(versionId, fileName, css); + expect(newCss).toBe('background: red'); + }); +}); + +describe('getHmrHref', () => { + const versionId = '1234'; + + it('update existing qs', () => { + const fileName = 'file-a.css'; + const oldHref = './file-a.css?s-hmr=4321&what=ever'; + + const newHref = getHmrHref(versionId, fileName, oldHref); + + expect(newHref).toBe('./file-a.css?s-hmr=1234&what=ever'); + }); + + it('add to existing qs', () => { + const fileName = 'file-a.css'; + const oldHref = './file-a.css?what=ever'; + + const newHref = getHmrHref(versionId, fileName, oldHref); + + expect(newHref).toBe('./file-a.css?what=ever&s-hmr=1234'); + }); + + it('update no prefix . or / relative href', () => { + const fileName = 'file-a.css'; + const oldHref = 'file-a.css'; + + const newHref = getHmrHref(versionId, fileName, oldHref); + + expect(newHref).toBe('file-a.css?s-hmr=1234'); + }); + + it('update exact href', () => { + const fileName = 'file-a.css'; + const oldHref = '/build/file-a.css'; + + const newHref = getHmrHref(versionId, fileName, oldHref); + + expect(newHref).toBe('/build/file-a.css?s-hmr=1234'); + }); + + it('not matching file name', () => { + const fileName = 'file-a.css'; + const oldHref = '/build/file-b.css'; + + const newHref = getHmrHref(versionId, fileName, oldHref); + + expect(newHref).toBe('/build/file-b.css'); + }); +}); + +describe('hmrInlineStyles', () => { + const versionId = '1234'; + + it('should update existing style element', () => { + const styleElm = document.createElement('style'); + styleElm.setAttribute('sty-id', 'sc-test-component'); + styleElm.innerHTML = '.old { color: red; }'; + document.head.appendChild(styleElm); + + hmrInlineStyles(document.documentElement, versionId, [ + { + styleId: 'sc-test-component', + styleTag: 'test-component', + styleText: '.new { color: blue; }', + }, + ]); + + expect(styleElm.innerHTML).toBe('.new { color: blue; }'); + expect(styleElm.getAttribute('data-hmr')).toBe(versionId); + + styleElm.remove(); + }); + + it('should remove style element when styleText is empty', () => { + const styleElm = document.createElement('style'); + styleElm.setAttribute('sty-id', 'sc-test-component'); + styleElm.innerHTML = '.old { color: red; }'; + document.head.appendChild(styleElm); + + hmrInlineStyles(document.documentElement, versionId, [ + { styleId: 'sc-test-component', styleTag: 'test-component', styleText: '' }, + ]); + + expect(document.querySelector('[sty-id="sc-test-component"]')).toBeNull(); + }); + + it('should create style element when CSS is added for the first time', () => { + // Create a component element (scoped, no shadow root) + const component = document.createElement('test-component'); + document.body.appendChild(component); + + // No existing style element for this component + expect(document.querySelector('[sty-id="sc-test-component"]')).toBeNull(); + + hmrInlineStyles(document.documentElement, versionId, [ + { + styleId: 'sc-test-component', + styleTag: 'test-component', + styleText: '.new { color: blue; }', + }, + ]); + + // Style element should be created in head + const styleElm = document.querySelector('[sty-id="sc-test-component"]'); + expect(styleElm).not.toBeNull(); + expect(styleElm!.innerHTML).toBe('.new { color: blue; }'); + expect(styleElm!.getAttribute('data-hmr')).toBe(versionId); + + component.remove(); + styleElm!.remove(); + }); + + it('should create style element in shadow root for shadow DOM components', () => { + // Create a shadow DOM component + const component = document.createElement('test-shadow-component'); + const shadowRoot = component.attachShadow({ mode: 'open' }); + document.body.appendChild(component); + + hmrInlineStyles(document.documentElement, versionId, [ + { + styleId: 'sc-test-shadow-component', + styleTag: 'test-shadow-component', + styleText: ':host { display: block; }', + }, + ]); + + // Style element should be created in shadow root + const styleElm = shadowRoot.querySelector('[sty-id="sc-test-shadow-component"]'); + expect(styleElm).not.toBeNull(); + expect(styleElm!.innerHTML).toBe(':host { display: block; }'); + + component.remove(); + }); + + it('should update style in shadow root', () => { + const component = document.createElement('test-shadow-component'); + const shadowRoot = component.attachShadow({ mode: 'open' }); + const styleElm = document.createElement('style'); + styleElm.setAttribute('sty-id', 'sc-test-shadow-component'); + styleElm.innerHTML = '.old { color: red; }'; + shadowRoot.appendChild(styleElm); + document.body.appendChild(component); + + hmrInlineStyles(document.documentElement, versionId, [ + { + styleId: 'sc-test-shadow-component', + styleTag: 'test-shadow-component', + styleText: ':host { display: block; }', + }, + ]); + + expect(styleElm.innerHTML).toBe(':host { display: block; }'); + expect(styleElm.getAttribute('data-hmr')).toBe(versionId); + + component.remove(); + }); +}); diff --git a/packages/dev-server/src/client/_test_/status.spec.ts b/packages/dev-server/src/client/_test_/status.spec.ts new file mode 100644 index 00000000000..ab3fd212221 --- /dev/null +++ b/packages/dev-server/src/client/_test_/status.spec.ts @@ -0,0 +1,74 @@ +/** + * @vitest-environment stencil + */ +import { beforeAll, describe, expect, it } from 'vitest'; + +import { initBuildStatus, updateFavIcon } from '../status'; + +describe('build-status', () => { + beforeAll(() => { + window.location.href = 'http://localhost:3000/'; + }); + it('should set error and remember org href', () => { + const linkElm = document.createElement('link'); + linkElm.href = 'org-icon.ico'; + linkElm.rel = 'shortcut icon'; + linkElm.type = 'org-type'; + document.head.appendChild(linkElm); + + initBuildStatus({ window: window }); + + expect(linkElm.dataset.href).toBe('http://localhost:3000/org-icon.ico'); + expect(linkElm.dataset.type).toBe('org-type'); + + updateFavIcon(linkElm, 'error'); + expect(linkElm.getAttribute('data-status')).toBe('error'); + expect(linkElm.type).toBe('image/x-icon'); + + // Cleanup + document.head.removeChild(linkElm); + }); + + it('should set pending status', () => { + const linkElm = document.createElement('link'); + linkElm.rel = 'icon'; + document.head.appendChild(linkElm); + + updateFavIcon(linkElm, 'pending'); + expect(linkElm.getAttribute('data-status')).toBe('pending'); + expect(linkElm.type).toBe('image/x-icon'); + expect(linkElm.href).toContain('data:image/png;base64,'); + + // Cleanup + document.head.removeChild(linkElm); + }); + + it('should set disabled status', () => { + const linkElm = document.createElement('link'); + linkElm.rel = 'icon'; + document.head.appendChild(linkElm); + + updateFavIcon(linkElm, 'disabled'); + expect(linkElm.getAttribute('data-status')).toBe('disabled'); + expect(linkElm.type).toBe('image/x-icon'); + + // Cleanup + document.head.removeChild(linkElm); + }); + + it('should restore original href on default status', () => { + const linkElm = document.createElement('link'); + linkElm.rel = 'icon'; + linkElm.dataset.href = 'http://example.com/my-icon.ico'; + linkElm.dataset.type = 'image/png'; + document.head.appendChild(linkElm); + + updateFavIcon(linkElm, 'default'); + expect(linkElm.getAttribute('data-status')).toBeNull(); + expect(linkElm.href).toBe('http://example.com/my-icon.ico'); + expect(linkElm.type).toBe('image/png'); + + // Cleanup + document.head.removeChild(linkElm); + }); +}); diff --git a/packages/dev-server/src/client/constants.ts b/packages/dev-server/src/client/constants.ts new file mode 100644 index 00000000000..c0f586aa269 --- /dev/null +++ b/packages/dev-server/src/client/constants.ts @@ -0,0 +1,20 @@ +/** + * Client-side constants for dev server. + */ + +export const DEV_SERVER_URL = '/~dev-server'; +export const DEV_SERVER_INIT_URL = `${DEV_SERVER_URL}-init`; +export const OPEN_IN_EDITOR_URL = `${DEV_SERVER_URL}-open-in-editor`; + +export const BUILD_LOG = 'devserver:buildlog'; +export const BUILD_RESULTS = 'devserver:buildresults'; +export const BUILD_STATUS = 'devserver:buildstatus'; + +export const NODE_TYPE_ELEMENT = 1; +export const NODE_TYPE_DOCUMENT_FRAGMENT = 11; + +// WebSocket reconnection settings +export const RECONNECT_ATTEMPTS = 1000; +export const RECONNECT_RETRY_MS = 2500; +export const NORMAL_CLOSURE_CODE = 1000; +export const REQUEST_BUILD_RESULTS_INTERVAL_MS = 500; diff --git a/packages/dev-server/src/client/css.d.ts b/packages/dev-server/src/client/css.d.ts new file mode 100644 index 00000000000..31058d4a514 --- /dev/null +++ b/packages/dev-server/src/client/css.d.ts @@ -0,0 +1,4 @@ +declare module "*.css" { + const content: string; + export default content; +} diff --git a/packages/dev-server/src/client/error.css b/packages/dev-server/src/client/error.css new file mode 100644 index 00000000000..9571c20d1bf --- /dev/null +++ b/packages/dev-server/src/client/error.css @@ -0,0 +1,309 @@ +#dev-server-modal * { + box-sizing: border-box !important; +} + +/* Backdrop overlay */ +#dev-server-modal { + direction: ltr !important; + display: block; + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + z-index: 99999 !important; + margin: 0 !important; + padding: 0 !important; + background: rgba(0, 0, 0, 0.66) !important; + overflow-y: auto !important; + -webkit-overflow-scrolling: touch !important; + font-family: + -apple-system, 'Roboto', BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' !important; + font-size: 14px !important; + line-height: 1.5 !important; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + text-size-adjust: none; + word-wrap: break-word; + user-select: auto; +} + +/* Modal window */ +#dev-server-modal-inner { + position: relative !important; + max-width: 80vw !important; + margin: 30px auto !important; + padding: 25px !important; + background-color: white !important; + border-radius: 8px !important; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5) !important; + color: #333 !important; +} + +.dev-server-diagnostic { + margin: 20px !important; + border: 1px solid #ddd !important; + border-radius: 3px !important; +} + +.dev-server-diagnostic-masthead { + padding: 8px 12px 12px 12px !important; +} + +.dev-server-diagnostic-title { + margin: 0 !important; + font-weight: bold !important; + color: #222 !important; +} + +.dev-server-diagnostic-message { + margin-top: 4px !important; + color: #555 !important; +} + +.dev-server-diagnostic-file { + position: relative !important; + border-top: 1px solid #ddd !important; +} + +.dev-server-diagnostic-file-header { + display: block !important; + padding: 5px 10px !important; + color: #555 !important; + border-bottom: 1px solid #ddd !important; + border-top-left-radius: 2px !important; + border-top-right-radius: 2px !important; + background-color: #f9f9f9 !important; + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; + font-size: 12px !important; +} + +a.dev-server-diagnostic-file-header { + color: #0000ee !important; + text-decoration: underline !important; +} + +a.dev-server-diagnostic-file-header:hover { + text-decoration: none !important; + background-color: #f4f4f4 !important; +} + +.dev-server-diagnostic-file-name { + font-weight: bold !important; +} + +.dev-server-diagnostic-blob { + overflow-x: auto !important; + overflow-y: hidden !important; + border-bottom-right-radius: 3px !important; + border-bottom-left-radius: 3px !important; +} + +.dev-server-diagnostic-table { + margin: 0 !important; + padding: 0 !important; + border-spacing: 0 !important; + border-collapse: collapse !important; + border-width: 0 !important; + border-style: none !important; + -moz-tab-size: 2; + tab-size: 2; +} + +.dev-server-diagnostic-table td, +.dev-server-diagnostic-table th { + padding: 0 !important; + border-width: 0 !important; + border-style: none !important; +} + +td.dev-server-diagnostic-blob-num { + padding-right: 10px !important; + padding-left: 10px !important; + width: 1% !important; + min-width: 50px !important; + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; + font-size: 12px !important; + line-height: 20px !important; + color: rgba(0, 0, 0, 0.3) !important; + text-align: right !important; + white-space: nowrap !important; + vertical-align: top !important; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + border: solid #eee !important; + border-width: 0 1px 0 0 !important; +} + +td.dev-server-diagnostic-blob-num::before { + content: attr(data-line-number) !important; +} + +.dev-server-diagnostic-error-line td.dev-server-diagnostic-blob-num { + background-color: #ffdddd !important; + border-color: #ffc9c9 !important; +} + +.dev-server-diagnostic-error-line td.dev-server-diagnostic-blob-code { + background: rgba(255, 221, 221, 0.25) !important; + z-index: -1; +} + +.dev-server-diagnostic-open-in-editor td.dev-server-diagnostic-blob-num:hover { + cursor: pointer; + background-color: #ffffe3 !important; + font-weight: bold; +} + +.dev-server-diagnostic-open-in-editor.dev-server-diagnostic-error-line + td.dev-server-diagnostic-blob-num:hover { + background-color: #ffdada !important; +} + +td.dev-server-diagnostic-blob-code { + position: relative !important; + padding-right: 10px !important; + padding-left: 10px !important; + line-height: 20px !important; + vertical-align: top !important; + overflow: visible !important; + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; + font-size: 12px !important; + color: #333 !important; + word-wrap: normal !important; + white-space: pre !important; +} + +td.dev-server-diagnostic-blob-code::before { + content: '' !important; +} + +.dev-server-diagnostic-error-chr { + position: relative !important; +} + +.dev-server-diagnostic-error-chr::before { + position: absolute !important; + z-index: -1; + top: -3px !important; + left: 0px !important; + width: 8px !important; + height: 20px !important; + background-color: #ffdddd !important; + content: '' !important; +} + +/** + * GitHub Gist Theme + * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro + * https://highlightjs.org/ + */ +.hljs-comment, +.hljs-meta { + color: #969896; +} + +.hljs-string, +.hljs-variable, +.hljs-template-variable, +.hljs-strong, +.hljs-emphasis, +.hljs-quote { + color: #df5000; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-type { + color: #a71d5d; +} + +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute { + color: #0086b3; +} + +.hljs-section, +.hljs-name { + color: #63a35c; +} + +.hljs-tag { + color: #333333; +} + +.hljs-title, +.hljs-attr, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #795da3; +} + +.hljs-addition { + color: #55a532; + background-color: #eaffea; +} + +.hljs-deletion { + color: #bd2c00; + background-color: #ffecec; +} + +.hljs-link { + text-decoration: underline; +} + +/* Error badge - bottom left corner indicator */ +.dev-server-error-badge { + position: fixed !important; + bottom: 20px !important; + left: 20px !important; + z-index: 99998 !important; + display: flex !important; + align-items: center !important; + gap: 8px !important; + padding: 10px 16px !important; + background: #ff5555 !important; + color: white !important; + border: none !important; + border-radius: 8px !important; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif !important; + font-size: 14px !important; + font-weight: 600 !important; + cursor: pointer !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; + transition: + transform 0.2s, + box-shadow 0.2s !important; +} + +.dev-server-error-badge:hover { + transform: translateY(-2px) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; +} + +.error-badge-icon { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 20px !important; + height: 20px !important; + background: white !important; + color: #ff5555 !important; + border-radius: 50% !important; + font-weight: bold !important; + font-size: 14px !important; +} + +.error-badge-count { + font-size: 14px !important; + font-weight: 600 !important; +} diff --git a/packages/dev-server/src/client/error.ts b/packages/dev-server/src/client/error.ts new file mode 100644 index 00000000000..ab42454f64c --- /dev/null +++ b/packages/dev-server/src/client/error.ts @@ -0,0 +1,393 @@ +// @ts-expect-error - tsdown doesn't yet have types for css imports +import appErrorCss from './error.css?inline'; +import type { CompilerBuildResults, Diagnostic, PrintLine } from './types'; + +interface AppErrorData { + window: Window; + buildResults: any; + openInEditor?: OpenInEditorCallback; +} + +type OpenInEditorCallback = (data: { file: string; line: number; column: number }) => void; + +interface AppErrorResults { + diagnostics: Diagnostic[]; + status: null | string; +} + +let errorCount = 0; + +export const appError = (data: AppErrorData): AppErrorResults => { + const results: AppErrorResults = { + diagnostics: [] as Diagnostic[], + status: null, + }; + + if (data && data.window && Array.isArray(data.buildResults.diagnostics)) { + const diagnostics = (data.buildResults as CompilerBuildResults).diagnostics.filter( + (diagnostic: Diagnostic) => diagnostic.level === 'error', + ); + + if (diagnostics.length > 0) { + errorCount = diagnostics.length; + const modal = getDevServerModal(data.window.document, data.openInEditor); + + diagnostics.forEach((diagnostic: Diagnostic) => { + results.diagnostics.push(diagnostic); + appendDiagnostic(data.window.document, data.openInEditor, modal, diagnostic); + }); + + removeErrorBadge(data.window.document); + results.status = 'error'; + } + } + + return results; +}; + +const appendDiagnostic = ( + doc: Document, + openInEditor: OpenInEditorCallback | undefined, + modal: HTMLElement, + diagnostic: Diagnostic, +) => { + const card = doc.createElement('div'); + card.className = 'dev-server-diagnostic'; + + const masthead = doc.createElement('div'); + masthead.className = 'dev-server-diagnostic-masthead'; + masthead.title = `${escapeHtml(diagnostic.type)} error: ${escapeHtml(diagnostic.code ?? 'unknown error')}`; + card.appendChild(masthead); + + const title = doc.createElement('div'); + title.className = 'dev-server-diagnostic-title'; + if (typeof diagnostic.header === 'string' && diagnostic.header.trim().length > 0) { + title.textContent = diagnostic.header; + } else { + title.textContent = `${titleCase(diagnostic.type)} ${titleCase(diagnostic.level)}`; + } + masthead.appendChild(title); + + const message = doc.createElement('div'); + message.className = 'dev-server-diagnostic-message'; + message.textContent = diagnostic.messageText; + masthead.appendChild(message); + + const file = doc.createElement('div'); + file.className = 'dev-server-diagnostic-file'; + card.appendChild(file); + + const isUrl = + typeof diagnostic.absFilePath === 'string' && diagnostic.absFilePath.indexOf('http') === 0; + const canOpenInEditor = + typeof openInEditor === 'function' && typeof diagnostic.absFilePath === 'string' && !isUrl; + + if (isUrl) { + const fileHeader = doc.createElement('a'); + fileHeader.href = diagnostic.absFilePath ?? ''; + fileHeader.setAttribute('target', '_blank'); + fileHeader.setAttribute('rel', 'noopener noreferrer'); + fileHeader.className = 'dev-server-diagnostic-file-header'; + + const filePath = doc.createElement('span'); + filePath.className = 'dev-server-diagnostic-file-path'; + filePath.textContent = diagnostic.absFilePath ?? ''; + + fileHeader.appendChild(filePath); + file.appendChild(fileHeader); + } else if (diagnostic.relFilePath) { + const fileHeader = doc.createElement(canOpenInEditor ? 'a' : 'div'); + fileHeader.className = 'dev-server-diagnostic-file-header'; + + if (diagnostic.absFilePath) { + fileHeader.title = escapeHtml(diagnostic.absFilePath); + + if (canOpenInEditor) { + addOpenInEditor( + openInEditor, + fileHeader, + diagnostic.absFilePath, + diagnostic.lineNumber, + diagnostic.columnNumber, + ); + } + } + + const parts = diagnostic.relFilePath.split('/'); + + const fileName = doc.createElement('span'); + fileName.className = 'dev-server-diagnostic-file-name'; + fileName.textContent = parts.pop() ?? ''; + + const filePath = doc.createElement('span'); + filePath.className = 'dev-server-diagnostic-file-path'; + filePath.textContent = parts.join('/') + '/'; + + fileHeader.appendChild(filePath); + fileHeader.appendChild(fileName); + file.appendChild(fileHeader); + } + + if (diagnostic.lines && diagnostic.lines.length > 0) { + const blob = doc.createElement('div'); + blob.className = 'dev-server-diagnostic-blob'; + file.appendChild(blob); + + const table = doc.createElement('table'); + table.className = 'dev-server-diagnostic-table'; + blob.appendChild(table); + + prepareLines(diagnostic.lines).forEach((l) => { + const tr = doc.createElement('tr'); + if (l.errorCharStart > 0) { + tr.classList.add('dev-server-diagnostic-error-line'); + } + if (canOpenInEditor) { + tr.classList.add('dev-server-diagnostic-open-in-editor'); + } + table.appendChild(tr); + + const tdNum = doc.createElement('td'); + tdNum.className = 'dev-server-diagnostic-blob-num'; + if (l.lineNumber > 0) { + tdNum.setAttribute('data-line-number', l.lineNumber + ''); + tdNum.title = escapeHtml(diagnostic.relFilePath ?? '') + ', line ' + l.lineNumber; + + const maybeFile = diagnostic.absFilePath; + if (canOpenInEditor && maybeFile) { + const column = l.lineNumber === diagnostic.lineNumber ? diagnostic.columnNumber : 1; + addOpenInEditor(openInEditor, tdNum, maybeFile, l.lineNumber, column); + } + } + tr.appendChild(tdNum); + + const tdCode = doc.createElement('td'); + tdCode.className = 'dev-server-diagnostic-blob-code'; + tdCode.innerHTML = highlightError(l.text ?? '', l.errorCharStart, l.errorLength ?? 0); + tr.appendChild(tdCode); + }); + } + + modal.appendChild(card); +}; + +const addOpenInEditor = ( + openInEditor: OpenInEditorCallback, + elm: HTMLElement, + file: string, + line: number | undefined, + column: number | undefined, +) => { + if (elm.tagName === 'A') { + (elm as HTMLAnchorElement).href = '#open-in-editor'; + } + + const lineNumber = typeof line !== 'number' || line < 1 ? 1 : line; + + const columnNumber = typeof column !== 'number' || column < 1 ? 1 : column; + + elm.addEventListener('click', (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + openInEditor({ + file: file, + line: lineNumber, + column: columnNumber, + }); + }); +}; + +const getDevServerModal = (doc: Document, _openInEditor?: OpenInEditorCallback): HTMLElement => { + let outer = doc.getElementById(DEV_SERVER_MODAL); + let isNewModal = false; + + if (!outer) { + isNewModal = true; + outer = doc.createElement('div'); + outer.id = DEV_SERVER_MODAL; + outer.setAttribute('role', 'dialog'); + doc.body.appendChild(outer); + + // Set up structure with style tag + outer.innerHTML = `
`; + + // Close on ESC key + const closeOnEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape' || e.code === 'Escape') { + closeDevServerModal(doc); + } + }; + doc.addEventListener('keydown', closeOnEsc); + (outer as any).__closeOnEsc = closeOnEsc; + + // Close on backdrop click (after innerHTML is set) + outer.addEventListener('click', (e) => { + if (e.target === outer) { + closeDevServerModal(doc); + } + }); + } + + // Reset display to show modal + outer.style.display = 'block'; + + const inner = doc.getElementById(`${DEV_SERVER_MODAL}-inner`) as HTMLElement; + + // Clear previous errors + inner.innerHTML = ''; + + // Prevent clicks inside modal from closing it (only set once) + if (isNewModal) { + inner.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } + + return inner; +}; + +const closeDevServerModal = (doc: Document) => { + const outer = doc.getElementById(DEV_SERVER_MODAL); + if (outer) { + outer.style.display = 'none'; + showErrorBadge(doc); + } +}; + +export const clearAppErrorModal = (data: { window: Window }) => { + const appErrorElm = data.window.document.getElementById(DEV_SERVER_MODAL); + if (appErrorElm) { + const closeOnEsc = (appErrorElm as any).__closeOnEsc; + if (closeOnEsc) { + data.window.document.removeEventListener('keydown', closeOnEsc); + } + if (appErrorElm.parentNode) { + appErrorElm.parentNode.removeChild(appErrorElm); + } + } + removeErrorBadge(data.window.document); + errorCount = 0; +}; + +const showErrorBadge = (doc: Document) => { + if (errorCount === 0) return; + + let badge = doc.getElementById(ERROR_BADGE_ID); + if (!badge) { + badge = doc.createElement('button'); + badge.id = ERROR_BADGE_ID; + badge.className = 'dev-server-error-badge'; + badge.setAttribute('aria-label', 'Show build errors'); + doc.body.appendChild(badge); + + badge.addEventListener('click', () => { + const modal = doc.getElementById(DEV_SERVER_MODAL); + if (modal) { + modal.style.display = 'block'; + removeErrorBadge(doc); + } + }); + } + + badge.innerHTML = `!${errorCount}`; + badge.style.display = 'flex'; +}; + +const removeErrorBadge = (doc: Document) => { + const badge = doc.getElementById(ERROR_BADGE_ID); + if (badge) { + badge.style.display = 'none'; + } +}; + +const escapeHtml = (unsafe: string) => { + if (typeof unsafe === 'number' || typeof unsafe === 'boolean') { + return (unsafe as any).toString(); + } + if (typeof unsafe === 'string') { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + return ''; +}; + +const titleCase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); + +const highlightError = (text: string, errorCharStart: number, errorLength: number) => { + if (typeof text !== 'string') { + return ''; + } + + const errorCharEnd = errorCharStart + errorLength; + + return text + .split('') + .map((inputChar, charIndex) => { + let outputChar: string; + + if (inputChar === `<`) { + outputChar = `<`; + } else if (inputChar === `>`) { + outputChar = `>`; + } else if (inputChar === `"`) { + outputChar = `"`; + } else if (inputChar === `'`) { + outputChar = `'`; + } else if (inputChar === `&`) { + outputChar = `&`; + } else { + outputChar = inputChar; + } + + if (charIndex >= errorCharStart && charIndex < errorCharEnd) { + outputChar = `${outputChar}`; + } + + return outputChar; + }) + .join(''); +}; + +const prepareLines = (orgLines: PrintLine[]) => { + const lines: PrintLine[] = JSON.parse(JSON.stringify(orgLines)); + + for (let x = 0; x < 100; x++) { + if (!eachLineHasLeadingWhitespace(lines)) { + return lines; + } + for (let i = 0; i < lines.length; i++) { + lines[i].text = lines[i].text?.slice(1) ?? ''; + lines[i].errorCharStart--; + if (!lines[i].text?.length) { + return lines; + } + } + } + + return lines; +}; + +const eachLineHasLeadingWhitespace = (lines: PrintLine[]) => { + if (!lines.length) { + return false; + } + + for (let i = 0; i < lines.length; i++) { + if (!lines[i].text || (lines[i].text?.length ?? 0) < 1) { + return false; + } + const firstChar = lines[i].text?.charAt(0); + if (firstChar !== ' ' && firstChar !== '\t') { + return false; + } + } + + return true; +}; + +const DEV_SERVER_MODAL = `dev-server-modal`; +const ERROR_BADGE_ID = 'dev-server-error-badge'; diff --git a/packages/dev-server/src/client/events.ts b/packages/dev-server/src/client/events.ts new file mode 100644 index 00000000000..d7ae22461f5 --- /dev/null +++ b/packages/dev-server/src/client/events.ts @@ -0,0 +1,39 @@ +/** + * Client-side event system for dev server. + */ + +import { BUILD_LOG, BUILD_RESULTS, BUILD_STATUS } from './constants'; +import type { BuildLog, CompilerBuildResults } from './types'; + +export const emitBuildLog = (win: Window, buildLog: BuildLog): void => { + win.dispatchEvent(new CustomEvent(BUILD_LOG, { detail: buildLog })); +}; + +export const emitBuildResults = (win: Window, buildResults: CompilerBuildResults): void => { + win.dispatchEvent(new CustomEvent(BUILD_RESULTS, { detail: buildResults })); +}; + +export const emitBuildStatus = (win: Window, buildStatus: string): void => { + win.dispatchEvent(new CustomEvent(BUILD_STATUS, { detail: buildStatus })); +}; + +export const onBuildLog = (win: Window, cb: (buildLog: BuildLog) => void): void => { + win.addEventListener(BUILD_LOG, ((ev: CustomEvent) => { + cb(ev.detail); + }) as EventListener); +}; + +export const onBuildResults = ( + win: Window, + cb: (buildResults: CompilerBuildResults) => void, +): void => { + win.addEventListener(BUILD_RESULTS, ((ev: CustomEvent) => { + cb(ev.detail); + }) as EventListener); +}; + +export const onBuildStatus = (win: Window, cb: (buildStatus: string) => void): void => { + win.addEventListener(BUILD_STATUS, ((ev: CustomEvent) => { + cb(ev.detail); + }) as EventListener); +}; diff --git a/packages/dev-server/src/client/hmr/component.ts b/packages/dev-server/src/client/hmr/component.ts new file mode 100644 index 00000000000..b20c9b4f677 --- /dev/null +++ b/packages/dev-server/src/client/hmr/component.ts @@ -0,0 +1,45 @@ +import { setHmrAttr, hasShadowRoot } from './utils'; +import type { HostElement } from '../types'; + +export const hmrComponents = ( + element: Element, + versionId: string, + hmrTagNames: string[], +): string[] => { + const updatedTags: string[] = []; + + hmrTagNames.forEach((hmrTagName) => { + hmrComponent(updatedTags, element, versionId, hmrTagName); + }); + + return updatedTags.sort(); +}; + +const hmrComponent = ( + updatedTags: string[], + element: Element, + versionId: string, + cmpTagName: string, +): void => { + if ( + element.nodeName.toLowerCase() === cmpTagName && + typeof (element as HostElement)['s-hmr'] === 'function' + ) { + (element as HostElement)['s-hmr']!(versionId); + setHmrAttr(element, versionId); + + if (updatedTags.indexOf(cmpTagName) === -1) { + updatedTags.push(cmpTagName); + } + } + + if (hasShadowRoot(element)) { + hmrComponent(updatedTags, element.shadowRoot as unknown as Element, versionId, cmpTagName); + } + + if (element.children) { + for (let i = 0; i < element.children.length; i++) { + hmrComponent(updatedTags, element.children[i], versionId, cmpTagName); + } + } +}; diff --git a/packages/dev-server/src/client/hmr/image.ts b/packages/dev-server/src/client/hmr/image.ts new file mode 100644 index 00000000000..7d988b88784 --- /dev/null +++ b/packages/dev-server/src/client/hmr/image.ts @@ -0,0 +1,205 @@ +import { + getHmrHref, + hasShadowRoot, + isElement, + isLinkStylesheet, + isTemplate, + setHmrAttr, + setHmrQueryString, + setQueryString, + updateCssUrlValue, +} from './utils'; + +export const hmrImages = ( + win: Window, + doc: Document, + versionId: string, + imageFileNames: string[], +): string[] => { + if (win.location.protocol !== 'file:' && doc.styleSheets) { + hmrStyleSheetsImages(doc, versionId, imageFileNames); + } + + hmrImagesElements(win, doc.documentElement, versionId, imageFileNames); + + return imageFileNames.sort(); +}; + +const hmrStyleSheetsImages = (doc: Document, versionId: string, imageFileNames: string[]): void => { + const cssImageProps = Object.keys(doc.documentElement.style).filter((cssProp) => { + return cssProp.endsWith('Image'); + }); + + for (let i = 0; i < doc.styleSheets.length; i++) { + hmrStyleSheetImages( + cssImageProps, + doc.styleSheets[i] as CSSStyleSheet, + versionId, + imageFileNames, + ); + } +}; + +const hmrStyleSheetImages = ( + cssImageProps: string[], + styleSheet: CSSStyleSheet, + versionId: string, + imageFileNames: string[], +): void => { + try { + const cssRules = styleSheet.cssRules; + for (let i = 0; i < cssRules.length; i++) { + const cssRule = cssRules[i]; + + switch (cssRule.type) { + case CSSRule.IMPORT_RULE: + hmrStyleSheetImages( + cssImageProps, + (cssRule as CSSImportRule).styleSheet!, + versionId, + imageFileNames, + ); + break; + case CSSRule.STYLE_RULE: + hmrStyleSheetRuleImages( + cssImageProps, + cssRule as CSSStyleRule, + versionId, + imageFileNames, + ); + break; + case CSSRule.MEDIA_RULE: + hmrStyleSheetImages( + cssImageProps, + cssRule as unknown as CSSStyleSheet, + versionId, + imageFileNames, + ); + break; + } + } + } catch (e) { + console.error('hmrStyleSheetImages:', e); + } +}; + +const hmrStyleSheetRuleImages = ( + cssImageProps: string[], + cssRule: CSSStyleRule, + versionId: string, + imageFileNames: string[], +): void => { + cssImageProps.forEach((cssImageProp) => { + imageFileNames.forEach((imageFileName) => { + const oldCssText = (cssRule.style as unknown as Record)[cssImageProp]; + const newCssText = updateCssUrlValue(versionId, imageFileName, oldCssText); + + if (oldCssText !== newCssText) { + (cssRule.style as unknown as Record)[cssImageProp] = newCssText; + } + }); + }); +}; + +const hmrImagesElements = ( + win: Window, + elm: Element, + versionId: string, + imageFileNames: string[], +): void => { + const tagName = elm.nodeName.toLowerCase(); + + if (tagName === 'img') { + hmrImgElement(elm as HTMLImageElement, versionId, imageFileNames); + } + + if (isElement(elm)) { + const styleAttr = elm.getAttribute('style'); + if (styleAttr) { + hmrUpdateStyleAttr(elm, versionId, imageFileNames, styleAttr); + } + } + + if (tagName === 'style') { + hmrUpdateStyleElementUrl(elm as HTMLStyleElement, versionId, imageFileNames); + } + + if (win.location.protocol !== 'file:' && isLinkStylesheet(elm)) { + hmrUpdateLinkElementUrl(elm as HTMLLinkElement, versionId, imageFileNames); + } + + if (isTemplate(elm)) { + hmrImagesElements( + win, + (elm as HTMLTemplateElement).content as unknown as Element, + versionId, + imageFileNames, + ); + } + + if (hasShadowRoot(elm)) { + hmrImagesElements(win, elm.shadowRoot as unknown as Element, versionId, imageFileNames); + } + + if (elm.children) { + for (let i = 0; i < elm.children.length; i++) { + hmrImagesElements(win, elm.children[i], versionId, imageFileNames); + } + } +}; + +const hmrImgElement = ( + imgElm: HTMLImageElement, + versionId: string, + imageFileNames: string[], +): void => { + imageFileNames.forEach((imageFileName) => { + const orgSrc = imgElm.getAttribute('src'); + const newSrc = getHmrHref(versionId, imageFileName, orgSrc || ''); + if (newSrc !== orgSrc) { + imgElm.setAttribute('src', newSrc); + setHmrAttr(imgElm, versionId); + } + }); +}; + +const hmrUpdateStyleElementUrl = ( + styleElm: HTMLStyleElement, + versionId: string, + imageFileNames: string[], +): void => { + imageFileNames.forEach((imageFileName) => { + const oldCssText = styleElm.innerHTML; + const newCssText = updateCssUrlValue(versionId, imageFileName, oldCssText); + if (newCssText !== oldCssText) { + styleElm.innerHTML = newCssText; + setHmrAttr(styleElm, versionId); + } + }); +}; + +const hmrUpdateLinkElementUrl = ( + linkElm: HTMLLinkElement, + versionId: string, + imageFileNames: string[], +): void => { + linkElm.href = setQueryString(linkElm.href, 's-hmr-urls', imageFileNames.sort().join(',')); + linkElm.href = setHmrQueryString(linkElm.href, versionId); + linkElm.setAttribute('data-hmr', versionId); +}; + +const hmrUpdateStyleAttr = ( + elm: Element, + versionId: string, + imageFileNames: string[], + oldStyleAttr: string, +) => { + imageFileNames.forEach((imageFileName) => { + const newStyleAttr = updateCssUrlValue(versionId, imageFileName, oldStyleAttr); + + if (newStyleAttr !== oldStyleAttr) { + elm.setAttribute('style', newStyleAttr); + setHmrAttr(elm, versionId); + } + }); +}; diff --git a/packages/dev-server/src/client/hmr/style.ts b/packages/dev-server/src/client/hmr/style.ts new file mode 100644 index 00000000000..f7624a0b2d8 --- /dev/null +++ b/packages/dev-server/src/client/hmr/style.ts @@ -0,0 +1,284 @@ +import { HmrStyleUpdate } from '../types'; +import { + getHmrHref, + hasShadowRoot, + isElement, + isLinkStylesheet, + isTemplate, + setHmrAttr, +} from './utils'; + +// Attribute used to identify style elements for HMR (matches HYDRATED_STYLE_ID in core) +const STYLE_ID_ATTR = 'sty-id'; + +// ============================================================================= +// HMR External Styles +// ============================================================================= + +export const hmrExternalStyles = ( + elm: Element, + versionId: string, + cssFileNames: string[], +): string[] => { + if (isLinkStylesheet(elm)) { + cssFileNames.forEach((cssFileName) => { + hmrStylesheetLink(elm as HTMLLinkElement, versionId, cssFileName); + }); + } + + if (isTemplate(elm)) { + hmrExternalStyles( + (elm as HTMLTemplateElement).content as unknown as Element, + versionId, + cssFileNames, + ); + } + + if (hasShadowRoot(elm)) { + hmrExternalStyles(elm.shadowRoot as unknown as Element, versionId, cssFileNames); + } + + if (elm.children) { + for (let i = 0; i < elm.children.length; i++) { + hmrExternalStyles(elm.children[i], versionId, cssFileNames); + } + } + + return cssFileNames.sort(); +}; + +const hmrStylesheetLink = ( + styleSheetElm: HTMLLinkElement, + versionId: string, + cssFileName: string, +): void => { + const orgHref = styleSheetElm.getAttribute('href'); + const newHref = getHmrHref(versionId, cssFileName, styleSheetElm.href); + if (newHref !== orgHref) { + styleSheetElm.setAttribute('href', newHref); + setHmrAttr(styleSheetElm, versionId); + } +}; + +// ============================================================================= +// HMR Inline Styles +// ============================================================================= + +/** + * Tracks which style updates actually found and updated existing style elements. + * Used to determine if we need to create new style elements for first-time CSS. + */ +interface StyleUpdateTracker { + styleUpdate: HmrStyleUpdate; + updated: boolean; +} + +export const hmrInlineStyles = ( + elm: Element, + versionId: string, + stylesUpdatedData: HmrStyleUpdate[], +): string[] => { + // Track which style updates actually found matching elements + const trackers: StyleUpdateTracker[] = stylesUpdatedData.map((styleUpdate) => ({ + styleUpdate, + updated: false, + })); + + // First pass: update or remove existing style elements + hmrInlineStylesTraverse(elm, versionId, trackers); + + // Second pass: create style elements for styles that had no matches (CSS added for first time) + for (const tracker of trackers) { + if (!tracker.updated && tracker.styleUpdate.styleText) { + // This style update didn't find any existing elements - CSS was added for first time + createStyleElementsForComponent(elm, versionId, tracker.styleUpdate); + } + } + + return stylesUpdatedData + .map((s) => s.styleTag) + .reduce((arr, v) => { + if (arr.indexOf(v) === -1) { + arr.push(v); + } + return arr; + }, []) + .sort(); +}; + +/** + * Traverse the DOM looking for style elements to update or remove. + * @param elm - the element to start traversal from + * @param versionId - the HMR version identifier + * @param trackers - the style update trackers + */ +const hmrInlineStylesTraverse = ( + elm: Element, + versionId: string, + trackers: StyleUpdateTracker[], +): void => { + if (isElement(elm) && elm.nodeName.toLowerCase() === 'style') { + trackers.forEach((tracker) => { + if (hmrStyleElement(elm, versionId, tracker.styleUpdate)) { + tracker.updated = true; + } + }); + } + + if (isTemplate(elm)) { + hmrInlineStylesTraverse( + (elm as HTMLTemplateElement).content as unknown as Element, + versionId, + trackers, + ); + } + + if (hasShadowRoot(elm)) { + hmrInlineStylesTraverse(elm.shadowRoot as unknown as Element, versionId, trackers); + } + + if (elm.children) { + for (let i = 0; i < elm.children.length; i++) { + hmrInlineStylesTraverse(elm.children[i], versionId, trackers); + } + } +}; + +// Slot-fallback CSS appended at runtime by the Stencil renderer for slot-patched components. +// Must be re-appended after HMR overwrites the style text. +const SLOT_FB_CSS = 'slot-fb{display:contents}slot-fb[hidden]{display:none}'; + +/** + * Update or remove a style element based on the HMR update. + * @param elm - the style element to update + * @param versionId - the HMR version identifier + * @param stylesUpdated - the HMR style update data + * @returns true if this element matched and was processed + */ +const hmrStyleElement = ( + elm: Element, + versionId: string, + stylesUpdated: HmrStyleUpdate, +): boolean => { + const styleId = elm.getAttribute(STYLE_ID_ATTR); + if (styleId === stylesUpdated.styleId) { + if (stylesUpdated.styleText) { + // Re-append slot-fb CSS if it was originally added at runtime + const slotFbSuffix = elm.hasAttribute('data-slot-fb') ? SLOT_FB_CSS : ''; + // Update existing style element + elm.innerHTML = stylesUpdated.styleText.replace(/\\n/g, '\n') + slotFbSuffix; + elm.setAttribute('data-hmr', versionId); + } else { + // CSS was removed entirely - remove the style element + elm.remove(); + } + return true; + } + return false; +}; + +/** + * Find all component instances with the matching tag name and create style elements. + * Handles both shadow DOM components (style in shadow root) and scoped components (style in head). + * @param rootElm - the root element to search from + * @param versionId - the HMR version identifier + * @param styleUpdate - the HMR style update data + */ +const createStyleElementsForComponent = ( + rootElm: Element, + versionId: string, + styleUpdate: HmrStyleUpdate, +): void => { + const { styleTag, styleId, styleText } = styleUpdate; + const doc = rootElm.ownerDocument; + + // Find all component instances with the matching tag name + const componentInstances = findComponentInstances(rootElm, styleTag); + + if (componentInstances.length === 0) { + // No component instances found - might be a global style or component not in DOM yet + // Create style in document head as fallback + createStyleElement(doc.head, styleId, styleText, versionId); + return; + } + + // Track which shadow roots we've already added styles to (avoid duplicates) + const processedShadowRoots = new Set(); + let addedToHead = false; + + for (const instance of componentInstances) { + if (instance.shadowRoot) { + // Shadow DOM component - add style to shadow root if not already there + if (!processedShadowRoots.has(instance.shadowRoot)) { + processedShadowRoots.add(instance.shadowRoot); + createStyleElement(instance.shadowRoot, styleId, styleText, versionId); + } + } else if (!addedToHead) { + // Scoped component - add style to document head (only once) + addedToHead = true; + createStyleElement(doc.head, styleId, styleText, versionId); + } + } +}; + +/** + * Find all instances of a component by tag name, including in shadow roots. + * @param elm - the element to search from + * @param tagName - the tag name to search for + * @returns an array of matching elements + */ +const findComponentInstances = (elm: Element, tagName: string): Element[] => { + const instances: Element[] = []; + findComponentInstancesTraverse(elm, tagName.toLowerCase(), instances); + return instances; +}; + +const findComponentInstancesTraverse = ( + elm: Element, + tagName: string, + instances: Element[], +): void => { + if (elm.nodeName.toLowerCase() === tagName) { + instances.push(elm); + } + + if (hasShadowRoot(elm)) { + findComponentInstancesTraverse(elm.shadowRoot as unknown as Element, tagName, instances); + } + + if (elm.children) { + for (let i = 0; i < elm.children.length; i++) { + findComponentInstancesTraverse(elm.children[i], tagName, instances); + } + } +}; + +/** + * Create a new style element with the given content. + * @param container - the container to insert the style element into + * @param styleId - the style identifier + * @param styleText - the CSS content + * @param versionId - the HMR version identifier + */ +const createStyleElement = ( + container: Element | ShadowRoot, + styleId: string, + styleText: string, + versionId: string, +): void => { + const doc = + 'ownerDocument' in container + ? container.ownerDocument + : (container as ShadowRoot).ownerDocument; + const styleElm = doc.createElement('style'); + styleElm.innerHTML = styleText.replace(/\\n/g, '\n'); + styleElm.setAttribute(STYLE_ID_ATTR, styleId); + styleElm.setAttribute('data-hmr', versionId); + + // Insert at the beginning of the container (prepend) + if (container.firstChild) { + container.insertBefore(styleElm, container.firstChild); + } else { + container.appendChild(styleElm); + } +}; diff --git a/packages/dev-server/src/client/hmr/utils.ts b/packages/dev-server/src/client/hmr/utils.ts new file mode 100644 index 00000000000..f5b7ecfd98e --- /dev/null +++ b/packages/dev-server/src/client/hmr/utils.ts @@ -0,0 +1,111 @@ +import { NODE_TYPE_DOCUMENT_FRAGMENT, NODE_TYPE_ELEMENT } from '../constants'; + +export const getHmrHref = (versionId: string, fileName: string, testUrl: string) => { + if (typeof testUrl === 'string' && testUrl.trim() !== '') { + if (getUrlFileName(fileName) === getUrlFileName(testUrl)) { + // only compare by filename w/out querystrings, not full path + return setHmrQueryString(testUrl, versionId); + } + } + return testUrl; +}; + +const getUrlFileName = (url: string) => { + // not using URL because IE11 doesn't support it + const splt = url.split('/'); + return splt[splt.length - 1].split('&')[0].split('?')[0]; +}; + +const parseQuerystring = (oldQs: string) => { + const newQs: { [key: string]: string } = {}; + if (typeof oldQs === 'string') { + oldQs.split('&').forEach((kv) => { + const splt = kv.split('='); + newQs[splt[0]] = splt[1] ? splt[1] : ''; + }); + } + return newQs; +}; + +const stringifyQuerystring = (qs: { [key: string]: string }) => + Object.keys(qs) + .map((key) => key + '=' + qs[key]) + .join('&'); + +export const setQueryString = (url: string, qsKey: string, qsValue: string) => { + // not using URL because IE11 doesn't support it + const urlSplt = url.split('?'); + const urlPath = urlSplt[0]; + const qs = parseQuerystring(urlSplt[1]); + qs[qsKey] = qsValue; + return urlPath + '?' + stringifyQuerystring(qs); +}; + +export const setHmrQueryString = (url: string, versionId: string) => + setQueryString(url, 's-hmr', versionId); + +export const updateCssUrlValue = (versionId: string, fileName: string, oldCss: string) => { + const reg = /url\((['"]?)(.*)\1\)/gi; + let result; + let newCss = oldCss; + + while ((result = reg.exec(oldCss)) !== null) { + const url = result[2]; + newCss = newCss.replace(url, getHmrHref(versionId, fileName, url)); + } + + return newCss; +}; + +/** + * Determine whether a given element is a `` tag pointing to a stylesheet + * + * @param elm the element to check + * @returns whether or not the element is a link stylesheet + */ +export const isLinkStylesheet = (elm: Element): boolean => + elm.nodeName.toLowerCase() === 'link' && + !!(elm as HTMLLinkElement).href && + !!(elm as HTMLLinkElement).rel && + (elm as HTMLLinkElement).rel.toLowerCase() === 'stylesheet'; + +/** + * Determine whether or not a given element is a template element + * + * @param elm the element to check + * @returns whether or not the element of interest is a template element + */ +export const isTemplate = (elm: Element) => + elm.nodeName.toLowerCase() === 'template' && + !!(elm as HTMLTemplateElement).content && + (elm as HTMLTemplateElement).content.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT; + +/** + * Set a new hmr version ID into the `data-hmr` attribute on an element. + * + * @param elm the element on which to set the property + * @param versionId a new HMR version id + */ +export const setHmrAttr = (elm: Element, versionId: string) => { + elm.setAttribute('data-hmr', versionId); +}; + +/** + * Determine whether or not an element has a shadow root + * + * @param elm the element to check + * @returns whether or not it has a shadow root + */ +export const hasShadowRoot = (elm: Element): boolean => + !!elm.shadowRoot && + elm.shadowRoot.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && + elm.shadowRoot !== (elm as any); + +/** + * Determine whether or not an element is an element node + * + * @param elm the element to check + * @returns whether or not it is an element node + */ +export const isElement = (elm: Element): boolean => + !!elm && elm.nodeType === NODE_TYPE_ELEMENT && !!elm.getAttribute; diff --git a/packages/dev-server/src/client/hmr/window.ts b/packages/dev-server/src/client/hmr/window.ts new file mode 100644 index 00000000000..d249a19a178 --- /dev/null +++ b/packages/dev-server/src/client/hmr/window.ts @@ -0,0 +1,69 @@ +import { hmrComponents } from './component'; +import { hmrImages } from './image'; +import { hmrInlineStyles, hmrExternalStyles } from './style'; +import { setHmrAttr } from './utils'; +import type { HmrResults, HotModuleReplacement } from '../types'; + +interface HmrWindowData { + window: Window; + hmr: HotModuleReplacement; +} + +export const hmrWindow = (data: HmrWindowData): HmrResults => { + const results: HmrResults = { + updatedComponents: [], + updatedExternalStyles: [], + updatedInlineStyles: [], + updatedImages: [], + versionId: '', + }; + + try { + if ( + !data || + !data.window || + !data.window.document.documentElement || + !data.hmr || + typeof data.hmr.versionId !== 'string' + ) { + return results; + } + + const win = data.window; + const doc = win.document; + const hmr = data.hmr; + const documentElement = doc.documentElement; + const versionId = hmr.versionId!; + results.versionId = versionId; + + if (hmr.componentsUpdated) { + results.updatedComponents = hmrComponents(documentElement, versionId, hmr.componentsUpdated); + } + + if (hmr.inlineStylesUpdated) { + results.updatedInlineStyles = hmrInlineStyles( + documentElement, + versionId, + hmr.inlineStylesUpdated, + ); + } + + if (hmr.externalStylesUpdated) { + results.updatedExternalStyles = hmrExternalStyles( + documentElement, + versionId, + hmr.externalStylesUpdated, + ); + } + + if (hmr.imagesUpdated) { + results.updatedImages = hmrImages(win, doc, versionId, hmr.imagesUpdated); + } + + setHmrAttr(documentElement, versionId); + } catch (e) { + console.error(e); + } + + return results; +}; diff --git a/packages/dev-server/src/client/index.ts b/packages/dev-server/src/client/index.ts new file mode 100644 index 00000000000..ef0cc08d5b1 --- /dev/null +++ b/packages/dev-server/src/client/index.ts @@ -0,0 +1,290 @@ +/** + * Stencil Dev Server Client + * + * Browser-side HMR (Hot Module Replacement) client for Stencil dev server. + * Handles WebSocket communication, component updates, style updates, and image updates. + * + * This module runs in the browser and is injected into pages during development. + * @module @stencil/dev-server/client + */ + +import { DEV_SERVER_INIT_URL, OPEN_IN_EDITOR_URL } from './constants'; +import { appError, clearAppErrorModal } from './error'; +import { emitBuildStatus, onBuildResults } from './events'; +import { hmrWindow } from './hmr/window'; +import { logBuild, logDiagnostic, logReload, logWarn } from './logger'; +import { initBuildProgress, initBuildStatus } from './status'; +import { initClientWebSocket } from './websocket'; +import type { + CompilerBuildResults, + DevClientConfig, + DevClientWindow, + HotModuleReplacement, +} from './types'; + +// Re-export everything for external use +export * from './constants'; +export * from './error'; +export * from './events'; +export * from './hmr/window'; +export * from './logger'; +export * from './status'; +export * from './types'; +export { initClientWebSocket } from './websocket'; + +// ============================================================================= +// App Update Handler +// ============================================================================= + +/** + * Initialize the app update handler for build results. + * + * @param win - the dev client window object + * @param config - the dev client configuration + */ +const initAppUpdate = (win: DevClientWindow, config: DevClientConfig): void => { + onBuildResults(win, (buildResults) => { + appUpdate(win, config, buildResults); + }); +}; + +/** + * Process app update based on build results. + * + * @param win - the dev client window object + * @param config - the dev client configuration + * @param buildResults - the compiler build results + */ +const appUpdate = ( + win: DevClientWindow, + config: DevClientConfig, + buildResults: CompilerBuildResults, +): void => { + try { + if (buildResults.buildId === win['s-build-id']) { + return; + } + win['s-build-id'] = buildResults.buildId; + + clearAppErrorModal({ window: win }); + + if (buildResults.hasError) { + const hasEditors = Array.isArray(config.editors) && config.editors.length > 0; + const errorResults = appError({ + window: win, + buildResults, + openInEditor: hasEditors + ? (data) => { + // Don't pass editor param - let launch-editor auto-detect + const params = new URLSearchParams({ + file: data.file, + line: String(data.line), + column: String(data.column), + }); + const url = `${OPEN_IN_EDITOR_URL}?${params.toString()}`; + win.fetch(url).catch((err) => { + console.error('Failed to open in editor:', err); + }); + } + : undefined, + }); + + errorResults.diagnostics.forEach(logDiagnostic); + if (errorResults.status) { + emitBuildStatus(win, errorResults.status); + } + + // If this is initial load, still forward to the page (with error overlay) + if (win['s-initial-load']) { + appReset(win, config, () => { + logReload('Initial load (with errors)'); + win.location.reload(); + }); + } + + return; + } + + if (win['s-initial-load']) { + appReset(win, config, () => { + logReload('Initial load'); + win.location.reload(); + }); + return; + } + + if (buildResults.hmr) { + appHmr(win, buildResults.hmr); + } + } catch (e) { + console.error(e); + } +}; + +/** + * Apply hot module replacement updates to the window. + * + * @param win - the browser window + * @param hmr - the hot module replacement data + */ +const appHmr = (win: Window, hmr: HotModuleReplacement): void => { + let shouldWindowReload = false; + + if (hmr.reloadStrategy === 'pageReload') { + shouldWindowReload = true; + } + + if (hmr.indexHtmlUpdated) { + logReload('Updated index.html'); + shouldWindowReload = true; + } + + if (hmr.serviceWorkerUpdated) { + logReload('Updated Service Worker: sw'); + shouldWindowReload = true; + } + + if (hmr.scriptsAdded && hmr.scriptsAdded.length > 0) { + logReload(`Added scripts: ${hmr.scriptsAdded.join(', ')}`); + shouldWindowReload = true; + } + + if (hmr.scriptsDeleted && hmr.scriptsDeleted.length > 0) { + logReload(`Deleted scripts: ${hmr.scriptsDeleted.join(', ')}`); + shouldWindowReload = true; + } + + if (hmr.excludeHmr && hmr.excludeHmr.length > 0) { + logReload(`Excluded From Hmr: ${hmr.excludeHmr.join(', ')}`); + shouldWindowReload = true; + } + + if (shouldWindowReload) { + win.location.reload(); + return; + } + + const results = hmrWindow({ window: win, hmr }); + + if (results.updatedComponents.length > 0) { + logBuild( + `Updated component${results.updatedComponents.length > 1 ? 's' : ''}: ${results.updatedComponents.join(', ')}`, + ); + } + + if (results.updatedInlineStyles.length > 0) { + logBuild(`Updated styles: ${results.updatedInlineStyles.join(', ')}`); + } + + if (results.updatedExternalStyles.length > 0) { + logBuild(`Updated stylesheets: ${results.updatedExternalStyles.join(', ')}`); + } + + if (results.updatedImages.length > 0) { + logBuild(`Updated images: ${results.updatedImages.join(', ')}`); + } +}; + +/** + * Reset the app state and unregister service workers. + * + * @param win - the dev client window object + * @param config - the dev client configuration + * @param cb - callback to invoke after reset + */ +const appReset = (win: DevClientWindow, config: DevClientConfig, cb: () => void): void => { + win.history.replaceState({}, 'App', config.basePath); + + if (!win.navigator.serviceWorker?.getRegistration) { + cb(); + } else { + win.navigator.serviceWorker + .getRegistration() + .then((swRegistration) => { + if (swRegistration) { + swRegistration.unregister().then((hasUnregistered) => { + if (hasUnregistered) { + logBuild('unregistered service worker'); + } + cb(); + }); + } else { + cb(); + } + }) + .catch((err) => { + logWarn('Service Worker', err); + cb(); + }); + } +}; + +// ============================================================================= +// Initialize Dev Client +// ============================================================================= + +/** + * Initialize the dev server client in the browser. + * + * @param win - the dev client window object + * @param config - the dev client configuration + */ +export const initDevClient = (win: DevClientWindow, config: DevClientConfig): void => { + try { + if (win['s-dev-server']) { + return; + } + win['s-dev-server'] = true; + + // Store config on window for debugging + win.devServerConfig = config; + + initBuildStatus({ window: win }); + initBuildProgress({ window: win }); + initAppUpdate(win, config); + + if (isInitialDevServerLoad(win, config)) { + win['s-initial-load'] = true; + appReset(win, config, () => { + initClientWebSocket(win, config); + }); + } else { + initClientWebSocket(win, config); + } + } catch (e) { + console.error(e); + } +}; + +/** + * Check if this is the initial dev server load. + * + * @param win - the dev client window object + * @param config - the dev client configuration + * @returns true if this is the initial load + */ +const isInitialDevServerLoad = (win: DevClientWindow, config: DevClientConfig): boolean => { + let pathname = win.location.pathname; + pathname = '/' + pathname.substring(config.basePath.length); + return pathname === DEV_SERVER_INIT_URL; +}; + +// ============================================================================= +// Auto-initialize +// ============================================================================= + +declare const appWindow: DevClientWindow | undefined; +declare const config: DevClientConfig | undefined; + +if (typeof appWindow !== 'undefined' && typeof config !== 'undefined') { + const defaultConfig: DevClientConfig = { + basePath: appWindow.location.pathname, + editors: [], + reloadStrategy: 'hmr', + socketUrl: `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.hostname}${ + location.port !== '' ? ':' + location.port : '' + }/`, + }; + + initDevClient(appWindow, { ...defaultConfig, ...appWindow.devServerConfig, ...config }); +} diff --git a/packages/dev-server/src/client/logger.ts b/packages/dev-server/src/client/logger.ts new file mode 100644 index 00000000000..038a247c4df --- /dev/null +++ b/packages/dev-server/src/client/logger.ts @@ -0,0 +1,56 @@ +/** + * Client-side logging utilities for dev server. + */ + +import type { Diagnostic } from './types'; + +const YELLOW = '#f39c12'; +const RED = '#c0392b'; +const BLUE = '#3498db'; +const GRAY = '#717171'; + +const log = (color: string, prefix: string, msg: string): void => { + console.log( + '%c' + prefix, + `background: ${color}; color: white; padding: 2px 3px; border-radius: 2px; font-size: 0.8em;`, + msg, + ); +}; + +export const logBuild = (msg: string): void => log(BLUE, 'Build', msg); +export const logReload = (msg: string): void => logWarn('Reload', msg); +export const logWarn = (prefix: string, msg: string): void => log(YELLOW, prefix, msg); +export const logDisabled = (prefix: string, msg: string): void => log(GRAY, prefix, msg); + +export const logDiagnostic = (diag: Diagnostic): void => { + let color = RED; + let prefix = 'Error'; + + if (diag.level === 'warn') { + color = YELLOW; + prefix = 'Warning'; + } + + if (diag.header) { + prefix = diag.header; + } + + let msg = ''; + + if (diag.relFilePath) { + msg += diag.relFilePath; + + if (typeof diag.lineNumber === 'number' && diag.lineNumber > 0) { + msg += ', line ' + diag.lineNumber; + + if (typeof diag.columnNumber === 'number' && diag.columnNumber > 0) { + msg += ', column ' + diag.columnNumber; + } + } + msg += '\n'; + } + + msg += diag.messageText; + + log(color, prefix, msg); +}; diff --git a/packages/dev-server/src/client/status.ts b/packages/dev-server/src/client/status.ts new file mode 100644 index 00000000000..ee02f8344df --- /dev/null +++ b/packages/dev-server/src/client/status.ts @@ -0,0 +1,235 @@ +/** + * Build status and favicon utilities for dev server client. + */ + +import { onBuildLog, onBuildStatus, onBuildResults } from './events'; + +// ============================================================================= +// Favicon Icons (Base64 encoded) +// ============================================================================= + +const ICON_DEFAULT = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAMAAABlApw1AAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAsGDs4wML8QEbBvr2FMhAM7+ILCUPnNzXrX04otO6j3RiT0ggzLSTcmtWUUWoZlknghZc2mZzAAACrklEQVR42u3dWXLiUAyFYWEwg40x8wxhSIAwJtH+99ZVeeinfriXVpWk5Hyr+C2VrgkAAAAAAAAAAAw5sZQ7aUhYypw07FjKC2ko2yxk2SQFgwYLOWSkYFhlIZ06KWhNWMhqRApGKxYyaZGCeoeFVIekIDuwkEaXFDSXLKRdkoYjS9mRhjlLSUjDO0s5kYYzS+mThn3OQsYqAbQQC7hZSgoGYgHUy0jBa42FvKkEUDERC6CCFIzeWEjtlRRkPbGAG5CCtCIWQAtS0ByzkHxPGvos5UEaNizlnTRsWconhbM4wTpSFHMTrFtKCroNFrLGBOsJLbGAWxWkoFiJBRAmWE/I1r4nWOmNheTeJ1gX0vDJUrYUweAEa04aHs5XePvc9wpPboJ1SCmOsRVkr04aromUEQEAgB9lxaZ++ATFpNDv6Y8qm1QdBk9QTAr9ni6mbFK7DJ6g2LQLXoHZlFCQdMY2nYJXYDb1g1dgNo2boSswm2Zp6ArMptCFyIVtCl2IlDmbNC0QcPEQcD8l4HLvAXdxHnBb5wG3QcDFQ8D9mIDrIeCiIeDiA25oNeA+EHDREHDxAbdmmxBwT0HARQbciW0KDbiEbQoNuB3bFBxwbTYJAfcUBFxkwFG/YlNJAADgxzCRcqUY9m7KGgNSUEx9H3XXO76Puv/OY5wedX/flHk+6j46v2maO79purPvm6Yz+75puua+b5q6Dd/PEsrNMyZfFM5gAMW+ymPtWciYV3ksBpBOwKUH3wHXXLKUM2l4cR5wG+cBlzgPuJ3zgJNb6FRwlP4Ln1X8wrOKeFbxP6Qz3wEn+KzilWLYe5UnMuDwY5BvD+cBt899B9zC+49Bqr4DrlXzHXDF1HfA1Tu+Ay5b+w649OY74OjoO+Bo7jzg7s4DDgAAAAAAAAAA/u0POrfnVIaqz/QAAAAASUVORK5CYII='; +const ICON_PENDING = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAMAAABlApw1AAAAjVBMVEUAAAD8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjL8kjLn7xn3AAAALnRSTlMAsFBgAaDxfPpAdTMcD/fs47kDBhVXJQpvLNbInIiBRvSqIb+TZ2OOONxdzUxpgKSpAAAAA69JREFUeNrt3FtvskAQxvERFQXFioqnCkqth572+3+8947dN00TliF5ZpP53ZOAveg/OzCklFJKKaWUUkoppQTZm77cCGFo+jIhhG/TlwchJAvTk/GIAA6x6Um+JoDti+nJ644A5h+mJ8eMALKj6cnHnAB2r80NLJ4jf3Vz+cuWANZ5cwPTM/l7by6PZwQwGptGQf4q++dLCOHdNIbkb2IvjwjAvYEf8pe6j4/wYxopr/9SQih4BXa3l5eEcJ7a++c9/gkSQE8bcCWvXwcrAjjYADrxHv8KCbi3JasgD5fm8i9IAG1swMXzDv0X2wDaEED21dzA5UDeVoPm8uUbAayvvAI42YA7EIDzA5pv8lc6/UoAoxMv4CZuvyKUpnHn9VNBAG6B7XkBtCeEO6/AbvbyihAiXsB92svfCcA9wap4j19DAmgWs37AZCrnBKvu8vgX9AmWE3BZh/6L7QkWJIA2RxtwHQpml9sAQp9gXWbkbxz4CdYDfIK1qk1j3IV9fPgJFlNECJXhYfSfsBHkhBCKwEd452nYI7wncwQJP8GKTU+uO0I4D/uSkVJKqXAkA5nK9icoIi3nrU9QRHrZtj5BESmetT5BEantPCh7NTJFrUdgMg1bj8BkSv1HYJ8RmjMQKf1HYDdC+/R/IyQFzbD4AxH+CIyPPxCJoEdQ/IFIMgXNEPkDkd8jMLQs5wRcTXA1J+By/BGO+0ovYwQGU3kPRLJfIzCkCSfgpgmhpc5AxD/gIkLb8wKO0DTgoNyaGQQecNfQAy7TgGtHA04DLtyA24UecHngAVdrwIkJuAitU8DJ1Dbghkam9gEnU+uAWxiRjhsdoXagI1TPgKNyIBO+ZpRSSrW3HfblTAA9/juPDwTAfiMK9VG3PY/hwX7Ubc9j+AoCWNWGp+NSH4HflE2IgXUEGPI3TTfmN4ndv2kSsRUJvpUn4W1FShbYb5rc84ySAtzKs3W3IgW4lWfO24q0zsFbebIjaysSjbtt5RHzUf0DHHCrAW8gVYEDzl0LGYW4lefB24uYQgOOfwN7dMANeW/k3DkBJ2CrUNE54GRsFYIHnPNR+iPEgHPWKo5DDDhnrWKeBRhwzlrFeNtlq5CgtYqzAAPODaBzgAH331rFAAOOqsDXKjL3IqboN7ILJ4BCDDh3r3SIAfd0AijEgHP3So/8wQNuvjRBbxVij5A6Bpy8EZJnwIkbIfkFnLwRkm/ASRshXbwDTtYICRRwt7BHqEoppZRSSimllFLqD/8AOXJZHefotiIAAAAASUVORK5CYII='; +const ICON_ERROR = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAMAAABlApw1AAAAkFBMVEUAAAD5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0H5Q0HYvLBZAAAAL3RSTlMAsGDjA/rsC/ElHRUBBssz9pFCvoh0UEcsD9ec3K19OLiiaNLEYlmoVeiCbmE+GuMl4I8AAAKQSURBVHja7d1njupQDIZhAimEUIZQQu9taN7/7q50pfl/TmTJtvQ9q3hzLDsEAAAAAAAAAACGzFjKiTS0WcqONMxZypg0fH5YyLFPChZdFnIYkILil4VcclLw3bCQ85KULM8sZPMlBfmFhfwWpGBwYCHdESnoH1nIz4c0jFnKnDTsWEqbNJxYyow03FjKlDTUKQtZqwTQXizgtgkpWGQsZKIScL0OCxmqBFC5EQugkhQshyyk0yMFgwkLyRakIGmJBdCeFPTXLCStScOUpdwogsEXrBdpuLKUJ4XDC9afKmUh94QUjLy/YGViAZRTOIMBtypJQXn2HUC5WMBleMFqILmzkLSicBZfsB6k4clSrqTh5XyEd3MeQHXqe4Qn94LVSiicwRHkJScNdVvKkgAAwI+qZdM0/AXFpE4v+AXFpKwIfkExKfR7ulyxSWkV/IJi0zx4BGbTm4IkW7ZpFjwCs2kaPAKzad0PHYHZtE1CR2A2TQahIzCbhnnwCMykVYmAi4aAQ8BZ4T3grgi4BhBwCDgbEHCNIOAQcCYg4BpCwCHgLEDAaYgPuDfbhIBrBAGHgDMhNOBo2rKpIgAA8KNoS6kplq2dsu6CFJQr30vd+dD3Uvf/nTLHS93J3flZwrHznaad852mE/veaXqw752mKvW90zTq+j5LWGS+r/J8xQKoU1AUa2chm1zlsXQWUifgkoPvgOsffQccjZ0H3Mx5wL2dB9zcecB9sJTePOBM3cU+46wiziq6C7hk6zvg3J9VfDK7vir0ch5wN+cBV6e+A27v/ccgme+AkxshTXKKYW6EFH0X29gIKTLgzI2QYgPO2ggpLuDsvaDEBZy9EVJcwBkcIT0IAAAAAAAAAADs+AdjeyF69/r87QAAAABJRU5ErkJggg=='; +const ICON_DISABLED = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAMAAABlApw1AAAAeFBMVEUAAAC4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7+4t7/uGGySAAAAJ3RSTlMAsGAE7OMcAQvxJRX69kHWyL8zq5GIdEcsD5zcfVg4uKLNa1JPZoK/xdPIAAACiklEQVR42u3dW5KqUAyF4QgCCggqIt7t9pb5z/Ccvjz2w95UqpJ0r28Uf2WTQAAAAAAAAAAAYMiWpTxJQ8JSTqThwVI2pKFZsJC3ghTs5izkmpKCcspCljNSkB9ZSLsnBfuWhRxzUjBbspBpSQrSKwuZr0lB8cZCFg1p2LCUB2k4sZSENNxYypY0nFlKTxqGmoUcClJwEQu4SUoKdmIBtEpJQZ6xkHeVAKqOYgFUkYL9OwvJclKQrsQCbkcK0olYAF1IQXFgIfVAGnqWcqZwFidYN4phb4L1onCYYMlPsLqUFKwxwRozwTIYcG1FCqrWdwBhgqU7wUo7FlJ7n2DdScPL+RPezfkT3tl5AA217yc89xMssYBbzUjDkEjZEwAA+NFMbOrDJygmZXnwBMWkaRk8QTFpvg6eoJi0aIInKDY9gp/AbEqCJyg2bYOfwGzqKUzPNh2K0Ccwm0IfRBK2KfSLkDvbFPog0tRsUlsh4EZAwP2SgKu9B9wdATcOAg4BZwACbgQEHALOCATcCAg4BJwVCLhREHB/LOAebFNwwC3YJATcKAi4yICjfmJTQwAA4EeZSBkojrWdsvmO4hjbKYtd6ra2Uxa71G1tp0xnqbvo+IPfpe4Nf3K703Ridr3T9OQPfnea7szseaepqX3vNH3NM/xe5fmeZ7i9yiMXQFlJEeydhYy4ymMygCICzmQAxQactbOQMQFnMoBiAs7iVaHIgDN3VSgq4AxeFYoOOGNXhbCUPkaJs4o4q/iXzyp2vgPO/VnFl/OAu/F/jq8KnZ0H3FD7DriL9x+DTH0HXJ75Driq9R1ws6XvgEuvvgOu6HwHHG18BxydnAfc03nAAQAAAAAAAADAz/4BoL2Us9XM2zMAAAAASUVORK5CYII='; +const ICON_TYPE = 'image/x-icon'; + +// ============================================================================= +// Build Status +// ============================================================================= + +export const initBuildStatus = (data: { window: Window }): void => { + const win = data.window; + const doc = win.document; + const iconElms = getFavIcons(doc); + + iconElms.forEach((iconElm) => { + if (iconElm.href) { + iconElm.dataset.href = iconElm.href; + iconElm.dataset.type = iconElm.type; + } + }); + + onBuildStatus(win, (buildStatus) => { + updateBuildStatus(doc, buildStatus); + }); +}; + +const updateBuildStatus = (doc: Document, status: string): void => { + const iconElms = getFavIcons(doc); + iconElms.forEach((iconElm) => { + updateFavIcon(iconElm, status); + }); +}; + +export const updateFavIcon = (linkElm: HTMLLinkElement, status: string): void => { + if (status === 'pending') { + linkElm.href = ICON_PENDING; + linkElm.type = ICON_TYPE; + linkElm.setAttribute('data-status', status); + } else if (status === 'error') { + linkElm.href = ICON_ERROR; + linkElm.type = ICON_TYPE; + linkElm.setAttribute('data-status', status); + } else if (status === 'disabled') { + linkElm.href = ICON_DISABLED; + linkElm.type = ICON_TYPE; + linkElm.setAttribute('data-status', status); + } else { + linkElm.removeAttribute('data-status'); + if (linkElm.dataset.href) { + linkElm.href = linkElm.dataset.href; + linkElm.type = linkElm.dataset.type || ICON_TYPE; + } else { + linkElm.href = ICON_DEFAULT; + linkElm.type = ICON_TYPE; + } + } +}; + +const getFavIcons = (doc: Document): HTMLLinkElement[] => { + const iconElms: HTMLLinkElement[] = []; + const linkElms = doc.querySelectorAll('link'); + + for (let i = 0; i < linkElms.length; i++) { + if ( + linkElms[i].href && + linkElms[i].rel && + (linkElms[i].rel.indexOf('shortcut') > -1 || linkElms[i].rel.indexOf('icon') > -1) + ) { + iconElms.push(linkElms[i]); + } + } + + if (iconElms.length === 0) { + const linkElm = doc.createElement('link'); + linkElm.rel = 'shortcut icon'; + doc.head.appendChild(linkElm); + iconElms.push(linkElm); + } + + return iconElms; +}; + +// ============================================================================= +// Build Progress +// ============================================================================= + +const PROGRESS_BAR_ID = `dev-server-progress-bar`; + +export const initBuildProgress = (data: { window: Window }) => { + const win = data.window; + const doc = win.document; + const barColor = `#5851ff`; + const errorColor = `#b70c19`; + let addBarTimerId: any; + let removeBarTimerId: any; + let opacityTimerId: any; + let incIntervalId: any; + let progressIncrease: number; + let currentProgress = 0; + + function update() { + clearTimeout(opacityTimerId); + clearTimeout(removeBarTimerId); + + const progressBar = getProgressBar(); + if (!progressBar) { + createProgressBar(); + addBarTimerId = setTimeout(update, 16); + return; + } + progressBar.style.background = barColor; + progressBar.style.opacity = `1`; + progressBar.style.transform = `scaleX(${Math.min(1, displayProgress())})`; + + if (incIntervalId == null) { + incIntervalId = setInterval(() => { + progressIncrease += Math.random() * 0.05 + 0.01; + if (displayProgress() < 0.9) { + update(); + } else { + clearInterval(incIntervalId); + } + }, 800); + } + } + + function reset() { + clearInterval(incIntervalId); + progressIncrease = 0.05; + incIntervalId = null; + clearTimeout(opacityTimerId); + clearTimeout(addBarTimerId); + clearTimeout(removeBarTimerId); + + let progressBar = getProgressBar(); + if (progressBar) { + if (currentProgress >= 1) { + progressBar.style.transform = `scaleX(1)`; + } + + opacityTimerId = setTimeout(() => { + try { + progressBar = getProgressBar(); + if (progressBar) { + progressBar.style.opacity = `0`; + } + } catch {} + }, 150); + + removeBarTimerId = setTimeout(() => { + try { + progressBar = getProgressBar(); + if (progressBar?.parentNode) { + progressBar.parentNode.removeChild(progressBar); + } + } catch {} + }, 1000); + } + } + + function displayProgress() { + const p = currentProgress + progressIncrease; + return Math.max(0, Math.min(1, p)); + } + + reset(); + + onBuildLog(win, (buildLog) => { + currentProgress = buildLog.progress; + + if (currentProgress >= 0 && currentProgress < 1) { + update(); + } else { + reset(); + } + }); + + onBuildResults(win, (buildResults) => { + if (buildResults.hasError) { + const progressBar = getProgressBar(); + if (progressBar) { + progressBar.style.transform = `scaleX(1)`; + progressBar.style.background = errorColor; + } + } + reset(); + }); + + onBuildStatus(win, (buildStatus) => { + if (buildStatus === 'disabled') { + reset(); + } + }); + + if (doc.head.dataset.tmpl === 'tmpl-initial-load') { + update(); + } + + function getProgressBar() { + return doc.getElementById(PROGRESS_BAR_ID); + } + + function createProgressBar() { + const progressBar = doc.createElement('div'); + progressBar.id = PROGRESS_BAR_ID; + progressBar.style.position = `absolute`; + progressBar.style.top = `0`; + progressBar.style.left = `0`; + progressBar.style.zIndex = `100001`; + progressBar.style.width = `100%`; + progressBar.style.height = `2px`; + progressBar.style.transform = `scaleX(0)`; + progressBar.style.opacity = `1`; + progressBar.style.background = barColor; + progressBar.style.transformOrigin = `left center`; + progressBar.style.transition = `transform .1s ease-in-out, opacity .5s ease-in`; + (progressBar.style as any).contain = `strict`; + doc.body.appendChild(progressBar); + } +}; diff --git a/packages/dev-server/src/client/types.ts b/packages/dev-server/src/client/types.ts new file mode 100644 index 00000000000..ff17b2c3c7b --- /dev/null +++ b/packages/dev-server/src/client/types.ts @@ -0,0 +1,66 @@ +/** + * Client-side type definitions for dev server. + */ + +// Import and re-export compiler types from @stencil/core +import type { + CompilerBuildResults, + Diagnostic, + HotModuleReplacement, + HmrStyleUpdate, + PrintLine, +} from '@stencil/core/compiler'; + +export type { CompilerBuildResults, Diagnostic, HotModuleReplacement, HmrStyleUpdate, PrintLine }; + +export interface DevClientWindow extends Window { + 's-dev-server'?: boolean; + 's-initial-load'?: boolean; + 's-build-id'?: number; + devServerConfig?: DevClientConfig; + WebSocket: typeof WebSocket; +} + +export interface DevClientConfig { + basePath: string; + editors: DevServerEditor[]; + reloadStrategy: 'hmr' | 'pageReload' | null; + socketUrl?: string; +} + +export interface DevServerEditor { + id: string; + name?: string; +} + +export interface DevServerMessage { + buildResults?: CompilerBuildResults; + buildLog?: BuildLog; + isActivelyBuilding?: boolean; + requestBuildResults?: boolean; +} + +export interface BuildLog { + buildId: number; + messages: string[]; + progress: number; +} + +export interface HostElement extends Element { + 's-hmr'?: (versionId: string) => void; +} + +export interface OpenInEditorData { + file?: string; + line?: number; + column?: number; + editor?: string; +} + +export interface HmrResults { + updatedComponents: string[]; + updatedExternalStyles: string[]; + updatedInlineStyles: string[]; + updatedImages: string[]; + versionId: string; +} diff --git a/packages/dev-server/src/client/websocket.ts b/packages/dev-server/src/client/websocket.ts new file mode 100644 index 00000000000..a37eaa09e94 --- /dev/null +++ b/packages/dev-server/src/client/websocket.ts @@ -0,0 +1,150 @@ +/** + * WebSocket client for dev server communication. + */ + +import { + NORMAL_CLOSURE_CODE, + RECONNECT_ATTEMPTS, + RECONNECT_RETRY_MS, + REQUEST_BUILD_RESULTS_INTERVAL_MS, +} from './constants'; +import { emitBuildLog, emitBuildResults, emitBuildStatus } from './events'; +import { logDisabled, logReload, logWarn } from './logger'; +import type { DevClientConfig, DevClientWindow, DevServerMessage } from './types'; + +export const initClientWebSocket = (win: DevClientWindow, config: DevClientConfig): void => { + let clientWs: WebSocket | null = null; + let reconnectTmrId: ReturnType | null = null; + let reconnectAttempts = 0; + let requestBuildResultsTmrId: ReturnType | null = null; + let hasGottenBuildResults = false; + let buildResultsRequests = 0; + + function onOpen(this: WebSocket): void { + if (reconnectAttempts > 0) { + emitBuildStatus(win, 'pending'); + } + + if (!hasGottenBuildResults) { + requestBuildResultsTmrId = setInterval(() => { + buildResultsRequests++; + if ( + !hasGottenBuildResults && + this.readyState === WebSocket.OPEN && + buildResultsRequests < 500 + ) { + const msg: DevServerMessage = { requestBuildResults: true }; + this.send(JSON.stringify(msg)); + } else if (requestBuildResultsTmrId) { + clearInterval(requestBuildResultsTmrId); + } + }, REQUEST_BUILD_RESULTS_INTERVAL_MS); + } + + if (reconnectTmrId) { + clearTimeout(reconnectTmrId); + } + } + + function onError(): void { + queueReconnect(); + } + + function onClose(event: CloseEvent): void { + emitBuildStatus(win, 'disabled'); + + if (event.code > NORMAL_CLOSURE_CODE) { + logWarn('Dev Server', `web socket closed: ${event.code} ${event.reason}`); + } else { + logDisabled('Dev Server', 'Disconnected, attempting to reconnect...'); + } + + queueReconnect(); + } + + function onMessage(event: MessageEvent): void { + const msg: DevServerMessage = JSON.parse(event.data); + + if (reconnectAttempts > 0) { + if (msg.isActivelyBuilding) { + return; + } + + if (msg.buildResults) { + logReload('Reconnected to dev server'); + hasGottenBuildResults = true; + buildResultsRequests = 0; + if (requestBuildResultsTmrId) { + clearInterval(requestBuildResultsTmrId); + } + + if (win['s-build-id'] !== msg.buildResults.buildId) { + win.location.reload(); + } + win['s-build-id'] = msg.buildResults.buildId; + return; + } + } + + if (msg.buildLog) { + if (msg.buildLog.progress < 1) { + emitBuildStatus(win, 'pending'); + } + emitBuildLog(win, msg.buildLog); + return; + } + + if (msg.buildResults) { + hasGottenBuildResults = true; + buildResultsRequests = 0; + if (requestBuildResultsTmrId) { + clearInterval(requestBuildResultsTmrId); + } + emitBuildStatus(win, 'default'); + emitBuildResults(win, msg.buildResults); + } + } + + function connect(): void { + if (reconnectTmrId) { + clearTimeout(reconnectTmrId); + } + + clientWs = new win.WebSocket(config.socketUrl!, ['xmpp']); + + clientWs.addEventListener('open', onOpen); + clientWs.addEventListener('error', onError); + clientWs.addEventListener('close', onClose); + clientWs.addEventListener('message', onMessage); + } + + function queueReconnect(): void { + hasGottenBuildResults = false; + + if (clientWs) { + if (clientWs.readyState === WebSocket.OPEN || clientWs.readyState === WebSocket.CONNECTING) { + clientWs.close(NORMAL_CLOSURE_CODE); + } + + clientWs.removeEventListener('open', onOpen); + clientWs.removeEventListener('error', onError); + clientWs.removeEventListener('close', onClose); + clientWs.removeEventListener('message', onMessage); + clientWs = null; + } + + if (reconnectTmrId) { + clearTimeout(reconnectTmrId); + } + + if (reconnectAttempts >= RECONNECT_ATTEMPTS) { + logWarn('Dev Server', 'Canceling reconnect attempts'); + } else { + reconnectAttempts++; + reconnectTmrId = setTimeout(connect, RECONNECT_RETRY_MS); + emitBuildStatus(win, 'disabled'); + } + } + + connect(); +}; diff --git a/packages/dev-server/src/server/_test_/server.spec.ts b/packages/dev-server/src/server/_test_/server.spec.ts new file mode 100644 index 00000000000..db741b53f03 --- /dev/null +++ b/packages/dev-server/src/server/_test_/server.spec.ts @@ -0,0 +1,60 @@ +import * as net from 'node:net'; +import { afterEach, describe, expect, it } from 'vitest'; + +import { findClosestOpenPort } from '../server'; + +describe('findClosestOpenPort', () => { + let testServer: net.Server | undefined; + const TEST_HOST = '127.0.0.1'; + const TEST_PORT = 9876; + + afterEach(async () => { + if (testServer) { + await new Promise((resolve) => { + testServer!.close(() => resolve()); + }); + testServer = undefined; + } + }); + + it('should return the same port if it is available', async () => { + const port = await findClosestOpenPort(TEST_HOST, TEST_PORT); + expect(port).toBe(TEST_PORT); + }); + + it('should find the next available port when strictPort is false', async () => { + testServer = net.createServer(); + await new Promise((resolve) => { + testServer!.listen(TEST_PORT, TEST_HOST, () => resolve()); + }); + + const port = await findClosestOpenPort(TEST_HOST, TEST_PORT, false); + expect(port).toBe(TEST_PORT + 1); + }); + + it('should find the next available port when strictPort is not provided (defaults to false)', async () => { + testServer = net.createServer(); + await new Promise((resolve) => { + testServer!.listen(TEST_PORT, TEST_HOST, () => resolve()); + }); + + const port = await findClosestOpenPort(TEST_HOST, TEST_PORT); + expect(port).toBe(TEST_PORT + 1); + }); + + it('should throw an error when port is taken and strictPort is true', async () => { + testServer = net.createServer(); + await new Promise((resolve) => { + testServer!.listen(TEST_PORT, TEST_HOST, () => resolve()); + }); + + await expect(findClosestOpenPort(TEST_HOST, TEST_PORT, true)).rejects.toThrow( + `Port ${TEST_PORT} is already in use. Please specify a different port or set strictPort to false.`, + ); + }); + + it('should return the port when available and strictPort is true', async () => { + const port = await findClosestOpenPort(TEST_HOST, TEST_PORT, true); + expect(port).toBe(TEST_PORT); + }); +}); diff --git a/packages/dev-server/src/server/_test_/utils.spec.ts b/packages/dev-server/src/server/_test_/utils.spec.ts new file mode 100644 index 00000000000..490060bebff --- /dev/null +++ b/packages/dev-server/src/server/_test_/utils.spec.ts @@ -0,0 +1,232 @@ +import { describe, expect, it } from 'vitest'; + +import { + DEV_SERVER_URL, + getBrowserUrl, + getDevServerClientUrl, + getSsrStaticDataPath, + isCssFile, + isExtensionLessPath, + isHtmlFile, + isSsrStaticDataPath, +} from '../utils'; +import type { DevServerConfig, HttpRequest } from '../types'; + +describe('isHtmlFile', () => { + it.each(['.html', 'foo.html', 'foo/bar.html'])( + 'returns true for .html files (%s)', + (filename) => { + expect(isHtmlFile(filename)).toBe(true); + }, + ); + + it.each(['.htm', 'foo.htm', 'foo/bar.htm'])('returns true for .htm files (%s)', (filename) => { + expect(isHtmlFile(filename)).toBe(true); + }); + + it.each(['.ht', 'foo.htmx', 'foo/bar.xaml'])( + 'returns false for other types of files (%s)', + (filename) => { + expect(isHtmlFile(filename)).toBe(false); + }, + ); + + it.each(['.hTMl', 'foo.HTML', 'foo/bar.htmL'])( + 'is case insensitive for filename (%s)', + (filename) => { + expect(isHtmlFile(filename)).toBe(true); + }, + ); +}); + +describe('isCssFile', () => { + it.each(['.css', 'foo.css', 'foo/bar.css'])('returns true for .css files (%s)', (filename) => { + expect(isCssFile(filename)).toBe(true); + }); + + it.each(['.txt', 'foo.sass', 'foo/bar.htm'])( + 'returns false for other types of files (%s)', + (filename) => { + expect(isCssFile(filename)).toBe(false); + }, + ); + + it.each(['.cSs', 'foo.cSS', 'foo/bar.CSS'])( + 'is case insensitive for filename (%s)', + (filename) => { + expect(isCssFile(filename)).toBe(true); + }, + ); +}); + +describe('getBrowserUrl', () => { + it('should get url with custom base url and pathname', () => { + const url = getBrowserUrl('http:', '0.0.0.0', 44, '/my-base-url/', '/my-custom-path-name'); + expect(url).toBe('http://localhost:44/my-base-url/my-custom-path-name'); + }); + + it('should get url with custom pathname', () => { + const url = getBrowserUrl('http', '0.0.0.0', 44, '/', '/my-custom-path-name'); + expect(url).toBe('http://localhost:44/my-custom-path-name'); + }); + + it('should get path with 80 port', () => { + const url = getBrowserUrl('http', '0.0.0.0', 80, '/', '/'); + expect(url).toBe('http://localhost/'); + }); + + it('should get path with no port', () => { + const url = getBrowserUrl('http', '0.0.0.0', undefined as any, '/', '/'); + expect(url).toBe('http://localhost/'); + }); + + it('should get path with https', () => { + const url = getBrowserUrl('https', '0.0.0.0', 3333, '/', '/'); + expect(url).toBe('https://localhost:3333/'); + }); + + it('should get path with custom address', () => { + const url = getBrowserUrl('http', 'staging.stenciljs.com', 3333, '/', '/'); + expect(url).toBe('http://staging.stenciljs.com:3333/'); + }); +}); + +describe('getDevServerClientUrl', () => { + it('should get path for dev server w/ host w/ port w/ protocol', () => { + const devServerConfig: DevServerConfig = { + protocol: 'http', + address: '0.0.0.0', + port: 3333, + basePath: '/my-base-url/', + }; + const url = getDevServerClientUrl(devServerConfig, 'staging.stenciljs:5555.com', 'https'); + expect(url).toBe(`https://staging.stenciljs:5555.com/my-base-url${DEV_SERVER_URL}`); + }); + + it('should get path for dev server w/ host w/ port no protocol', () => { + const devServerConfig: DevServerConfig = { + protocol: 'http', + address: '0.0.0.0', + port: 3333, + basePath: '/my-base-url/', + }; + const url = getDevServerClientUrl(devServerConfig, 'staging.stenciljs:5555.com', undefined); + expect(url).toBe(`http://staging.stenciljs:5555.com/my-base-url${DEV_SERVER_URL}`); + }); + + it('should get path for dev server w/ host no port', () => { + const devServerConfig: DevServerConfig = { + protocol: 'http', + address: '0.0.0.0', + port: 3333, + basePath: '/my-base-url/', + }; + const url = getDevServerClientUrl(devServerConfig, 'staging.stenciljs.com', undefined); + expect(url).toBe(`http://staging.stenciljs.com/my-base-url${DEV_SERVER_URL}`); + }); + + it('should get path for dev server w/ base url and port, no host', () => { + const devServerConfig: DevServerConfig = { + protocol: 'http', + address: '0.0.0.0', + port: 3333, + basePath: '/my-base-url/', + }; + const url = getDevServerClientUrl(devServerConfig, undefined, undefined); + expect(url).toBe(`http://localhost:3333/my-base-url${DEV_SERVER_URL}`); + }); + + it('should get path for dev server w/ base url and w/out port', () => { + const devServerConfig: DevServerConfig = { + protocol: 'http', + address: '0.0.0.0', + basePath: '/my-base-url/', + }; + const url = getDevServerClientUrl(devServerConfig, undefined, undefined); + expect(url).toBe(`http://localhost/my-base-url${DEV_SERVER_URL}`); + }); + + it('should get path for dev server w/ custom address, base url and port', () => { + const devServerConfig: DevServerConfig = { + protocol: 'http', + address: '1.2.3.4', + port: 3333, + basePath: '/my-base-url/', + }; + const url = getDevServerClientUrl(devServerConfig, undefined, undefined); + expect(url).toBe(`http://1.2.3.4:3333/my-base-url${DEV_SERVER_URL}`); + }); +}); + +describe('isExtensionLessPath', () => { + it('returns true for paths without extensions', () => { + expect(isExtensionLessPath('http://stenciljs.com/')).toBe(true); + expect(isExtensionLessPath('http://stenciljs.com/blog')).toBe(true); + expect(isExtensionLessPath('http://stenciljs.com/blog/')).toBe(true); + }); + + it('returns false for paths with extensions', () => { + expect(isExtensionLessPath('http://stenciljs.com/.')).toBe(false); + expect(isExtensionLessPath('http://stenciljs.com/data.json')).toBe(false); + expect(isExtensionLessPath('http://stenciljs.com/index.html')).toBe(false); + expect(isExtensionLessPath('http://stenciljs.com/blog.html')).toBe(false); + }); +}); + +describe('isSsrStaticDataPath', () => { + it('returns false for non-SSR paths', () => { + expect(isSsrStaticDataPath('http://stenciljs.com/')).toBe(false); + expect(isSsrStaticDataPath('http://stenciljs.com/index.html')).toBe(false); + }); + + it('returns true for SSR static data paths', () => { + expect(isSsrStaticDataPath('http://stenciljs.com/page.state.json')).toBe(true); + }); +}); + +describe('getSsrStaticDataPath', () => { + it('handles root path', () => { + const req: HttpRequest = { + url: new URL('http://stenciljs.com/page.static.json'), + method: 'GET', + acceptHeader: '', + searchParams: null, + }; + const r = getSsrStaticDataPath(req); + expect(r.fileName).toBe('page.static.json'); + expect(r.hasQueryString).toBe(false); + expect(r.ssrPath).toBe('http://stenciljs.com/'); + }); + + it('handles path with query string and no trailing slash referer', () => { + const req: HttpRequest = { + url: new URL('http://stenciljs.com/blog/page.static.json?v=1234'), + method: 'GET', + acceptHeader: '', + searchParams: null, + headers: { + Referer: 'http://stenciljs.com/page', + }, + }; + const r = getSsrStaticDataPath(req); + expect(r.fileName).toBe('page.static.json'); + expect(r.hasQueryString).toBe(true); + expect(r.ssrPath).toBe('http://stenciljs.com/blog'); + }); + + it('handles path with trailing slash referer', () => { + const req: HttpRequest = { + url: new URL('http://stenciljs.com/blog/page.static.json?v=1234'), + method: 'GET', + acceptHeader: '', + searchParams: null, + headers: { + Referer: 'http://stenciljs.com/page/', + }, + }; + const r = getSsrStaticDataPath(req); + expect(r.fileName).toBe('page.static.json'); + expect(r.hasQueryString).toBe(true); + expect(r.ssrPath).toBe('http://stenciljs.com/blog/'); + }); +}); diff --git a/packages/dev-server/src/server/context.ts b/packages/dev-server/src/server/context.ts new file mode 100644 index 00000000000..df20619b299 --- /dev/null +++ b/packages/dev-server/src/server/context.ts @@ -0,0 +1,182 @@ +/** + * Server context factory. + * Creates the shared context object passed to request handlers. + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { inspect } from 'node:util'; + +import { responseHeaders } from './utils'; +import type { + CompilerBuildResults, + CompilerRequestResponse, + CompilerSystem, + DevServerConfig, + DevServerContext, + DevServerSendMessage, + HttpRequest, +} from './types'; +import type { ServerResponse } from 'node:http'; + +export interface CompilerRequestResolve { + path: string; + resolve: (results: CompilerRequestResponse) => void; + reject: (msg: unknown) => void; +} + +export interface BuildRequestResolve { + resolve: (results: CompilerBuildResults) => void; + reject: (msg: unknown) => void; +} + +export function createServerContext( + sys: CompilerSystem, + sendMsg: DevServerSendMessage, + devServerConfig: DevServerConfig, + buildResultsResolves: BuildRequestResolve[], + compilerRequestResolves: CompilerRequestResolve[], +): DevServerContext { + const logRequest = (req: HttpRequest, status: number): void => { + if (devServerConfig) { + sendMsg({ + requestLog: { + method: req.method || '?', + url: req.pathname || '?', + status, + }, + }); + } + }; + + const serve500 = ( + req: HttpRequest, + res: ServerResponse, + error: unknown, + xSource: string, + ): void => { + try { + if (res.headersSent) { + // Headers already sent, just end the response + res.end(); + return; + } + res.writeHead( + 500, + responseHeaders({ + 'content-type': 'text/plain; charset=utf-8', + 'x-source': xSource, + }), + ); + res.write(inspect(error)); + res.end(); + logRequest(req, 500); + } catch (e) { + sendMsg({ error: { message: 'serve500: ' + e } }); + } + }; + + const serve404 = ( + req: HttpRequest, + res: ServerResponse, + xSource: string, + content: string | null = null, + ): void => { + try { + if (res.headersSent) { + res.end(); + return; + } + + if (req.pathname === '/favicon.ico') { + const defaultFavicon = path.join(devServerConfig.devServerDir!, 'static', 'favicon.ico'); + const rs = fs.createReadStream(defaultFavicon); + rs.on('error', () => { + // Favicon not found - just end the response silently + if (!res.headersSent) { + res.writeHead(404); + } + res.end(); + }); + res.writeHead( + 200, + responseHeaders({ + 'content-type': 'image/x-icon', + 'x-source': `favicon: ${xSource}`, + }), + ); + rs.pipe(res); + return; + } + + if (content == null) { + content = ['404 File Not Found', 'Url: ' + req.pathname, 'File: ' + req.filePath].join( + '\n', + ); + } + res.writeHead( + 404, + responseHeaders({ + 'content-type': 'text/plain; charset=utf-8', + 'x-source': xSource, + }), + ); + res.write(content); + res.end(); + + logRequest(req, 404); + } catch (e) { + serve500(req, res, e, xSource); + } + }; + + const serve302 = ( + req: HttpRequest, + res: ServerResponse, + pathname: string | null = null, + ): void => { + logRequest(req, 302); + res.writeHead(302, { location: pathname || devServerConfig.basePath || '/' }); + res.end(); + }; + + const getBuildResults = (): Promise => + new Promise((resolve, reject) => { + if (serverCtx.isServerListening) { + buildResultsResolves.push({ resolve, reject }); + sendMsg({ requestBuildResults: true }); + } else { + reject('dev server closed'); + } + }); + + const getCompilerRequest = (compilerRequestPath: string): Promise => + new Promise((resolve, reject) => { + if (serverCtx.isServerListening) { + compilerRequestResolves.push({ + path: compilerRequestPath, + resolve, + reject, + }); + sendMsg({ compilerRequestPath }); + } else { + reject('dev server closed'); + } + }); + + const serverCtx: DevServerContext = { + connectorHtml: null, + dirTemplate: null, + getBuildResults, + getCompilerRequest, + isServerListening: false, + logRequest, + prerenderConfig: null, + serve302, + serve404, + serve500, + sys, + }; + + return serverCtx; +} diff --git a/packages/dev-server/src/server/editor.ts b/packages/dev-server/src/server/editor.ts new file mode 100644 index 00000000000..84180f99259 --- /dev/null +++ b/packages/dev-server/src/server/editor.ts @@ -0,0 +1,192 @@ +/** + * Editor integration using launch-editor. + * Consolidated from open-in-browser.ts, open-in-editor.ts, and open-in-editor-api.ts. + */ + +import { responseHeaders } from './utils'; +import type { DevServerContext, DevServerEditor, HttpRequest, OpenInEditorData } from './types'; +import type { ServerResponse } from 'node:http'; + +// ============================================================================= +// Open in Browser +// ============================================================================= + +export async function openInBrowser(opts: { url: string }): Promise { + // Dynamically import 'open' package + const { default: open } = await import('open'); + await open(opts.url); +} + +// ============================================================================= +// Launch Editor Integration +// ============================================================================= + +let launchEditorLoaded = false; +let launchEditor: + | (( + file: string, + specifiedEditor?: string, + onErrorCallback?: (fileName: string, errorMessage: string | null) => void, + ) => void) + | null = null; + +async function loadLaunchEditor(): Promise { + if (launchEditorLoaded) return; + + try { + const mod = await import('launch-editor'); + launchEditor = mod.default || mod; + } catch { + console.warn( + 'launch-editor package is not available. Open in editor functionality will be disabled.', + ); + // Package not available + launchEditor = null; + } + + launchEditorLoaded = true; +} + +// ============================================================================= +// Open in Editor Handler +// ============================================================================= + +export async function serveOpenInEditor( + serverCtx: DevServerContext, + req: HttpRequest, + res: ServerResponse, +): Promise { + let status = 200; + const data: OpenInEditorData = {}; + + try { + await parseEditorData(serverCtx.sys, req, data); + await openDataInEditor(data); + } catch (e) { + data.error = String(e); + status = 500; + } + + serverCtx.logRequest(req, status); + + res.writeHead( + status, + responseHeaders({ + 'content-type': 'application/json; charset=utf-8', + }), + ); + + res.write(JSON.stringify(data, null, 2)); + res.end(); +} + +async function parseEditorData( + sys: DevServerContext['sys'], + req: HttpRequest, + data: OpenInEditorData, +): Promise { + const qs = req.searchParams!; + + if (!qs.has('file')) { + data.error = 'missing file'; + return; + } + + data.file = qs.get('file')!; + + if (qs.has('line') && !isNaN(Number(qs.get('line')))) { + data.line = parseInt(qs.get('line')!, 10); + } + if (typeof data.line !== 'number' || data.line < 1) { + data.line = 1; + } + + if (qs.has('column') && !isNaN(Number(qs.get('column')))) { + data.column = parseInt(qs.get('column')!, 10); + } + if (typeof data.column !== 'number' || data.column < 1) { + data.column = 1; + } + + if (qs.has('editor')) { + data.editor = qs.get('editor')!; + } + + const stat = await sys.stat(data.file); + data.exists = stat.isFile; +} + +async function openDataInEditor(data: OpenInEditorData): Promise { + if (!data.exists || data.error) { + return; + } + + await loadLaunchEditor(); + + if (!launchEditor) { + data.error = 'launch-editor not available'; + return; + } + + try { + // Format: file:line:column + const fileSpec = `${data.file}:${data.line}:${data.column}`; + + await new Promise((resolve, reject) => { + let errorCalled = false; + + launchEditor!( + fileSpec, + data.editor || process.env.EDITOR, // Try editor param, then env var, then auto-detect + (_fileName: string, errorMessage: string | null) => { + errorCalled = true; + const errMsg = errorMessage || 'Unknown error'; + // Log to dev server console so user sees it + console.error('Editor launch failed.'); + console.error('The "code" executable was not found in your PATH.'); + console.error("This usually means your editor's command-line tool isn't installed."); + console.error('Try running:'); + console.error(' code --version'); + console.error( + "If that fails, install your editor's CLI command and ensure it's in your PATH.\n", + ); + data.error = errMsg; + reject(new Error(errMsg)); + }, + ); + + // launch-editor doesn't have a success callback + // Give it a moment to call the error callback if there's an issue + setTimeout(() => { + if (!errorCalled) { + data.open = fileSpec; + resolve(); + } + }, 100); + }); + } catch (e) { + if (!data.error) { + data.error = String(e); + } + } +} + +// ============================================================================= +// Editor Detection (Simplified - launch-editor handles this internally) +// ============================================================================= + +export function getEditors(): Promise { + // launch-editor automatically detects editors, so we just return common ones + // The actual detection happens when opening a file + return Promise.resolve([ + { id: 'code', name: 'Visual Studio Code' }, + { id: 'cursor', name: 'Cursor' }, + { id: 'code-insiders', name: 'VS Code Insiders' }, + { id: 'webstorm', name: 'WebStorm' }, + { id: 'idea', name: 'IntelliJ IDEA' }, + { id: 'sublime', name: 'Sublime Text' }, + { id: 'atom', name: 'Atom' }, + { id: 'vim', name: 'Vim' }, + { id: 'emacs', name: 'Emacs' }, + ]); +} diff --git a/packages/dev-server/src/server/handlers.ts b/packages/dev-server/src/server/handlers.ts new file mode 100644 index 00000000000..8b7fd7101dd --- /dev/null +++ b/packages/dev-server/src/server/handlers.ts @@ -0,0 +1,630 @@ +/** + * Request handlers. + * Consolidated from request-handler.ts, serve-file.ts, serve-dev-client.ts, + * serve-dev-node-module.ts, and serve-directory-index.ts. + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import * as zlib from 'node:zlib'; + +import { getEditors, serveOpenInEditor } from './editor'; +import { ssrPageRequest, ssrStaticDataRequest } from './ssr'; +import { + DEV_SERVER_URL, + VERSION, + getContentType, + getDevServerClientUrl, + isDevClient, + isDevModule, + isDevServerClient, + isExtensionLessPath, + isHtmlFile, + isCssFile, + isInitialDevServerLoad, + isOpenInEditor, + isSimpleText, + isSsrStaticDataPath, + normalizePath, + responseHeaders, + shouldCompress, +} from './utils'; +import type { DevServerConfig, DevServerContext, DevClientConfig, HttpRequest } from './types'; +import type { IncomingMessage, ServerResponse } from 'node:http'; + +// ============================================================================= +// Main Request Handler +// ============================================================================= + +export function createRequestHandler( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, +): (req: IncomingMessage, res: ServerResponse) => Promise { + let userRequestHandler: + | ((req: IncomingMessage, res: ServerResponse, next: () => void) => void) + | null = null; + let userHandlerLoaded = false; + + return async function (incomingReq: IncomingMessage, res: ServerResponse): Promise { + // Lazy load user request handler on first request + if (!userHandlerLoaded && typeof devServerConfig.requestListenerPath === 'string') { + userHandlerLoaded = true; + try { + const userModule = await import(pathToFileURL(devServerConfig.requestListenerPath).href); + userRequestHandler = userModule.default || userModule; + } catch (e) { + console.error('Failed to load user request handler:', e); + } + } + async function defaultHandler(): Promise { + try { + const req = normalizeHttpRequest(devServerConfig, incomingReq); + + if (!req.url) { + return serverCtx.serve302(req, res); + } + + // Ping route for health checks + if (devServerConfig.pingRoute !== null && req.pathname === devServerConfig.pingRoute) { + try { + const result = await serverCtx.getBuildResults(); + if (!result.hasSuccessfulBuild) { + return serverCtx.serve500(req, res, 'Build not successful', 'build error'); + } + res.writeHead(200, 'OK'); + res.write('OK'); + res.end(); + } catch { + serverCtx.serve500(req, res, 'Error getting build results', 'ping error'); + } + return; + } + + // Dev client routes + if (isDevClient(req.pathname!) && devServerConfig.websocket) { + return serveDevClient(devServerConfig, serverCtx, req, res); + } + + // Dev module routes + if (isDevModule(req.pathname!)) { + return serveDevNodeModule(serverCtx, req, res); + } + + // Validate base path + if (!isValidUrlBasePath(devServerConfig.basePath!, req.url)) { + return serverCtx.serve404( + req, + res, + 'invalid basePath', + `404 File Not Found, base path: ${devServerConfig.basePath}`, + ); + } + + // SSR routes + if (devServerConfig.ssr) { + if (isExtensionLessPath(req.url.pathname)) { + return ssrPageRequest(devServerConfig, serverCtx, req, res); + } + if (isSsrStaticDataPath(req.url.pathname)) { + return ssrStaticDataRequest(devServerConfig, serverCtx, req, res); + } + } + + // Static file serving + req.stats = await serverCtx.sys.stat(req.filePath!); + if (req.stats.isFile) { + return serveFile(devServerConfig, serverCtx, req, res); + } + + // Directory index + if (req.stats.isDirectory) { + return serveDirectoryIndex(devServerConfig, serverCtx, req, res); + } + + // History API fallback + const xSource = ['notfound']; + const validHistoryApi = isValidHistoryApi(devServerConfig, req); + xSource.push(`validHistoryApi: ${validHistoryApi}`); + + if (validHistoryApi) { + try { + const indexFilePath = path.join( + devServerConfig.root!, + devServerConfig.historyApiFallback!.index!, + ); + xSource.push(`indexFilePath: ${indexFilePath}`); + + req.stats = await serverCtx.sys.stat(indexFilePath); + if (req.stats.isFile) { + req.filePath = indexFilePath; + return serveFile(devServerConfig, serverCtx, req, res); + } + } catch (e) { + xSource.push(`notfound error: ${e}`); + } + } + + return serverCtx.serve404(req, res, xSource.join(', ')); + } catch (e) { + // Use a minimal request object for error handling since req may not be defined + const errorReq: HttpRequest = { + method: (incomingReq.method || 'GET').toUpperCase() as HttpRequest['method'], + acceptHeader: '', + url: null, + searchParams: null, + }; + return serverCtx.serve500(errorReq, res, e, 'not found error'); + } + } + + if (typeof userRequestHandler === 'function') { + await userRequestHandler(incomingReq, res, defaultHandler); + } else { + await defaultHandler(); + } + }; +} + +// ============================================================================= +// Request Normalization +// ============================================================================= + +function normalizeHttpRequest( + devServerConfig: DevServerConfig, + incomingReq: IncomingMessage, +): HttpRequest { + const req: HttpRequest = { + method: (incomingReq.method || 'GET').toUpperCase() as HttpRequest['method'], + headers: incomingReq.headers as Record, + acceptHeader: + (incomingReq.headers && + typeof incomingReq.headers.accept === 'string' && + incomingReq.headers.accept) || + '', + host: + (incomingReq.headers && + typeof incomingReq.headers.host === 'string' && + incomingReq.headers.host) || + undefined, + url: null, + searchParams: null, + }; + + const incomingUrl = (incomingReq.url || '').trim() || null; + if (incomingUrl) { + if (req.host) { + req.url = new URL(incomingReq.url!, `http://${req.host}`); + } else { + req.url = new URL(incomingReq.url!, 'http://dev.stenciljs.com'); + } + req.searchParams = req.url.searchParams; + } + + if (req.url) { + const parts = req.url.pathname.replace(/\\/g, '/').split('/'); + + req.pathname = parts.map((part) => decodeURIComponent(part)).join('/'); + if (req.pathname.length > 0 && !isDevClient(req.pathname)) { + req.pathname = '/' + req.pathname.substring(devServerConfig.basePath!.length); + } + + req.filePath = normalizePath( + path.normalize(path.join(devServerConfig.root!, path.relative('/', req.pathname))), + ); + } + + return req; +} + +function isValidUrlBasePath(basePath: string, url: URL): boolean { + let pathname = url.pathname; + if (!pathname.endsWith('/')) { + pathname += '/'; + } + if (!basePath.endsWith('/')) { + basePath += '/'; + } + return pathname.startsWith(basePath); +} + +function isValidHistoryApi(devServerConfig: DevServerConfig, req: HttpRequest): boolean { + if (!devServerConfig.historyApiFallback) { + return false; + } + if (req.method !== 'GET') { + return false; + } + if (!req.acceptHeader.includes('text/html')) { + return false; + } + if (!devServerConfig.historyApiFallback.disableDotRule && req.pathname?.includes('.')) { + return false; + } + return true; +} + +// ============================================================================= +// Static File Serving +// ============================================================================= + +const urlVersionIds = new Map(); + +async function serveFile( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, + req: HttpRequest, + res: ServerResponse, +): Promise { + try { + if (isSimpleText(req.filePath!)) { + let content = await serverCtx.sys.readFile(req.filePath!, 'utf8'); + + if ( + devServerConfig.websocket && + isHtmlFile(req.filePath!) && + !isDevServerClient(req.pathname!) + ) { + content = appendDevServerClientScript(devServerConfig, req, content); + } else if (isCssFile(req.filePath!)) { + content = updateStyleUrls(req.url!, content); + } + + if (shouldCompress(devServerConfig, req)) { + res.writeHead( + 200, + responseHeaders({ + 'content-type': getContentType(req.filePath!) + '; charset=utf-8', + 'content-encoding': 'gzip', + vary: 'Accept-Encoding', + }), + ); + + zlib.gzip(content, { level: 9 }, (_, data) => { + res.end(data); + }); + } else { + res.writeHead( + 200, + responseHeaders({ + 'content-type': getContentType(req.filePath!) + '; charset=utf-8', + 'content-length': Buffer.byteLength(content, 'utf8'), + }), + ); + res.write(content); + res.end(); + } + } else { + const readStream = fs.createReadStream(req.filePath!); + + // Handle stream errors before piping to avoid "headers already sent" errors + readStream.on('error', (err) => { + if (!res.headersSent) { + serverCtx.serve500(req, res, err, 'serveFile'); + } else { + // Headers already sent, just end the response + res.end(); + } + }); + + res.writeHead( + 200, + responseHeaders({ + 'content-type': getContentType(req.filePath!), + 'content-length': req.stats!.size, + }), + ); + readStream.pipe(res); + } + + serverCtx.logRequest(req, 200); + } catch (e) { + serverCtx.serve500(req, res, e, 'serveFile'); + } +} + +function updateStyleUrls(url: URL, oldCss: string): string { + const versionId = url.searchParams.get('s-hmr'); + const hmrUrls = url.searchParams.get('s-hmr-urls'); + + if (versionId && hmrUrls) { + hmrUrls.split(',').forEach((hmrUrl) => { + urlVersionIds.set(hmrUrl, versionId); + }); + } + + const reg = /url\((['"]?)(.*)\1\)/gi; + let result; + let newCss = oldCss; + + while ((result = reg.exec(oldCss)) !== null) { + const oldUrl = result[2]; + const parsedUrl = new URL(oldUrl, url); + const fileName = path.basename(parsedUrl.pathname); + const cachedVersionId = urlVersionIds.get(fileName); + + if (!cachedVersionId) { + continue; + } + + parsedUrl.searchParams.set('s-hmr', cachedVersionId); + newCss = newCss.replace(oldUrl, parsedUrl.pathname); + } + + return newCss; +} + +export function appendDevServerClientScript( + devServerConfig: DevServerConfig, + req: HttpRequest, + content: string, +): string { + const devServerClientUrl = getDevServerClientUrl( + devServerConfig, + req.headers?.['x-forwarded-host'] ?? req.host, + req.headers?.['x-forwarded-proto'], + ); + const iframe = ``; + return appendDevServerClientIframe(content, iframe); +} + +function appendDevServerClientIframe(content: string, iframe: string): string { + if (content.includes('')) { + return content.replace('', `${iframe}`); + } + if (content.includes('')) { + return content.replace('', `${iframe}`); + } + return `${content}${iframe}`; +} + +// ============================================================================= +// Dev Client Serving +// ============================================================================= + +async function serveDevClient( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, + req: HttpRequest, + res: ServerResponse, +): Promise { + try { + if (isOpenInEditor(req.pathname!)) { + return serveOpenInEditor(serverCtx, req, res); + } + + if (isDevServerClient(req.pathname!)) { + return serveDevClientScript(devServerConfig, serverCtx, req, res); + } + + if (isInitialDevServerLoad(req.pathname!)) { + req.filePath = path.join(devServerConfig.devServerDir!, 'templates', 'initial-load.html'); + } else { + // Strip the /~dev-server/ prefix and serve from appropriate directory + const subPath = req.pathname!.replace(DEV_SERVER_URL + '/', ''); + if (subPath.startsWith('client/')) { + // Serve client JS module + req.filePath = path.join(devServerConfig.devServerDir!, subPath); + } else { + // Serve static assets (favicon, etc.) + req.filePath = path.join(devServerConfig.devServerDir!, 'static', subPath); + } + } + + try { + req.stats = await serverCtx.sys.stat(req.filePath!); + if (req.stats.isFile) { + return serveFile(devServerConfig, serverCtx, req, res); + } + return serverCtx.serve404(req, res, 'serveDevClient not file'); + } catch (e) { + return serverCtx.serve404(req, res, `serveDevClient stats error ${e}`); + } + } catch (e) { + return serverCtx.serve500(req, res, e, 'serveDevClient'); + } +} + +async function serveDevClientScript( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, + req: HttpRequest, + res: ServerResponse, +): Promise { + try { + if (serverCtx.connectorHtml == null) { + const filePath = path.join(devServerConfig.devServerDir!, 'connector.html'); + + serverCtx.connectorHtml = serverCtx.sys.readFileSync(filePath, 'utf8'); + if (typeof serverCtx.connectorHtml !== 'string') { + return serverCtx.serve404(req, res, 'serveDevClientScript'); + } + + const devClientConfig: DevClientConfig = { + basePath: devServerConfig.basePath!, + editors: await getEditors(), + reloadStrategy: devServerConfig.reloadStrategy!, + }; + + serverCtx.connectorHtml = serverCtx.connectorHtml.replace( + 'window.__DEV_CLIENT_CONFIG__', + JSON.stringify(devClientConfig), + ); + } + + res.writeHead( + 200, + responseHeaders({ + 'content-type': 'text/html; charset=utf-8', + }), + ); + res.write(serverCtx.connectorHtml); + res.end(); + } catch (e) { + return serverCtx.serve500(req, res, e, 'serveDevClientScript'); + } +} + +// ============================================================================= +// Dev Node Module Serving +// ============================================================================= + +async function serveDevNodeModule( + serverCtx: DevServerContext, + req: HttpRequest, + res: ServerResponse, +): Promise { + try { + const results = await serverCtx.getCompilerRequest(req.pathname!); + + const headers: Record = { + 'content-type': 'application/javascript; charset=utf-8', + 'content-length': Buffer.byteLength(results.content, 'utf8'), + 'x-dev-node-module-id': results.nodeModuleId, + 'x-dev-node-module-version': results.nodeModuleVersion, + 'x-dev-node-module-resolved-path': results.nodeResolvedPath, + 'x-dev-node-module-cache-path': results.cachePath, + 'x-dev-node-module-cache-hit': results.cacheHit, + }; + + res.writeHead(results.status, responseHeaders(headers as any)); + res.write(results.content); + res.end(); + } catch (e) { + serverCtx.serve500(req, res, e, 'serveDevNodeModule'); + } +} + +// ============================================================================= +// Directory Index Serving +// ============================================================================= + +interface DirectoryItem { + name: string; + pathname: string; + isDirectory: boolean; +} + +async function serveDirectoryIndex( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, + req: HttpRequest, + res: ServerResponse, +): Promise { + const indexFilePath = path.join(req.filePath!, 'index.html'); + req.stats = await serverCtx.sys.stat(indexFilePath); + + if (req.stats.isFile) { + req.filePath = indexFilePath; + return serveFile(devServerConfig, serverCtx, req, res); + } + + if (!req.pathname!.endsWith('/')) { + return serverCtx.serve302(req, res, req.pathname + '/'); + } + + try { + const dirFilePaths = await serverCtx.sys.readDir(req.filePath!); + + try { + if (serverCtx.dirTemplate == null) { + const dirTemplatePath = path.join( + devServerConfig.devServerDir!, + 'templates', + 'directory-index.html', + ); + serverCtx.dirTemplate = serverCtx.sys.readFileSync(dirTemplatePath); + } + + const files = await getDirectoryFiles(serverCtx.sys, req.url!, dirFilePaths); + + const templateHtml = serverCtx.dirTemplate + .replace('{{title}}', req.pathname!) + .replace('{{nav}}', getDirectoryNav(req.pathname!)) + .replace('{{files}}', files); + + serverCtx.logRequest(req, 200); + + res.writeHead( + 200, + responseHeaders({ + 'content-type': 'text/html; charset=utf-8', + 'x-directory-index': req.pathname, + }), + ); + + res.write(templateHtml); + res.end(); + } catch (e) { + return serverCtx.serve500(req, res, e, 'serveDirectoryIndex'); + } + } catch { + return serverCtx.serve404(req, res, 'serveDirectoryIndex'); + } +} + +async function getDirectoryFiles( + sys: DevServerContext['sys'], + baseUrl: URL, + dirItemNames: string[], +): Promise { + const items = await getDirectoryItems(sys, baseUrl, dirItemNames); + + if (baseUrl.pathname !== '/') { + items.unshift({ + isDirectory: true, + pathname: '../', + name: '..', + }); + } + + return items + .map((item) => { + return ` +
  • + + + ${item.name} + +
  • `; + }) + .join(''); +} + +async function getDirectoryItems( + sys: DevServerContext['sys'], + baseUrl: URL, + dirFilePaths: string[], +): Promise { + const items = await Promise.all( + dirFilePaths.map(async (dirFilePath) => { + const fileName = path.basename(dirFilePath); + const url = new URL(fileName, baseUrl); + const stats = await sys.stat(dirFilePath); + + return { + name: fileName, + pathname: url.pathname, + isDirectory: stats.isDirectory, + }; + }), + ); + return items; +} + +function getDirectoryNav(pathName: string): string { + const dirs = pathName.split('/'); + dirs.pop(); + + let url = ''; + + return ( + dirs + .map((dir, index) => { + url += dir + '/'; + const text = index === 0 ? '~' : dir; + return `${text}`; + }) + .join('/') + '/' + ); +} diff --git a/packages/dev-server/src/server/index.ts b/packages/dev-server/src/server/index.ts new file mode 100644 index 00000000000..86bee67b46c --- /dev/null +++ b/packages/dev-server/src/server/index.ts @@ -0,0 +1,286 @@ +/** + * Stencil Dev Server + * + * A modern development server for Stencil with DOM-based HMR. + * Designed for lazy-loading component architectures where module graphs + * are discovered at runtime from the DOM. + * @module @stencil/dev-server + */ + +import * as path from 'node:path'; + +import { initServerProcess } from './server'; +import { initServerProcessWorkerProxy } from './worker-main'; +import type { + CompilerBuildResults, + CompilerWatcher, + DevServer, + DevServerConfig, + DevServerMessage, + Logger, + StencilDevServerConfig, +} from './types'; + +// Re-export types for consumers +export type { + DevServer, + DevServerConfig, + StencilDevServerConfig, + Logger, + CompilerWatcher, +} from './types'; + +/** + * Callback to remove the watcher listener. + */ +type BuildOnEventRemove = () => void; + +/** + * Function signature for initializing the server process (either in-process or worker) + */ +type InitServerProcess = ( + receiveFromMain: (msg: DevServerMessage) => void, +) => (msg: DevServerMessage) => void; + +/** + * Start the Stencil development server. + * + * @param stencilDevServerConfig - Configuration for the dev server + * @param logger - Logger instance for output + * @param watcher - Optional compiler watcher for build events + * @returns Promise resolving to the DevServer instance + */ +export function start( + stencilDevServerConfig: StencilDevServerConfig, + logger: Logger, + watcher?: CompilerWatcher, +): Promise { + return new Promise(async (resolve, reject) => { + try { + const devServerConfig: DevServerConfig = { + // Point to dist/ where templates/, static/, and connector.html are copied during build + devServerDir: import.meta.dirname, + ...stencilDevServerConfig, + }; + + if (!path.isAbsolute(devServerConfig.root!)) { + devServerConfig.root = path.join(process.cwd(), devServerConfig.root!); + } + + // Determine whether to use worker architecture or run in-process + let initServerProcessFn: InitServerProcess; + + if (stencilDevServerConfig.worker === true || stencilDevServerConfig.worker === undefined) { + // Fork a worker process (default for stability and isolation) + initServerProcessFn = initServerProcessWorkerProxy; + } else { + // Run server in the same process (useful for debugging) + initServerProcessFn = initServerProcess; + } + + startServer(devServerConfig, logger, watcher, initServerProcessFn, resolve, reject); + } catch (e) { + reject(e); + } + }); +} + +/** + * Internal function to start the dev server. + * + * @param devServerConfig - configuration for the dev server + * @param logger - logger instance for output + * @param watcher - optional compiler watcher for build events + * @param initServerProcessFn - function to initialize the server process + * @param resolve - promise resolve callback + * @param reject - promise reject callback + */ +function startServer( + devServerConfig: DevServerConfig, + logger: Logger, + watcher: CompilerWatcher | undefined, + initServerProcessFn: InitServerProcess, + resolve: (devServer: DevServer) => void, + reject: (err: unknown) => void, +): void { + const timespan = logger.createTimeSpan('starting dev server', true); + + const startupTimeout = + logger.getLevel() !== 'debug' || devServerConfig.startupTimeout !== 0 + ? setTimeout(() => { + reject('dev server startup timeout'); + }, devServerConfig.startupTimeout ?? 15000) + : null; + + let isActivelyBuilding = false; + let lastBuildResults: CompilerBuildResults | null = null; + let devServer: DevServer | null = null; + let removeWatcher: BuildOnEventRemove | null = null; + let closeResolve: (() => void) | null = null; + let hasStarted = false; + let browserUrl = ''; + + let sendToServer: ((msg: DevServerMessage) => void) | null = null; + + const closePromise = new Promise((res) => { + closeResolve = res; + }); + + const close = async (): Promise => { + if (startupTimeout) { + clearTimeout(startupTimeout); + } + isActivelyBuilding = false; + + if (removeWatcher) { + removeWatcher(); + } + if (devServer) { + devServer = null; + } + if (sendToServer) { + sendToServer({ closeServer: true }); + sendToServer = null; + } + return closePromise; + }; + + const emit = (eventName: string, data: any): void => { + if (sendToServer) { + if (eventName === 'buildFinish') { + isActivelyBuilding = false; + lastBuildResults = { ...(data as CompilerBuildResults) }; + sendToServer({ buildResults: { ...lastBuildResults }, isActivelyBuilding }); + } else if (eventName === 'buildLog') { + sendToServer({ buildLog: { ...data } }); + } else if (eventName === 'buildStart') { + isActivelyBuilding = true; + } + } + }; + + const serverStarted = (msg: DevServerMessage): void => { + hasStarted = true; + if (startupTimeout) { + clearTimeout(startupTimeout); + } + devServerConfig = msg.serverStarted!; + + devServer = { + address: devServerConfig.address!, + basePath: devServerConfig.basePath!, + browserUrl: devServerConfig.browserUrl!, + protocol: devServerConfig.protocol!, + port: devServerConfig.port!, + root: devServerConfig.root!, + emit, + close, + }; + + browserUrl = devServerConfig.browserUrl!; + + timespan.finish(`dev server started: ${browserUrl}`); + + resolve(devServer); + }; + + const requestLog = (msg: DevServerMessage): void => { + if (devServerConfig.logRequests && msg.requestLog) { + if (msg.requestLog.status >= 500) { + logger.info( + logger.red(`${msg.requestLog.method} ${msg.requestLog.url} (${msg.requestLog.status})`), + ); + } else if (msg.requestLog.status >= 400) { + logger.info( + logger.dim( + logger.red(`${msg.requestLog.method} ${msg.requestLog.url} (${msg.requestLog.status})`), + ), + ); + } else if (msg.requestLog.status >= 300) { + logger.info( + logger.dim( + logger.magenta( + `${msg.requestLog.method} ${msg.requestLog.url} (${msg.requestLog.status})`, + ), + ), + ); + } else { + logger.info(logger.dim(`${logger.cyan(msg.requestLog.method)} ${msg.requestLog.url}`)); + } + } + }; + + const serverError = async (msg: DevServerMessage): Promise => { + if (msg.error) { + if (hasStarted) { + logger.error(msg.error.message + ' ' + msg.error.stack); + } else { + await close(); + reject(msg.error.message); + } + } + }; + + const requestBuildResults = (): void => { + if (sendToServer) { + if (lastBuildResults != null) { + const msg: DevServerMessage = { + buildResults: { ...lastBuildResults }, + isActivelyBuilding, + }; + // Don't send previous live reload data + delete msg.buildResults!.hmr; + sendToServer(msg); + } else { + sendToServer({ isActivelyBuilding: true }); + } + } + }; + + const compilerRequest = async (compilerRequestPath: string): Promise => { + if (watcher?.request && sendToServer) { + const compilerRequestResults = await watcher.request({ path: compilerRequestPath }); + sendToServer({ compilerRequestResults }); + } + }; + + const receiveFromServer = (msg: DevServerMessage): void => { + try { + if (msg.serverStarted) { + serverStarted(msg); + } else if (msg.serverClosed) { + logger.debug(`dev server closed: ${browserUrl}`); + closeResolve?.(); + } else if (msg.requestBuildResults) { + requestBuildResults(); + } else if (msg.compilerRequestPath) { + compilerRequest(msg.compilerRequestPath); + } else if (msg.requestLog) { + requestLog(msg); + } else if (msg.error) { + serverError(msg); + } else { + logger.debug(`server msg not handled: ${JSON.stringify(msg)}`); + } + } catch (e) { + logger.error('receiveFromServer: ' + e); + } + }; + + try { + if (watcher) { + // Cast emit to the generic callback signature that watcher.on accepts + removeWatcher = watcher.on(emit as (eventName: string, data: any) => void); + } + + // Initialize server process (either worker or in-process) + sendToServer = initServerProcessFn(receiveFromServer); + + sendToServer({ startServer: devServerConfig }); + } catch (e) { + close(); + reject(e); + } +} + +export { initServerProcess } from './server'; diff --git a/packages/dev-server/src/server/open-in-editor.d.ts b/packages/dev-server/src/server/open-in-editor.d.ts new file mode 100644 index 00000000000..152c7cf7638 --- /dev/null +++ b/packages/dev-server/src/server/open-in-editor.d.ts @@ -0,0 +1,23 @@ +/** + * Type declarations for the optional 'open-in-editor' package. + */ +declare module "open-in-editor" { + export interface OpenInEditorOptions { + editor?: string; + } + + export interface Editor { + open(path: string): Promise; + } + + export interface EditorDetector { + detect(): Promise; + } + + export function configure( + options: OpenInEditorOptions, + callback: (err: unknown) => void, + ): Editor | null; + + export const editors: Record; +} diff --git a/packages/dev-server/src/server/server.ts b/packages/dev-server/src/server/server.ts new file mode 100644 index 00000000000..e27123eb0e9 --- /dev/null +++ b/packages/dev-server/src/server/server.ts @@ -0,0 +1,321 @@ +/** + * HTTP and WebSocket server. + * Consolidated from server-process.ts, server-http.ts, and server-web-socket.ts. + * Uses native Node 22+ WebSocket instead of the 'ws' package. + */ + +import * as http from 'node:http'; +import * as https from 'node:https'; +import * as net from 'node:net'; +import { WebSocketServer, type WebSocket as NodeWebSocket } from 'ws'; + +import { + createServerContext, + type BuildRequestResolve, + type CompilerRequestResolve, +} from './context'; +import { openInBrowser } from './editor'; +import { createRequestHandler } from './handlers'; +import { getBrowserUrl, normalizePath, DEV_SERVER_INIT_URL } from './utils'; +import type { + CompilerBuildResults, + DevServerConfig, + DevServerContext, + DevServerMessage, + DevServerSendMessage, + DevWebSocket, +} from './types'; + +// ============================================================================= +// HTTP Server +// ============================================================================= + +function createHttpServer( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, +): http.Server | https.Server { + const reqHandler = createRequestHandler(devServerConfig, serverCtx); + const credentials = devServerConfig.https; + + return credentials ? https.createServer(credentials, reqHandler) : http.createServer(reqHandler); +} + +// ============================================================================= +// Port Detection +// ============================================================================= + +export async function findClosestOpenPort( + host: string, + port: number, + strictPort = false, +): Promise { + const isTaken = await isPortTaken(host, port); + + if (!isTaken) { + return port; + } + + if (strictPort) { + throw new Error( + `Port ${port} is already in use. Please specify a different port or set strictPort to false.`, + ); + } + + // Recursively find the next available port + async function findNext(portToCheck: number): Promise { + const taken = await isPortTaken(host, portToCheck); + if (!taken) { + return portToCheck; + } + return findNext(portToCheck + 1); + } + + return findNext(port + 1); +} + +function isPortTaken(host: string, port: number): Promise { + return new Promise((resolve, reject) => { + const tester = net + .createServer() + .once('error', () => { + resolve(true); + }) + .once('listening', () => { + tester.once('close', () => resolve(false)).close(); + }) + .on('error', (err) => { + reject(err); + }) + .listen(port, host); + }); +} + +// ============================================================================= +// WebSocket Server (Native Node 22+) +// ============================================================================= + +interface DevWS extends NodeWebSocket { + isAlive: boolean; +} + +function createWebSocket( + httpServer: http.Server | https.Server, + onMessageFromClient: (msg: DevServerMessage) => void, +): DevWebSocket { + const wsServer = new WebSocketServer({ server: httpServer }); + + wsServer.on('connection', (rawWs: NodeWebSocket) => { + const ws = rawWs as DevWS; + ws.isAlive = true; + + ws.on('message', (data) => { + try { + onMessageFromClient(JSON.parse(data.toString())); + } catch (e) { + console.error('WebSocket message parse error:', e); + } + }); + + ws.on('pong', () => { + ws.isAlive = true; + }); + + // Handle errors gracefully + ws.on('error', (err) => { + console.error('WebSocket error:', err); + }); + }); + + // Heartbeat interval to detect stale connections + const pingInterval = setInterval(() => { + wsServer.clients.forEach((ws) => { + const devWs = ws as DevWS; + if (!devWs.isAlive) { + return devWs.close(1000); + } + devWs.isAlive = false; + devWs.ping(); + }); + }, 10000); + + return { + sendToBrowser: (msg: DevServerMessage): void => { + if (msg && wsServer && wsServer.clients) { + const data = JSON.stringify(msg); + wsServer.clients.forEach((ws) => { + if (ws.readyState === ws.OPEN) { + ws.send(data); + } + }); + } + }, + close: (): Promise => { + return new Promise((resolve, reject) => { + clearInterval(pingInterval); + wsServer.clients.forEach((ws) => { + ws.close(1000); + }); + wsServer.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }, + }; +} + +// ============================================================================= +// Server Process +// ============================================================================= + +interface ServerProcessOptions { + sys: { + destroy(): Promise; + stat(path: string): Promise<{ isFile: boolean; isDirectory: boolean; size: number }>; + readFile(path: string, encoding: string): Promise; + readFileSync(path: string, encoding?: string): string; + readDir(path: string): Promise; + }; +} + +export function initServerProcess(sendMsg: DevServerSendMessage): (msg: DevServerMessage) => void { + let server: http.Server | https.Server | null = null; + let webSocket: DevWebSocket | null = null; + let serverCtx: DevServerContext | null = null; + + const buildResultsResolves: BuildRequestResolve[] = []; + const compilerRequestResolves: CompilerRequestResolve[] = []; + + const createNodeSys = async (): Promise => { + const { createNodeSys: createSys } = await import('@stencil/core/sys/node'); + return createSys({ process }) as ServerProcessOptions['sys']; + }; + + const startServer = async (msg: DevServerMessage): Promise => { + const devServerConfig = msg.startServer!; + + devServerConfig.port = await findClosestOpenPort( + devServerConfig.address!, + devServerConfig.port!, + devServerConfig.strictPort, + ); + + devServerConfig.browserUrl = getBrowserUrl( + devServerConfig.protocol!, + devServerConfig.address!, + devServerConfig.port!, + devServerConfig.basePath!, + '/', + ); + + devServerConfig.root = normalizePath(devServerConfig.root!); + + const sys = await createNodeSys(); + serverCtx = createServerContext( + sys as any, + sendMsg, + devServerConfig, + buildResultsResolves, + compilerRequestResolves, + ); + + server = createHttpServer(devServerConfig, serverCtx); + + webSocket = devServerConfig.websocket ? createWebSocket(server, sendMsg) : null; + + server.listen(devServerConfig.port, devServerConfig.address); + serverCtx.isServerListening = true; + + if (devServerConfig.openBrowser) { + const initialLoadUrl = getBrowserUrl( + devServerConfig.protocol!, + devServerConfig.address!, + devServerConfig.port!, + devServerConfig.basePath!, + devServerConfig.initialLoadUrl || DEV_SERVER_INIT_URL, + ); + openInBrowser({ url: initialLoadUrl }); + } + + sendMsg({ serverStarted: devServerConfig }); + }; + + const closeServer = (): void => { + const promises: Promise[] = []; + + buildResultsResolves.forEach((r) => r.reject('dev server closed')); + buildResultsResolves.length = 0; + + compilerRequestResolves.forEach((r) => r.reject('dev server closed')); + compilerRequestResolves.length = 0; + + if (serverCtx?.sys) { + promises.push(serverCtx.sys.destroy()); + } + + if (webSocket) { + promises.push(webSocket.close()); + webSocket = null; + } + + if (server) { + promises.push( + new Promise((resolve) => { + server!.close((err) => { + if (err) { + console.error(`close error: ${err}`); + } + resolve(); + }); + }), + ); + } + + Promise.all(promises).finally(() => { + sendMsg({ serverClosed: true }); + }); + }; + + const receiveMessageFromMain = (msg: DevServerMessage): void => { + try { + if (msg) { + if (msg.startServer) { + startServer(msg); + } else if (msg.closeServer) { + closeServer(); + } else if (msg.compilerRequestResults) { + for (let i = compilerRequestResolves.length - 1; i >= 0; i--) { + const r = compilerRequestResolves[i]; + if (r.path === msg.compilerRequestResults.path) { + r.resolve(msg.compilerRequestResults); + compilerRequestResolves.splice(i, 1); + } + } + } else if (serverCtx) { + if (msg.buildResults && !msg.isActivelyBuilding) { + buildResultsResolves.forEach((r) => + r.resolve(msg.buildResults as CompilerBuildResults), + ); + buildResultsResolves.length = 0; + } + if (webSocket) { + webSocket.sendToBrowser(msg); + } + } + } + } catch (e) { + let stack: string | null = null; + if (e instanceof Error) { + stack = e.stack ?? null; + } + sendMsg({ + error: { message: String(e), stack }, + }); + } + }; + + return receiveMessageFromMain; +} diff --git a/packages/dev-server/src/server/ssr.ts b/packages/dev-server/src/server/ssr.ts new file mode 100644 index 00000000000..065601ccf92 --- /dev/null +++ b/packages/dev-server/src/server/ssr.ts @@ -0,0 +1,342 @@ +/** + * SSR (Server-Side Rendering) request handling. + * Migrated from ssr-request.ts. + */ + +import * as path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { appendDevServerClientScript } from './handlers'; +import { getSsrStaticDataPath, responseHeaders } from './utils'; +import type { + DevServerConfig, + DevServerContext, + Diagnostic, + HttpRequest, + PrerenderOptions, + SsrResults, +} from './types'; +import type { ServerResponse } from 'node:http'; + +// ============================================================================= +// Types +// ============================================================================= + +interface HydrateApp { + renderToString: (html: string, options: PrerenderOptions) => Promise; +} + +interface SetupResult { + ssrApp: HydrateApp | null; + srcIndexHtml: string | null; + diagnostics: Diagnostic[]; +} + +// ============================================================================= +// SSR Page Request +// ============================================================================= + +export async function ssrPageRequest( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, + req: HttpRequest, + res: ServerResponse, +): Promise { + try { + let status = 500; + let content = ''; + + const { ssrApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx); + + if (!diagnostics.some((diagnostic) => diagnostic.level === 'error')) { + try { + const opts = getSsrHydrateOptions(devServerConfig, serverCtx, req.url!); + + const ssrResults = await ssrApp!.renderToString(srcIndexHtml!, opts); + + diagnostics.push(...ssrResults.diagnostics); + status = ssrResults.httpStatus ?? 500; + content = ssrResults.html ?? ''; + } catch (e) { + catchError(diagnostics, e); + } + } + + if (diagnostics.some((diagnostic) => diagnostic.level === 'error')) { + content = getSsrErrorContent(diagnostics); + status = 500; + } + + if (devServerConfig.websocket) { + content = appendDevServerClientScript(devServerConfig, req, content); + } + + serverCtx.logRequest(req, status); + + res.writeHead( + status, + responseHeaders({ + 'content-type': 'text/html; charset=utf-8', + 'content-length': Buffer.byteLength(content, 'utf8'), + }), + ); + res.write(content); + res.end(); + } catch (e) { + serverCtx.serve500(req, res, e, 'ssrPageRequest'); + } +} + +// ============================================================================= +// SSR Static Data Request +// ============================================================================= + +export async function ssrStaticDataRequest( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, + req: HttpRequest, + res: ServerResponse, +): Promise { + try { + const data: Record = {}; + let httpCache = false; + + const { ssrApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx); + + if (!diagnostics.some((diagnostic) => diagnostic.level === 'error')) { + try { + const { ssrPath, hasQueryString } = getSsrStaticDataPath(req); + const url = new URL(ssrPath, req.url!); + + const opts = getSsrHydrateOptions(devServerConfig, serverCtx, url); + + const ssrResults = await ssrApp!.renderToString(srcIndexHtml!, opts); + + diagnostics.push(...ssrResults.diagnostics); + + ssrResults.staticData.forEach((s: { type: string; id: string; content: string }) => { + if (s.type === 'application/json') { + data[s.id] = JSON.parse(s.content); + } else { + data[s.id] = s.content; + } + }); + data.components = ssrResults.components.map((c: { tag: string }) => c.tag).sort(); + httpCache = hasQueryString; + } catch (e) { + catchError(diagnostics, e); + } + } + + if (diagnostics.length > 0) { + data.diagnostics = diagnostics; + } + + const status = diagnostics.some((diagnostic) => diagnostic.level === 'error') ? 500 : 200; + const content = JSON.stringify(data); + serverCtx.logRequest(req, status); + + res.writeHead( + status, + responseHeaders( + { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(content, 'utf8'), + }, + httpCache && status === 200, + ), + ); + res.write(content); + res.end(); + } catch (e) { + serverCtx.serve500(req, res, e, 'ssrStaticDataRequest'); + } +} + +// ============================================================================= +// Hydrate App Setup +// ============================================================================= + +async function setupHydrateApp( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, +): Promise { + let srcIndexHtml: string | null = null; + let ssrApp: HydrateApp | null = null; + + const buildResults = await serverCtx.getBuildResults(); + const diagnostics: Diagnostic[] = []; + + if (serverCtx.prerenderConfig == null && isString(devServerConfig.prerenderConfig)) { + try { + // Dynamic import the compiler + const compiler = await import('@stencil/core/compiler'); + const prerenderConfigResults = await compiler.nodeRequire(devServerConfig.prerenderConfig); + diagnostics.push(...prerenderConfigResults.diagnostics); + if (prerenderConfigResults.module?.config) { + serverCtx.prerenderConfig = prerenderConfigResults.module.config; + } + } catch (e) { + catchError(diagnostics, e); + } + } + + if (!isString(buildResults.ssrAppFilePath)) { + diagnostics.push({ + messageText: 'Missing ssrAppFilePath', + level: 'error', + type: 'ssr', + lines: [], + }); + } else if (!isString(devServerConfig.srcIndexHtml)) { + diagnostics.push({ + messageText: 'Missing srcIndexHtml', + level: 'error', + type: 'ssr', + lines: [], + }); + } else { + srcIndexHtml = await serverCtx.sys.readFile(devServerConfig.srcIndexHtml, 'utf8'); + if (!isString(srcIndexHtml)) { + diagnostics.push({ + level: 'error', + lines: [], + messageText: `Unable to load src index html: ${devServerConfig.srcIndexHtml}`, + type: 'ssr', + }); + } else { + const ssrAppFilePath = path.resolve(buildResults.ssrAppFilePath); + + try { + // Use cache-busting query string for ESM dynamic import + // This ensures we get a fresh module on each build + const hydrateUrl = pathToFileURL(ssrAppFilePath); + hydrateUrl.search = `?t=${Date.now()}`; + const hydrateModule = await import(hydrateUrl.href); + ssrApp = hydrateModule.default || hydrateModule; + } catch (e) { + catchError(diagnostics, e); + } + } + } + + return { + ssrApp, + srcIndexHtml, + diagnostics, + }; +} + +// ============================================================================= +// SSR Hydrate Options +// ============================================================================= + +function getSsrHydrateOptions( + devServerConfig: DevServerConfig, + serverCtx: DevServerContext, + url: URL, +): PrerenderOptions { + const opts: PrerenderOptions = { + url: url.href, + addModulePreloads: false, + approximateLineWidth: 120, + inlineExternalStyleSheets: false, + minifyScriptElements: false, + minifyStyleElements: false, + removeAttributeQuotes: false, + removeBooleanAttributeQuotes: false, + removeEmptyAttributes: false, + removeHtmlComments: false, + prettyHtml: true, + }; + + const prerenderConfig = serverCtx?.prerenderConfig; + + if (isFunction(prerenderConfig?.prerenderOptions)) { + const userOpts = prerenderConfig.prerenderOptions(url); + if (userOpts) { + Object.assign(opts, userOpts); + } + } + + if (isFunction(serverCtx.sys.applyPrerenderGlobalPatch)) { + const orgBeforeHydrate = opts.beforeSsr; + const applyPatch = serverCtx.sys.applyPrerenderGlobalPatch; + opts.beforeSsr = (document: Document) => { + const devServerBaseUrl = new URL(devServerConfig.browserUrl!); + const devServerHostUrl = devServerBaseUrl.origin; + applyPatch({ + devServerHostUrl, + window: document.defaultView, + }); + + if (typeof orgBeforeHydrate === 'function') { + return orgBeforeHydrate(document); + } + }; + } + + return opts; +} + +// ============================================================================= +// Error Handling +// ============================================================================= + +function getSsrErrorContent(diagnostics: Diagnostic[]): string { + return ` + + + SSR Error + + + +

    SSR Dev Error

    + ${diagnostics + .map( + (diagnostic) => ` +

    + ${diagnostic.messageText} +

    + `, + ) + .join('')} + +`; +} + +function catchError(diagnostics: Diagnostic[], err: unknown): void { + const diagnostic: Diagnostic = { + level: 'error', + type: 'runtime', + messageText: '', + lines: [], + }; + + if (err instanceof Error) { + diagnostic.messageText = err.message; + if (err.stack) { + diagnostic.messageText += '\n' + err.stack; + } + } else { + diagnostic.messageText = String(err); + } + + diagnostics.push(diagnostic); +} + +// ============================================================================= +// Utility Functions +// ============================================================================= + +function isString(val: unknown): val is string { + return typeof val === 'string'; +} + +function isFunction(val: unknown): val is Function { + return typeof val === 'function'; +} diff --git a/packages/dev-server/src/server/types.ts b/packages/dev-server/src/server/types.ts new file mode 100644 index 00000000000..53bb695cda4 --- /dev/null +++ b/packages/dev-server/src/server/types.ts @@ -0,0 +1,152 @@ +/** + * Types for the Stencil Dev Server. + * Re-exports relevant types from @stencil/core and defines local interfaces. + */ + +import type { ServerResponse } from 'node:http'; + +// Re-export types from core that we need +export type { + CompilerBuildResults, + CompilerFsStats, + CompilerRequestResponse, + CompilerSystem, + CompilerWatcher, + DevServer, + DevServerConfig, + DevServerEditor, + Diagnostic, + Logger, + PageReloadStrategy, + PrerenderConfig, + PrerenderOptions, + SsrResults, + StencilDevServerConfig, +} from '@stencil/core/compiler'; + +/** + * Internal dev server message protocol for communication between + * main process and browser clients. + */ +export interface DevServerMessage { + startServer?: DevServerConfig; + closeServer?: boolean; + serverStarted?: DevServerConfig; + serverClosed?: boolean; + buildStart?: boolean; + buildLog?: BuildLog; + buildResults?: CompilerBuildResults; + requestBuildResults?: boolean; + error?: { message?: string; type?: string; stack?: string | null }; + isActivelyBuilding?: boolean; + compilerRequestPath?: string; + compilerRequestResults?: CompilerRequestResponse; + requestLog?: { + method: string; + url: string; + status: number; + }; +} + +export interface BuildLog { + buildId?: number; + messages?: string[]; + progress?: number; +} + +export type DevServerSendMessage = (msg: DevServerMessage) => void; + +/** + * Server context passed to request handlers. + */ +export interface DevServerContext { + connectorHtml: string | null; + dirTemplate: string | null; + getBuildResults: () => Promise; + getCompilerRequest: (path: string) => Promise; + isServerListening: boolean; + logRequest: (req: HttpRequest, status: number) => void; + prerenderConfig: PrerenderConfig | null; + serve302: (req: HttpRequest, res: ServerResponse, pathname?: string) => void; + serve404: (req: HttpRequest, res: ServerResponse, xSource: string, content?: string) => void; + serve500: (req: HttpRequest, res: ServerResponse, error: unknown, xSource: string) => void; + sys: CompilerSystem; +} + +/** + * Normalized HTTP request object. + */ +export interface HttpRequest { + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS'; + acceptHeader: string; + url: URL | null; + searchParams: URLSearchParams | null; + pathname?: string; + filePath?: string; + stats?: CompilerFsStats; + headers?: Record; + host?: string; +} + +/** + * Response headers for dev server responses. + */ +export interface DevResponseHeaders { + 'cache-control'?: string; + expires?: string; + 'content-type'?: string; + 'content-length'?: number; + date?: string; + 'access-control-allow-origin'?: string; + 'access-control-expose-headers'?: string; + 'content-encoding'?: 'gzip'; + vary?: 'Accept-Encoding'; + server?: string; + 'x-directory-index'?: string; + 'x-source'?: string; + // Index signature for compatibility with OutgoingHttpHeaders + [key: string]: string | number | string[] | undefined; +} + +/** + * Open in editor request data. + */ +export interface OpenInEditorData { + file?: string; + line?: number; + column?: number; + editor?: string; + exists?: boolean; + open?: string; + error?: string; +} + +/** + * Dev client configuration sent to the browser. + */ +export interface DevClientConfig { + basePath: string; + editors: DevServerEditor[]; + reloadStrategy: PageReloadStrategy; + socketUrl?: string; +} + +/** + * WebSocket server interface for browser communication. + */ +export interface DevWebSocket { + sendToBrowser: (msg: DevServerMessage) => void; + close: () => Promise; +} + +// Import core types +import type { + CompilerBuildResults, + CompilerRequestResponse, + DevServerConfig, + DevServerEditor, + PrerenderConfig, + CompilerFsStats, + CompilerSystem, + PageReloadStrategy, +} from '@stencil/core/compiler'; diff --git a/packages/dev-server/src/server/utils.ts b/packages/dev-server/src/server/utils.ts new file mode 100644 index 00000000000..7098a0e5ce6 --- /dev/null +++ b/packages/dev-server/src/server/utils.ts @@ -0,0 +1,374 @@ +/** + * Dev server utilities and constants. + * Consolidated from dev-server-constants.ts and dev-server-utils.ts + */ + +import type { DevResponseHeaders, HttpRequest, DevServerConfig } from './types'; +import type { OutgoingHttpHeaders } from 'node:http'; + +// ============================================================================= +// Constants +// ============================================================================= + +export const DEV_SERVER_URL = '/~dev-server'; +const DEV_MODULE_URL = '/~dev-module'; +export const DEV_SERVER_INIT_URL = `${DEV_SERVER_URL}-init`; +const OPEN_IN_EDITOR_URL = `${DEV_SERVER_URL}-open-in-editor`; + +// Dev server version - will be injected at build time +export const VERSION = '5.0.0'; + +// ============================================================================= +// Response Headers +// ============================================================================= + +const DEFAULT_HEADERS: DevResponseHeaders = { + 'cache-control': 'no-cache, no-store, must-revalidate, max-age=0', + expires: '0', + date: 'Wed, 1 Jan 2000 00:00:00 GMT', + server: `Stencil Dev Server ${VERSION}`, + 'access-control-allow-origin': '*', + 'access-control-expose-headers': '*', +}; + +/** + * Build response headers with optional HTTP caching. + * + * @param headers - custom headers to merge with defaults + * @param httpCache - whether to enable HTTP caching + * @returns the combined response headers + */ +export function responseHeaders( + headers: DevResponseHeaders, + httpCache = false, +): OutgoingHttpHeaders { + const result: OutgoingHttpHeaders = { ...DEFAULT_HEADERS, ...headers }; + if (httpCache) { + result['cache-control'] = 'max-age=3600'; + delete result['date']; + delete result['expires']; + } + return result; +} + +// ============================================================================= +// URL Utilities +// ============================================================================= + +/** + * Build a browser URL from components. + * + * @param protocol - the URL protocol (http or https) + * @param address - the server address + * @param port - the server port + * @param basePath - the base path + * @param pathname - the URL pathname + * @returns the complete browser URL + */ +export function getBrowserUrl( + protocol: string, + address: string, + port: number, + basePath: string, + pathname: string, +): string { + address = address === '0.0.0.0' ? 'localhost' : address; + const portSuffix = !port || port === 80 || port === 443 ? '' : ':' + port; + + let path = basePath; + if (pathname.startsWith('/')) { + pathname = pathname.substring(1); + } + path += pathname; + + protocol = protocol.replace(/:/g, ''); + + return `${protocol}://${address}${portSuffix}${path}`; +} + +/** + * Get the URL for the dev server client script. + * + * @param devServerConfig - the dev server configuration + * @param host - optional host override + * @param protocol - optional protocol override + * @returns the dev server client URL + */ +export function getDevServerClientUrl( + devServerConfig: DevServerConfig, + host: string | undefined, + protocol: string | undefined, +): string { + let address = devServerConfig.address!; + let port: number | null = devServerConfig.port!; + + if (host) { + address = host; + port = null; + } + + return getBrowserUrl( + protocol ?? devServerConfig.protocol!, + address, + port!, + devServerConfig.basePath!, + DEV_SERVER_URL, + ); +} + +// ============================================================================= +// Content Type Detection +// ============================================================================= + +// Simple content type database - common web file extensions +const CONTENT_TYPES: Record = { + html: 'text/html', + htm: 'text/html', + css: 'text/css', + js: 'text/javascript', + mjs: 'text/javascript', + json: 'application/json', + xml: 'application/xml', + svg: 'image/svg+xml', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + ico: 'image/x-icon', + woff: 'font/woff', + woff2: 'font/woff2', + ttf: 'font/ttf', + otf: 'font/otf', + eot: 'application/vnd.ms-fontobject', + mp3: 'audio/mpeg', + mp4: 'video/mp4', + webm: 'video/webm', + ogg: 'audio/ogg', + wav: 'audio/wav', + pdf: 'application/pdf', + zip: 'application/zip', + wasm: 'application/wasm', + map: 'application/json', + txt: 'text/plain', + md: 'text/markdown', + ts: 'text/typescript', + tsx: 'text/typescript-jsx', +}; + +/** + * Get the content type for a file based on its extension. + * + * @param filePath - the file path to check + * @returns the MIME content type + */ +export function getContentType(filePath: string): string { + const last = filePath.replace(/^.*[/\\]/, '').toLowerCase(); + const ext = last.replace(/^.*\./, '').toLowerCase(); + + const hasPath = last.length < filePath.length; + const hasDot = ext.length < last.length - 1; + + return ((hasDot || !hasPath) && CONTENT_TYPES[ext]) || 'application/octet-stream'; +} + +// ============================================================================= +// File Type Checks +// ============================================================================= + +/** + * Check if a file is an HTML file. + * + * @param filePath - the file path to check + * @returns true if the file is HTML + */ +export function isHtmlFile(filePath: string): boolean { + const lower = filePath.toLowerCase().trim(); + return lower.endsWith('.html') || lower.endsWith('.htm'); +} + +/** + * Check if a file is a CSS file. + * + * @param filePath - the file path to check + * @returns true if the file is CSS + */ +export function isCssFile(filePath: string): boolean { + return filePath.toLowerCase().trim().endsWith('.css'); +} + +const TXT_EXT = ['css', 'html', 'htm', 'js', 'json', 'svg', 'xml', 'mjs', 'ts', 'tsx', 'md', 'txt']; + +/** + * Check if a file is simple text (CSS, HTML, JS, JSON, etc.). + * + * @param filePath - the file path to check + * @returns true if the file is a simple text format + */ +export function isSimpleText(filePath: string): boolean { + const ext = filePath.toLowerCase().trim().split('.').pop(); + return ext ? TXT_EXT.includes(ext) : false; +} + +/** + * Check if a pathname has no file extension. + * + * @param pathname - the URL pathname to check + * @returns true if the path has no extension + */ +export function isExtensionLessPath(pathname: string): boolean { + const parts = pathname.split('/'); + const lastPart = parts[parts.length - 1]; + return !lastPart.includes('.'); +} + +/** + * Check if a pathname is for SSR static data (page.state.json). + * + * @param pathname - the URL pathname to check + * @returns true if the path is for SSR static data + */ +export function isSsrStaticDataPath(pathname: string): boolean { + const parts = pathname.split('/'); + const fileName = parts[parts.length - 1].split('?')[0]; + return fileName === 'page.state.json'; +} + +/** + * Extract SSR static data path information from an HTTP request. + * + * @param req - the HTTP request object + * @returns an object containing ssrPath, fileName, and hasQueryString + */ +export function getSsrStaticDataPath(req: HttpRequest): { + ssrPath: string; + fileName: string; + hasQueryString: boolean; +} { + const parts = req.url!.href.split('/'); + const fileName = parts[parts.length - 1]; + const fileNameParts = fileName.split('?'); + + parts.pop(); + + let ssrPath = new URL(parts.join('/')).href; + if (!ssrPath.endsWith('/') && req.headers) { + const h = new Headers(req.headers as HeadersInit); + if (h.get('referer')?.endsWith('/')) { + ssrPath += '/'; + } + } + + return { + ssrPath, + fileName: fileNameParts[0], + hasQueryString: typeof fileNameParts[1] === 'string' && fileNameParts[1].length > 0, + }; +} + +// ============================================================================= +// Path Type Checks +// ============================================================================= + +/** + * Check if a pathname is for the dev client. + * + * @param pathname - the URL pathname to check + * @returns true if the path is for the dev client + */ +export function isDevClient(pathname: string): boolean { + return pathname.startsWith(DEV_SERVER_URL); +} + +/** + * Check if a pathname is for a dev module. + * + * @param pathname - the URL pathname to check + * @returns true if the path is for a dev module + */ +export function isDevModule(pathname: string): boolean { + return pathname.includes(DEV_MODULE_URL); +} + +/** + * Check if a pathname is for the open-in-editor endpoint. + * + * @param pathname - the URL pathname to check + * @returns true if the path is for opening in editor + */ +export function isOpenInEditor(pathname: string): boolean { + return pathname === OPEN_IN_EDITOR_URL; +} + +/** + * Check if a pathname is for the initial dev server load. + * + * @param pathname - the URL pathname to check + * @returns true if the path is for initial dev server load + */ +export function isInitialDevServerLoad(pathname: string): boolean { + return pathname === DEV_SERVER_INIT_URL; +} + +/** + * Check if a pathname is for the dev server client script. + * + * @param pathname - the URL pathname to check + * @returns true if the path is for the dev server client + */ +export function isDevServerClient(pathname: string): boolean { + return pathname === DEV_SERVER_URL; +} + +// ============================================================================= +// Compression +// ============================================================================= + +/** + * Check if a response should be gzip compressed. + * + * @param devServerConfig - the dev server configuration + * @param req - the HTTP request object + * @returns true if the response should be compressed + */ +export function shouldCompress(devServerConfig: DevServerConfig, req: HttpRequest): boolean { + if (!devServerConfig.gzip) { + return false; + } + + if (req.method !== 'GET') { + return false; + } + + const acceptEncoding = req.headers?.['accept-encoding']; + if (typeof acceptEncoding !== 'string') { + return false; + } + + return acceptEncoding.includes('gzip'); +} + +// ============================================================================= +// Path Normalization +// ============================================================================= + +/** + * Normalize a file path to use forward slashes and remove redundant slashes. + * + * @param path - the file path to normalize + * @returns the normalized path + */ +export function normalizePath(path: string): string { + // Convert backslashes to forward slashes + let normalized = path.replace(/\\/g, '/'); + + // Remove redundant slashes (but keep leading double slash for UNC paths) + normalized = normalized.replace(/\/+/g, '/'); + + // Handle Windows UNC paths + if (path.startsWith('\\\\')) { + normalized = '/' + normalized; + } + + return normalized; +} diff --git a/packages/dev-server/src/server/worker-main.ts b/packages/dev-server/src/server/worker-main.ts new file mode 100644 index 00000000000..017b37804c6 --- /dev/null +++ b/packages/dev-server/src/server/worker-main.ts @@ -0,0 +1,98 @@ +/** + * Worker process proxy for dev server. + * Forks a child process to run the HTTP and WebSocket server in isolation. + */ + +import { fork, type ChildProcess } from 'node:child_process'; +import * as path from 'node:path'; + +import type { DevServerMessage } from './types'; + +/** + * Initialize the dev server in a forked worker process. + * This provides process isolation so that server crashes don't affect the main compiler. + * + * @param sendToMain - Callback to send messages from worker to main process + * @returns Function to send messages from main to worker process + */ +export function initServerProcessWorkerProxy( + sendToMain: (msg: DevServerMessage) => void, +): (msg: DevServerMessage) => void { + // Resolve the worker thread entry point + const workerPath = path.join(import.meta.dirname, 'worker-thread.js'); + + // Filter out debug/inspect args to avoid port conflicts + const filteredExecArgs = process.execArgv.filter((v) => !/^--(debug|inspect)/.test(v)); + + const forkOpts = { + execArgv: filteredExecArgs, + env: process.env, + cwd: process.cwd(), + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] as ['pipe', 'pipe', 'pipe', 'ipc'], + }; + + // Start a new child process for the HTTP and WebSocket server + let serverProcess: ChildProcess | null = fork(workerPath, [], forkOpts); + + /** + * Send a message from main to the worker process + * + * @param msg - the message to send to the worker + */ + const receiveFromMain = (msg: DevServerMessage): void => { + if (serverProcess && serverProcess.connected) { + serverProcess.send(msg); + } else if (msg.closeServer) { + sendToMain({ serverClosed: true }); + } + }; + + // Get messages from the worker and send them to main + serverProcess.on('message', (msg: DevServerMessage) => { + if (msg.serverClosed && serverProcess) { + serverProcess.kill('SIGINT'); + serverProcess = null; + } + sendToMain(msg); + }); + + // Forward stdout from worker to console + serverProcess.stdout?.on('data', (data: Buffer) => { + console.log(`dev server: ${data}`); + }); + + // Forward stderr from worker to main as error message + serverProcess.stderr?.on('data', (data: Buffer) => { + sendToMain({ error: { message: 'stderr: ' + data.toString(), type: 'stderr', stack: null } }); + }); + + // Handle worker process errors + serverProcess.on('error', (error) => { + sendToMain({ + error: { + message: error.message, + type: 'worker-error', + stack: error.stack || null, + }, + }); + }); + + // Handle worker process exit + serverProcess.on('exit', (code) => { + if (code !== 0 && code !== null) { + sendToMain({ + error: { + message: `Worker process exited with code ${code}`, + type: 'worker-exit', + stack: null, + }, + }); + } + if (serverProcess) { + serverProcess = null; + sendToMain({ serverClosed: true }); + } + }); + + return receiveFromMain; +} diff --git a/packages/dev-server/src/server/worker-thread.js b/packages/dev-server/src/server/worker-thread.js new file mode 100644 index 00000000000..90da4678604 --- /dev/null +++ b/packages/dev-server/src/server/worker-thread.js @@ -0,0 +1,63 @@ +/** + * Worker thread entry point for dev server. + * This file is the main entry for the forked child process that runs the HTTP/WebSocket server. + * It communicates with the parent process via IPC messages. + */ + +import { initServerProcess } from './index.mjs'; + +let closeTmr = null; + +/** + * Handle errors when sending messages to parent process + * + * @param err - the error object from process.send + */ +const sendHandle = (err) => { + if (err && err.code === 'ERR_IPC_CHANNEL_CLOSED') { + // Parent process closed the IPC channel, exit cleanly + process.exit(0); + } +}; + +/** + * Initialize the server process and set up message passing + */ +const receiveMessageFromMain = initServerProcess((msg) => { + // Send message from worker going to main + process.send(msg, sendHandle); + + if (msg.serverClosed) { + clearTimeout(closeTmr); + process.exit(0); + } +}); + +/** + * Receive messages from the main process + */ +process.on('message', (msg) => { + if (msg && msg.closeServer) { + // Set a timeout to force exit if graceful shutdown takes too long + closeTmr = setTimeout(() => { + process.exit(0); + }, 5000); + } + + receiveMessageFromMain(msg); +}); + +/** + * Handle uncaught promise rejections and report to main process + */ +process.on('unhandledRejection', (e) => { + process.send( + { + error: { + message: 'unhandledRejection: ' + e, + stack: typeof e.stack === 'string' ? e.stack : null, + }, + }, + sendHandle, + ); +}); diff --git a/src/dev-server/static/favicon.ico b/packages/dev-server/static/favicon.ico similarity index 100% rename from src/dev-server/static/favicon.ico rename to packages/dev-server/static/favicon.ico diff --git a/src/dev-server/templates/directory-index.html b/packages/dev-server/templates/directory-index.html old mode 100755 new mode 100644 similarity index 80% rename from src/dev-server/templates/directory-index.html rename to packages/dev-server/templates/directory-index.html index 331c818159b..431e64e50ad --- a/src/dev-server/templates/directory-index.html +++ b/packages/dev-server/templates/directory-index.html @@ -1,10 +1,13 @@ - + {{title}} - - + + - +

    {{nav}}

    -
      {{files}} +
        + {{files}}
    diff --git a/packages/dev-server/templates/initial-load.html b/packages/dev-server/templates/initial-load.html new file mode 100644 index 00000000000..5cc27ed0c7e --- /dev/null +++ b/packages/dev-server/templates/initial-load.html @@ -0,0 +1,157 @@ + + + + + + + + Initializing First Build... + + + + +
    +
    +
    Initializing First Build...
    +
    + +
    +
    +
    + +
    +
    
    +    
    + + + + diff --git a/packages/dev-server/tsconfig.json b/packages/dev-server/tsconfig.json new file mode 100644 index 00000000000..4fdab3781ba --- /dev/null +++ b/packages/dev-server/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "declaration": true, + "emitDeclarationOnly": true, + "declarationDir": "dist", + "rootDir": "src", + "outDir": "dist", + "forceConsistentCasingInFileNames": true, + "lib": ["dom", "es2022"], + "module": "esnext", + "moduleResolution": "bundler", + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": false, + "strict": true, + "target": "es2022", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "dist"] +} diff --git a/packages/dev-server/tsdown.config.ts b/packages/dev-server/tsdown.config.ts new file mode 100644 index 00000000000..d2d471fed9a --- /dev/null +++ b/packages/dev-server/tsdown.config.ts @@ -0,0 +1,45 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig([ + // Server-side Node targets + { + entry: { + index: 'src/server/index.ts', + }, + outDir: 'dist', + format: ['esm'], + platform: 'node', + target: 'node22', + dts: true, + clean: true, + deps: { + neverBundle: [/^node:/, '@stencil/core'], + skipNodeModulesBundle: true, + }, + copy: [ + // Copy static assets needed by the dev server + { from: 'templates', to: 'dist' }, + { from: 'static', to: 'dist' }, + { from: 'connector.html', to: 'dist' }, + // Copy worker thread entry point (must be .js for fork to load it) + { from: 'src/server/worker-thread.js', to: 'dist' }, + ], + }, + // Browser-side client (HMR, connector) + { + entry: { + 'client/index': 'src/client/index.ts', + }, + inputOptions: { + moduleTypes: { + '.css': 'js', + }, + }, + outDir: 'dist', + format: ['esm'], + platform: 'browser', + target: ['es2022'], + dts: true, + clean: false, + }, +]); diff --git a/packages/mock-doc/package.json b/packages/mock-doc/package.json new file mode 100644 index 00000000000..a5f8768ed99 --- /dev/null +++ b/packages/mock-doc/package.json @@ -0,0 +1,52 @@ +{ + "name": "@stencil/mock-doc", + "version": "5.0.0-alpha.5", + "description": "A minimal mock DOM implementation for SSR and testing", + "keywords": [ + "dom", + "happy-dom", + "jsdom", + "mock", + "node", + "ssr", + "testing", + "virtual" + ], + "homepage": "https://stenciljs.com/", + "license": "MIT", + "author": "StencilJs Contributors", + "repository": { + "type": "git", + "url": "git+https://github.com/stenciljs/core.git" + }, + "files": [ + "dist/" + ], + "type": "module", + "main": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, + "scripts": { + "build": "tsdown", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "nwsapi": "^2.2.23", + "parse5": "7.2.1" + }, + "devDependencies": { + "@stencil/vitest": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/src/mock-doc/test/attribute.spec.ts b/packages/mock-doc/src/_test_/attribute.spec.ts similarity index 83% rename from src/mock-doc/test/attribute.spec.ts rename to packages/mock-doc/src/_test_/attribute.spec.ts index 98dd0274d1d..762553b5e6b 100644 --- a/src/mock-doc/test/attribute.spec.ts +++ b/packages/mock-doc/src/_test_/attribute.spec.ts @@ -1,5 +1,7 @@ -import { XLINK_NS } from '../../runtime/runtime-constants'; +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockAttr, MockAttributeMap } from '../attribute'; +import { XLINK_NS } from '../constants'; import { MockDocument } from '../document'; import { MockElement, MockHTMLElement } from '../node'; @@ -49,16 +51,15 @@ describe('attributes', () => { element.setAttribute('viewbox', '0 0 20 20'); expect(element.attributes.length).toBe(2); - expect(element.attributes.getNamedItem('viewBox').value).toEqual('0 0 10 10'); - expect(element.attributes.getNamedItem('viewbox').value).toEqual('0 0 20 20'); - - expect(element.attributes.getNamedItemNS(null, 'viewBox').value).toEqual('0 0 10 10'); - expect(element.attributes.getNamedItemNS(null, 'viewbox').value).toEqual('0 0 20 20'); + expect(element.attributes.getNamedItem('viewBox')?.value).toEqual('0 0 10 10'); + expect(element.attributes.getNamedItem('viewbox')?.value).toEqual('0 0 20 20'); + expect(element.attributes.getNamedItemNS(null, 'viewBox')?.value).toEqual('0 0 10 10'); + expect(element.attributes.getNamedItemNS(null, 'viewbox')?.value).toEqual('0 0 20 20'); element.removeAttribute('viewBox'); element.removeAttribute('viewbox'); - testNsAttributes(element); + testNsAttributes(element as any); }); it('should cast attribute values to string', () => { @@ -78,7 +79,7 @@ describe('attributes', () => { expect(element.getAttribute('prop6')).toBe(''); expect(element).toEqualHtml( - `
    `, + `
    `, ); }); @@ -99,7 +100,7 @@ describe('attributes', () => { expect(element.getAttribute('prop6')).toBe(''); expect(element).toEqualHtml( - `
    `, + `
    `, ); }); @@ -122,12 +123,11 @@ describe('attributes', () => { element.setAttribute('viewBox', '0 0 10 10'); element.setAttribute('viewbox', '0 0 20 20'); - expect(element.attributes.getNamedItem('viewBox').value).toEqual('0 0 20 20'); - expect(element.attributes.getNamedItem('viewbox').value).toEqual('0 0 20 20'); + expect(element.attributes.getNamedItem('viewBox')?.value).toEqual('0 0 20 20'); + expect(element.attributes.getNamedItem('viewbox')?.value).toEqual('0 0 20 20'); expect(element.attributes.getNamedItemNS(null, 'viewBox')).toEqual(null); - expect(element.attributes.getNamedItemNS(null, 'viewbox').value).toEqual('0 0 20 20'); - + expect(element.attributes.getNamedItemNS(null, 'viewbox')?.value).toEqual('0 0 20 20'); element.removeAttribute('viewbox'); testNsAttributes(element); @@ -170,8 +170,9 @@ describe('attributes', () => { const div = doc.createElement('div'); div.setAttribute('draggable', 'true'); expect(div.getAttributeNode('draggable')).toEqual({ - _name: 'draggable', + _localName: 'draggable', _namespaceURI: null, + _prefix: null, _value: 'true', }); }); @@ -190,10 +191,10 @@ describe('attributes', () => { expect(element.attributes.length).toBe(2); expect(element.attributes.getNamedItemNS('test', 'viewBox')).toEqual(null); expect(element.attributes.getNamedItemNS('test', 'viewbox')).toEqual(null); - expect(element.attributes.getNamedItemNS('tEst', 'viewBox').name).toEqual('viewBox'); - expect(element.attributes.getNamedItemNS('tEst', 'viewbox').name).toEqual('viewbox'); - expect(element.attributes.getNamedItemNS('tEst', 'viewBox').value).toEqual('1'); - expect(element.attributes.getNamedItemNS('tEst', 'viewbox').value).toEqual('2'); + expect(element.attributes.getNamedItemNS('tEst', 'viewBox')?.name).toEqual('viewBox'); + expect(element.attributes.getNamedItemNS('tEst', 'viewbox')?.name).toEqual('viewbox'); + expect(element.attributes.getNamedItemNS('tEst', 'viewBox')?.value).toEqual('1'); + expect(element.attributes.getNamedItemNS('tEst', 'viewbox')?.value).toEqual('2'); element.removeAttributeNS('test', 'viewBox'); element.removeAttributeNS('test', 'viewbox'); diff --git a/packages/mock-doc/src/_test_/clone.spec.ts b/packages/mock-doc/src/_test_/clone.spec.ts new file mode 100644 index 00000000000..5e0465f0069 --- /dev/null +++ b/packages/mock-doc/src/_test_/clone.spec.ts @@ -0,0 +1,54 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + +import { createDocument, MockDocument } from '../document'; +import { cloneDocument } from '../window'; + +describe('cloneNode', () => { + let document: MockDocument; + beforeEach(() => { + document = new MockDocument(); + }); + + it('style', () => { + const elm = document.createElement('div'); + elm.setAttribute('style', 'color: red;'); + + const cloned = elm.cloneNode(true); + expect(cloned.getAttribute('style')).toEqual(`color: red;`); + }); + + it('id', () => { + const elm = document.createElement('div'); + elm.setAttribute('id', 'value'); + + const cloned = elm.cloneNode(true); + expect(cloned.getAttribute('id')).toEqual(`value`); + }); + + it('div', () => { + const doc = createDocument(` +
    + content +
    + `); + + const cloned = cloneDocument(doc); + const clonedDiv = cloned?.querySelector('div'); + + expect(clonedDiv?.innerHTML.trim()).toEqual(`content`); + }); + + it('template', () => { + const doc = createDocument(` + + `); + + const cloned = cloneDocument(doc); + const clonedTemplate = cloned?.querySelector('template'); + + expect(clonedTemplate?.innerHTML.trim()).toEqual(`content`); + expect(clonedTemplate?.content.firstChild?.textContent?.trim()).toEqual(`content`); + }); +}); diff --git a/src/mock-doc/test/css-style-declaration.spec.ts b/packages/mock-doc/src/_test_/css-style-declaration.spec.ts similarity index 94% rename from src/mock-doc/test/css-style-declaration.spec.ts rename to packages/mock-doc/src/_test_/css-style-declaration.spec.ts index 4fe1e257780..942191120f2 100644 --- a/src/mock-doc/test/css-style-declaration.spec.ts +++ b/packages/mock-doc/src/_test_/css-style-declaration.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockCSSStyleDeclaration } from '../css-style-declaration'; import { MockDocument } from '../document'; import { MockHTMLElement } from '../node'; diff --git a/src/mock-doc/test/css-style-sheet.spec.ts b/packages/mock-doc/src/_test_/css-style-sheet.spec.ts similarity index 97% rename from src/mock-doc/test/css-style-sheet.spec.ts rename to packages/mock-doc/src/_test_/css-style-sheet.spec.ts index 6b5f8fa5b83..f650cf4f556 100644 --- a/src/mock-doc/test/css-style-sheet.spec.ts +++ b/packages/mock-doc/src/_test_/css-style-sheet.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockDocument } from '../document'; import { MockStyleElement } from '../element'; diff --git a/src/mock-doc/test/custom-elements.spec.ts b/packages/mock-doc/src/_test_/custom-elements.spec.ts similarity index 92% rename from src/mock-doc/test/custom-elements.spec.ts rename to packages/mock-doc/src/_test_/custom-elements.spec.ts index 58e8ef131cb..8879e6ac645 100644 --- a/src/mock-doc/test/custom-elements.spec.ts +++ b/packages/mock-doc/src/_test_/custom-elements.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect } from '@stencil/vitest'; + import { createWindow } from '../window'; describe('customElements', () => { @@ -117,7 +119,7 @@ describe('customElements', () => { expect(connectedInc).toBe(1); expect(disconnectedInc).toBe(0); - expect(document.body.outerHTML).toEqualHtml(` + expect(document.body).toEqualHtml(`
    @@ -128,7 +130,7 @@ describe('customElements', () => { document.body.innerHTML = ''; expect(connectedInc).toBe(1); expect(disconnectedInc).toBe(1); - expect(document.body.outerHTML).toEqualHtml(``); + expect(document.body).toEqualHtml(``); }); it('connectedCallback, multiple appendChild', () => { @@ -151,7 +153,7 @@ describe('customElements', () => { expect(connectedInc).toBe(1); document.body.appendChild(cmpA2); expect(connectedInc).toBe(2); - expect(document.body.outerHTML).toEqualHtml(` + expect(document.body).toEqualHtml(` @@ -176,7 +178,7 @@ describe('customElements', () => { expect(connectedInc).toBe(0); document.body.insertBefore(cmpA, null); expect(connectedInc).toBe(1); - expect(document.body.outerHTML).toEqualHtml(``); + expect(document.body).toEqualHtml(``); }); it('connectedCallback, insertBefore elm', () => { @@ -197,7 +199,7 @@ describe('customElements', () => { const cmpA = document.createElement('cmp-a'); document.body.insertBefore(cmpA, ref); expect(connectedInc).toBe(1); - expect(document.body.outerHTML).toEqualHtml(`
    `); + expect(document.body).toEqualHtml(`
    `); }); it('appendChild nested, scoped to mocked window', () => { @@ -228,7 +230,11 @@ describe('customElements', () => { expect(connectedInc).toBe(1); expect(disconnectedInc).toBe(0); - expect(win.document.body.outerHTML).toEqualHtml(`
    `); + expect(win.document.body.outerHTML).toEqualHtml(` +
    + +
    + `); win.document.body.removeChild(parentElm); expect(connectedInc).toBe(1); diff --git a/src/mock-doc/test/dataset.spec.ts b/packages/mock-doc/src/_test_/dataset.spec.ts similarity index 94% rename from src/mock-doc/test/dataset.spec.ts rename to packages/mock-doc/src/_test_/dataset.spec.ts index d6e50935567..69fe6420e53 100644 --- a/src/mock-doc/test/dataset.spec.ts +++ b/packages/mock-doc/src/_test_/dataset.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { createDocument } from '../document'; describe('dataset', () => { diff --git a/src/mock-doc/test/doc-style.spec.ts b/packages/mock-doc/src/_test_/doc-style.spec.ts similarity index 97% rename from src/mock-doc/test/doc-style.spec.ts rename to packages/mock-doc/src/_test_/doc-style.spec.ts index 7cdf1d407ee..ddda9e59ee6 100644 --- a/src/mock-doc/test/doc-style.spec.ts +++ b/packages/mock-doc/src/_test_/doc-style.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockDocument } from '../document'; describe('style', () => { diff --git a/src/mock-doc/test/document-fragment.spec.ts b/packages/mock-doc/src/_test_/document-fragment.spec.ts similarity index 89% rename from src/mock-doc/test/document-fragment.spec.ts rename to packages/mock-doc/src/_test_/document-fragment.spec.ts index 79a44028d22..b34d697566b 100644 --- a/src/mock-doc/test/document-fragment.spec.ts +++ b/packages/mock-doc/src/_test_/document-fragment.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockDocument } from '../document'; import { MockDocumentFragment } from '../document-fragment'; @@ -40,9 +42,11 @@ describe('documentFragment', () => { expect(frag).toEqualHtml(``); expect(doc.body).toEqualHtml(` -
    - - text + +
    + + text + `); }); }); diff --git a/src/mock-doc/test/element.spec.ts b/packages/mock-doc/src/_test_/element.spec.ts similarity index 90% rename from src/mock-doc/test/element.spec.ts rename to packages/mock-doc/src/_test_/element.spec.ts index 53a4c1fb5c0..59adb6cd91c 100644 --- a/src/mock-doc/test/element.spec.ts +++ b/packages/mock-doc/src/_test_/element.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockDocument } from '../document'; import { MockAnchorElement, MockMetaElement, MockSVGElement, MockUListElement } from '../element'; import { MockElement, MockHTMLElement } from '../node'; @@ -32,7 +34,18 @@ describe('element', () => { const insertElm = doc.createElement('i'); insertElm.textContent = 'c'; elm.insertAdjacentElement('beforebegin', insertElm); - expect(doc.body).toEqualHtml(`c
    0
    `); + expect(doc.body).toEqualHtml(` + + + c + +
    + + 0 + +
    + + `); }); it('insertAdjacentElement afterbegin', () => { @@ -42,7 +55,18 @@ describe('element', () => { const insertElm = doc.createElement('i'); insertElm.textContent = 'c'; elm.insertAdjacentElement('afterbegin', insertElm); - expect(doc.body).toEqualHtml(`
    c0
    `); + expect(doc.body).toEqualHtml(` + +
    + + c + + + 0 + +
    + + `); }); it('insertAdjacentElement beforeend', () => { @@ -52,7 +76,18 @@ describe('element', () => { const insertElm = doc.createElement('i'); insertElm.textContent = 'c'; elm.insertAdjacentElement('beforeend', insertElm); - expect(doc.body).toEqualHtml(`
    0c
    `); + expect(doc.body).toEqualHtml(` + +
    + + 0 + + + c + +
    + + `); }); it('insertAdjacentElement afterend', () => { @@ -62,7 +97,18 @@ describe('element', () => { const insertElm = doc.createElement('i'); insertElm.textContent = 'c'; elm.insertAdjacentElement('afterend', insertElm); - expect(doc.body).toEqualHtml(`
    0
    c`); + expect(doc.body).toEqualHtml(` + +
    + + 0 + +
    + + c + + + `); }); it('insertAdjacentText beforebegin', () => { @@ -70,7 +116,16 @@ describe('element', () => { elm.innerHTML = '0'; doc.body.appendChild(elm); elm.insertAdjacentText('beforebegin', 'a'); - expect(doc.body).toEqualHtml(`a
    0
    `); + expect(doc.body).toEqualHtml(` + + a +
    + + 0 + +
    + + `); }); it('insertAdjacentText afterbegin', () => { @@ -78,7 +133,16 @@ describe('element', () => { elm.innerHTML = '0'; doc.body.appendChild(elm); elm.insertAdjacentText('afterbegin', 'a'); - expect(doc.body).toEqualHtml(`
    a0
    `); + expect(doc.body).toEqualHtml(` + +
    + a + + 0 + +
    + + `); }); it('insertAdjacentText beforeend', () => { @@ -86,7 +150,16 @@ describe('element', () => { elm.innerHTML = '0'; doc.body.appendChild(elm); elm.insertAdjacentText('beforeend', 'a'); - expect(doc.body).toEqualHtml(`
    0a
    `); + expect(doc.body).toEqualHtml(` + +
    + + 0 + + a +
    + + `); }); it('insertAdjacentText afterend', () => { @@ -94,7 +167,16 @@ describe('element', () => { elm.innerHTML = '0'; doc.body.appendChild(elm); elm.insertAdjacentText('afterend', 'a'); - expect(doc.body).toEqualHtml(`
    0
    a`); + expect(doc.body).toEqualHtml(` + +
    + + 0 + +
    + a + + `); }); it('insertAdjacentHTML beforebegin', () => { @@ -102,7 +184,17 @@ describe('element', () => { elm.textContent = '0'; doc.body.appendChild(elm); elm.insertAdjacentHTML('beforebegin', '88mph'); - expect(doc.body).toEqualHtml(`88mph
    0
    `); + expect(doc.body).toEqualHtml(` + + + 88 + + mph +
    + 0 +
    + + `); }); it('insertAdjacentHTML afterbegin', () => { @@ -110,7 +202,20 @@ describe('element', () => { elm.textContent = '0'; doc.body.appendChild(elm); elm.insertAdjacentHTML('afterbegin', '88mph!'); - expect(doc.body).toEqualHtml(`
    88mph!0
    `); + expect(doc.body).toEqualHtml(` + +
    + + 88 + + mph + + ! + + 0 +
    + + `); }); it('insertAdjacentHTML beforeend', () => { @@ -118,7 +223,20 @@ describe('element', () => { elm.textContent = '0'; doc.body.appendChild(elm); elm.insertAdjacentHTML('beforeend', '88mph!'); - expect(doc.body).toEqualHtml(`
    088mph!
    `); + expect(doc.body).toEqualHtml(` + +
    + 0 + + 88 + + mph + + ! + +
    + + `); }); it('insertAdjacentHTML afterend', () => { @@ -126,7 +244,16 @@ describe('element', () => { elm.innerHTML = '0'; doc.body.appendChild(elm); elm.insertAdjacentHTML('afterend', 'a'); - expect(doc.body).toEqualHtml(`
    0
    a`); + expect(doc.body).toEqualHtml(` + +
    + + 0 + +
    + a + + `); }); it('clone elements', () => { @@ -155,11 +282,19 @@ describe('element', () => { clonedWin.document.title = 'Hello Title!'; const titleElm = clonedWin.document.head.querySelector('title'); - expect(titleElm).toEqualHtml(`Hello Title!`); + expect(titleElm).toEqualHtml(` + + Hello Title! + + `); // we just asserted that this object isn't falsy, allowing us to use the bang operator here titleElm!.text = 'Hello Text!'; - expect(titleElm).toEqualHtml(`Hello Text!`); + expect(titleElm).toEqualHtml(` + + Hello Text! + + `); }); it('meta content', () => { @@ -455,7 +590,10 @@ describe('element', () => { expect(foreignObject.tagName).toEqual('FOREIGNOBJECT'); expect(foreignObject.nodeName).toEqual('FOREIGNOBJECT'); - const foreignObject2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'foreignObject'); + const foreignObject2 = document.createElementNS( + 'http://www.w3.org/1999/xhtml', + 'foreignObject', + ); expect(foreignObject2.tagName).toEqual('FOREIGNOBJECT'); expect(foreignObject2.nodeName).toEqual('FOREIGNOBJECT'); @@ -575,7 +713,9 @@ describe('element', () => { elm.innerHTML = ''; expect(slot.assignedNodes().length).toEqual(0); expect(slot.assignedNodes({ flatten: true }).length).toEqual(1); - expect(slot.assignedNodes({ flatten: true })[0].textContent.trim()).toEqual('Fallback content'); + expect(slot.assignedNodes({ flatten: true })[0].textContent.trim()).toEqual( + 'Fallback content', + ); }); it('returns correct elements with `assignedElements`', () => { @@ -611,7 +751,9 @@ describe('element', () => { elm.innerHTML = ''; expect(slot.assignedElements().length).toEqual(0); expect(slot.assignedElements({ flatten: true }).length).toEqual(1); - expect(slot.assignedElements({ flatten: true })[0].textContent.trim()).toEqual('Fallback content'); + expect(slot.assignedElements({ flatten: true })[0].textContent.trim()).toEqual( + 'Fallback content', + ); }); }); diff --git a/src/mock-doc/test/event.spec.ts b/packages/mock-doc/src/_test_/event.spec.ts similarity index 99% rename from src/mock-doc/test/event.spec.ts rename to packages/mock-doc/src/_test_/event.spec.ts index 5505e94d020..778c74fc161 100644 --- a/src/mock-doc/test/event.spec.ts +++ b/packages/mock-doc/src/_test_/event.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockDocument } from '../document'; import { MockEvent } from '../event'; import { MockElement } from '../node'; diff --git a/src/mock-doc/test/global.spec.ts b/packages/mock-doc/src/_test_/global.spec.ts similarity index 94% rename from src/mock-doc/test/global.spec.ts rename to packages/mock-doc/src/_test_/global.spec.ts index fa15c95ac93..e7d384e70d8 100644 --- a/src/mock-doc/test/global.spec.ts +++ b/packages/mock-doc/src/_test_/global.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect } from '@stencil/vitest'; + describe('global', () => { it('HTMLElement', () => { expect(HTMLElement).toBeDefined(); diff --git a/src/mock-doc/test/headers.spec.ts b/packages/mock-doc/src/_test_/headers.spec.ts similarity index 98% rename from src/mock-doc/test/headers.spec.ts rename to packages/mock-doc/src/_test_/headers.spec.ts index baba8e416e8..778e07ec8d0 100644 --- a/src/mock-doc/test/headers.spec.ts +++ b/packages/mock-doc/src/_test_/headers.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect } from '@stencil/vitest'; + import { MockHeaders } from '../headers'; describe('MockHeaders', () => { diff --git a/src/mock-doc/test/html-parse.spec.ts b/packages/mock-doc/src/_test_/html-parse.spec.ts similarity index 89% rename from src/mock-doc/test/html-parse.spec.ts rename to packages/mock-doc/src/_test_/html-parse.spec.ts index d42a5610efd..22b2a0f657f 100644 --- a/src/mock-doc/test/html-parse.spec.ts +++ b/packages/mock-doc/src/_test_/html-parse.spec.ts @@ -1,7 +1,15 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { NODE_TYPES } from '../constants'; import { createFragment } from '../document'; import { MockDocument } from '../document'; -import { MockDOMMatrix, MockDOMPoint, MockSVGRect, MockSVGSVGElement, MockSVGTextContentElement } from '../element'; +import { + MockDOMMatrix, + MockDOMPoint, + MockSVGRect, + MockSVGSVGElement, + MockSVGTextContentElement, +} from '../element'; import { parseHtmlToDocument, parseHtmlToFragment } from '../parse-html'; describe('parseHtml', () => { @@ -57,26 +65,36 @@ describe('parseHtml', () => { expect(doc.body?.firstElementChild?.tagName).toEqual('DIV'); expect(doc.body?.firstElementChild?.firstElementChild?.tagName).toEqual('svg'); expect(doc.body?.firstElementChild?.firstElementChild?.children?.[0]?.tagName).toEqual('a'); - expect(doc.body?.firstElementChild?.firstElementChild?.children?.[1]?.tagName).toEqual('feImage'); - expect(doc.body?.firstElementChild?.firstElementChild?.children?.[2]?.tagName).toEqual('foreignObject'); - expect(doc.body?.firstElementChild?.firstElementChild?.children?.[2].children?.[0]?.tagName).toEqual('A'); - expect(doc.body?.firstElementChild?.firstElementChild?.children?.[2]?.children?.[1]?.tagName).toEqual('FEIMAGE'); + expect(doc.body?.firstElementChild?.firstElementChild?.children?.[1]?.tagName).toEqual( + 'feImage', + ); + expect(doc.body?.firstElementChild?.firstElementChild?.children?.[2]?.tagName).toEqual( + 'foreignObject', + ); + expect( + doc.body?.firstElementChild?.firstElementChild?.children?.[2].children?.[0]?.tagName, + ).toEqual('A'); + expect( + doc.body?.firstElementChild?.firstElementChild?.children?.[2]?.children?.[1]?.tagName, + ).toEqual('FEIMAGE'); expect(doc.body).toEqualHtml(` -
    - - - Hello - - - - - Hello - - - - - -
    + +
    + + + Hello + + + + + Hello + + + + + +
    + `); }); @@ -96,7 +114,8 @@ describe('parseHtml', () => { `); - const svgElem: MockSVGSVGElement = doc.body.firstElementChild?.firstElementChild as MockSVGSVGElement; + const svgElem: MockSVGSVGElement = doc.body.firstElementChild + ?.firstElementChild as MockSVGSVGElement; expect(svgElem).toBeDefined(); expect(svgElem.getBBox()).toEqual(new MockSVGRect()); expect(svgElem.createSVGPoint()).toEqual(new MockDOMPoint()); @@ -113,7 +132,8 @@ describe('parseHtml', () => { `); - const text: MockSVGTextContentElement = doc.body.firstElementChild?.firstElementChild as MockSVGTextContentElement; + const text: MockSVGTextContentElement = doc.body.firstElementChild + ?.firstElementChild as MockSVGTextContentElement; expect(text).toBeDefined(); expect(text.tagName).toEqual('text'); @@ -288,7 +308,9 @@ describe('parseHtml', () => { }); it('should respect case in svg', () => { - const elm = parseHtmlToFragment(''); + const elm = parseHtmlToFragment( + '', + ); expect(elm.children.length).toBe(1); expect(elm.children[0].attributes.item(0).name).toBe('viewBox'); expect(elm.children[0].children[0].attributes.item(0).name).toBe('viewBox'); diff --git a/src/mock-doc/test/location.spec.ts b/packages/mock-doc/src/_test_/location.spec.ts similarity index 89% rename from src/mock-doc/test/location.spec.ts rename to packages/mock-doc/src/_test_/location.spec.ts index 0e016368f85..876d9b3385c 100644 --- a/src/mock-doc/test/location.spec.ts +++ b/packages/mock-doc/src/_test_/location.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockWindow } from '../window'; describe('location.href', () => { @@ -13,7 +15,9 @@ describe('location.href', () => { }); it('window.location.href', () => { - expect(win.location.href).toBe('http://stencil:secret@stenciljs.com:3000/path/to/page?var=var#hash'); + expect(win.location.href).toBe( + 'http://stencil:secret@stenciljs.com:3000/path/to/page?var=var#hash', + ); }); it('window.location.protocol', () => { expect(win.location.protocol).toBe('http:'); @@ -60,7 +64,9 @@ describe('location', () => { }); it('window.location.href', () => { - expect(win.location.href).toBe('http://stencil:secret@stenciljs.com:3000/path/to/page?var=var#hash'); + expect(win.location.href).toBe( + 'http://stencil:secret@stenciljs.com:3000/path/to/page?var=var#hash', + ); }); it('window.location.protocol', () => { expect(win.location.protocol).toBe('http:'); diff --git a/src/mock-doc/test/match-media.spec.ts b/packages/mock-doc/src/_test_/match-media.spec.ts similarity index 94% rename from src/mock-doc/test/match-media.spec.ts rename to packages/mock-doc/src/_test_/match-media.spec.ts index b6430ab690c..ba5ff06014d 100644 --- a/src/mock-doc/test/match-media.spec.ts +++ b/packages/mock-doc/src/_test_/match-media.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockWindow } from '../window'; describe('matchMedia', () => { diff --git a/src/mock-doc/test/request-response.spec.ts b/packages/mock-doc/src/_test_/request-response.spec.ts similarity index 98% rename from src/mock-doc/test/request-response.spec.ts rename to packages/mock-doc/src/_test_/request-response.spec.ts index 8ef9efcb783..dae6bdf2985 100644 --- a/src/mock-doc/test/request-response.spec.ts +++ b/packages/mock-doc/src/_test_/request-response.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect } from '@stencil/vitest'; + import { MockHeaders } from '../headers'; import { MockRequest, MockResponse } from '../request-response'; diff --git a/packages/mock-doc/src/_test_/selector.spec.ts b/packages/mock-doc/src/_test_/selector.spec.ts new file mode 100644 index 00000000000..c5eb021f4f2 --- /dev/null +++ b/packages/mock-doc/src/_test_/selector.spec.ts @@ -0,0 +1,314 @@ +import { describe, it, expect } from '@stencil/vitest'; + +import { MockDocument } from '../document'; + +describe('selector', () => { + it('closest', () => { + const doc = new MockDocument(` +
    + +

    +
    +
    + `); + + const p = doc.querySelector('p'); + const div = doc.querySelector('div'); + expect(p?.closest('div')).toBe(div); + }); + + it('no closest', () => { + const doc = new MockDocument(` +
    + +

    +
    +
    + `); + + const p = doc.querySelector('p'); + expect(p?.closest('div#my-id')).toBe(null); + }); + + it('matches, tag/class/id', () => { + const doc = new MockDocument(); + const elm = doc.createElement('h1'); + elm.classList.add('my-class'); + elm.id = 'my-id'; + expect(elm.matches('h1.my-class#my-id')).toBe(true); + }); + + it('no matches, tag/class/id', () => { + const doc = new MockDocument(); + const elm = doc.createElement('h1'); + expect(elm.matches('h1.my-class#my-id')).toBe(false); + }); + + it('matches, tag', () => { + const doc = new MockDocument(); + const elm = doc.createElement('h1'); + expect(elm.matches('h1')).toBe(true); + }); + + it('no matches, tag', () => { + const doc = new MockDocument(); + const elm = doc.createElement('h1'); + expect(elm.matches('div')).toBe(false); + }); + + it('not find input.checked.a.b', () => { + const doc = new MockDocument(` + + `); + + const checkbox = doc.querySelector('input.checked.a.b'); + expect(checkbox).toBe(null); + }); + + it('find input.checked', () => { + const doc = new MockDocument(` + + `); + + const checkbox = doc.querySelector('input.checked'); + expect(checkbox?.id).toBe('checkbox'); + }); + + it('find input[checked=true][disabled]', () => { + const doc = new MockDocument(` + + `); + + const checkbox = doc.querySelector('input[checked=true][disabled]'); + expect(checkbox?.id).toBe('checkbox'); + }); + + it('find input[checked=true]', () => { + const doc = new MockDocument(` + + `); + + const checkbox = doc.querySelector('input[checked=true]'); + expect(checkbox?.id).toBe('checkbox'); + }); + + it('find input[checked]', () => { + const doc = new MockDocument(` + + `); + + const checkbox = doc.querySelector('input[checked]'); + expect(checkbox?.id).toBe('checkbox'); + }); + + it('find all tag names', () => { + const doc = new MockDocument(` +
    1
    + + `); + + const elms = doc.querySelectorAll('a,div,nav'); + expect(elms.length).toBe(2); + }); + + it('find first tag name', () => { + const doc = new MockDocument(` +
    1
    + + `); + + const div = doc.querySelector('a,div,nav'); + expect(div?.outerHTML).toBe('
    1
    '); + }); + + it('find one tag name', () => { + const doc = new MockDocument(` +
    1
    + + `); + + const div = doc.querySelector('div'); + expect(div?.outerHTML).toBe('
    1
    '); + + const nav = doc.querySelector('nav'); + expect(nav?.outerHTML).toBe(''); + }); + + it('finds child', () => { + const doc = new MockDocument(` +
    + +
    + `); + + const span = doc.querySelector('div > span'); + expect(span?.outerHTML).toBe(''); + }); + + it('finds child if multiple children', () => { + const doc = new MockDocument(` +
    + + +
    + `); + + const span = doc.querySelector('div > span'); + expect(span?.outerHTML).toBe(''); + }); + + it('finds child if multiple selectors', () => { + const doc = new MockDocument(` +
    + + +
    +
    +
    +
    + `); + + const span = doc.querySelector('div > span > .inner'); + expect(span?.outerHTML).toBe('
    '); + }); + + it('not find child if does not exist', () => { + const doc = new MockDocument(` +
    + + +
    +
    +
    +
    + `); + + const span = doc.querySelector('div > span > .none'); + expect(span).toBeFalsy(); + }); + + it(':not()', () => { + const doc = new MockDocument(` + + + + `); + const q1 = doc.querySelector('a:not([nope]) b'); + expect(q1).toBe(null); + }); + + it('descendent, two deep', () => { + const doc = new MockDocument(); + const div = doc.createElement('div'); + const span = doc.createElement('span'); + span.classList.add('c'); + const a = doc.createElement('a'); + const b = doc.createElement('b'); + div.appendChild(span); + span.appendChild(a); + a.appendChild(b); + + const q1 = div.querySelector('span b'); + expect(q1.tagName).toBe('B'); + + const q2 = div.querySelector('span.c b'); + expect(q2.tagName).toBe('B'); + }); + + it('descendent, one deep', () => { + const doc = new MockDocument(); + const div = doc.createElement('div'); + const span = doc.createElement('span'); + span.classList.add('c'); + const a = doc.createElement('a'); + div.appendChild(span); + span.appendChild(a); + + const q1 = div.querySelector('span a'); + expect(q1.tagName).toBe('A'); + + const q2 = div.querySelector('span.c a'); + expect(q2.tagName).toBe('A'); + }); + + // Tests for selectors that previously didn't work with jQuery + describe('modern CSS selectors', () => { + it(':scope selector', () => { + const doc = new MockDocument(` +
    + direct +
    + nested +
    +
    + `); + + const parent = doc.querySelector('#parent'); + // :scope > span should only match direct children + const directChildren = parent?.querySelectorAll(':scope > span'); + expect(directChildren?.length).toBe(1); + expect(directChildren?.[0].textContent).toBe('direct'); + }); + + it(':is() selector', () => { + const doc = new MockDocument(` +
    +

    heading 1

    +

    heading 2

    +

    paragraph

    +
    + `); + + const headings = doc.querySelectorAll(':is(h1, h2)'); + expect(headings?.length).toBe(2); + }); + + it(':where() selector', () => { + const doc = new MockDocument(` +
    +

    intro

    +

    normal

    +
    +
    +

    section intro

    +
    + `); + + // :where() has zero specificity but should still match + const intros = doc.querySelectorAll(':where(article, section) .intro'); + expect(intros.length).toBe(2); + }); + + it(':is() with complex selectors', () => { + const doc = new MockDocument(` +
    + +
    + +
    + +
    + `); + + const buttons = doc.querySelectorAll(':is(.card, .modal) button'); + expect(buttons.length).toBe(2); + }); + + it('combines :scope with other selectors', () => { + const doc = new MockDocument(` +
      +
    • 1
    • +
    • 2
    • +
    • 3
    • +
    + `); + + const list = doc.querySelector('#list'); + const activeItems = list?.querySelectorAll(':scope > .item.active'); + expect(activeItems?.length).toBe(1); + expect(activeItems?.[0].textContent).toBe('2'); + }); + }); +}); diff --git a/src/mock-doc/test/serialize-node.spec.ts b/packages/mock-doc/src/_test_/serialize-node.spec.ts similarity index 90% rename from src/mock-doc/test/serialize-node.spec.ts rename to packages/mock-doc/src/_test_/serialize-node.spec.ts index ddd38ed303d..26665bb31f3 100644 --- a/src/mock-doc/test/serialize-node.spec.ts +++ b/packages/mock-doc/src/_test_/serialize-node.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockDocument } from '../document'; import { EMPTY_ELEMENTS, serializeNodeToHtml } from '../serialize-node'; @@ -13,7 +15,9 @@ describe('serializeNodeToHtml', () => { elm.innerHTML = `
    \n\n\n \t\t \n \tvar 88
    `; const html = serializeNodeToHtml(elm); - expect(html).toBe(`
    \tvar 88
    `); + expect(html).toBe( + `
    \tvar 88
    `, + ); }); it('remove most whitespace in text nodes, but not all of it when not in pretty print', () => { @@ -22,7 +26,9 @@ describe('serializeNodeToHtml', () => { elm.innerHTML = `
    var \n \t \n\n\n\t value\n\n\n\n\n \t = 88 ;
    `; const html = serializeNodeToHtml(elm); - expect(html).toBe(`
    var value = 88 ;
    `); + expect(html).toBe( + `
    var value = 88 ;
    `, + ); }); it('do not add extra indentation when pretty print
    ', () => {
    @@ -31,7 +37,9 @@ describe('serializeNodeToHtml', () => {
         elm.innerHTML = `
    88
    `; const html = serializeNodeToHtml(elm, { prettyHtml: true }); - expect(html).toBe(`
    \n
    88
    \n
    \n
    `); + expect(html).toBe( + `
    \n
    88
    \n
    \n
    `, + ); }); it('do not pretty print
    ', () => {
    @@ -49,7 +57,9 @@ describe('serializeNodeToHtml', () => {
         elm.innerHTML = `
    install cordova-plugin-purchase\nnpx cap update
    `; const html = serializeNodeToHtml(elm, { prettyHtml: true }); - expect(html).toBe(`
    install cordova-plugin-purchase\nnpx cap update
    `); + expect(html).toBe( + `
    install cordova-plugin-purchase\nnpx cap update
    `, + ); }); it('do not pretty print
     w/ html comments', () => {
    @@ -99,7 +109,9 @@ describe('serializeNodeToHtml', () => {
           

    - Hello + + Hello +

    @@ -110,7 +122,9 @@ describe('serializeNodeToHtml', () => {

    - Hello + + Hello +

    @@ -144,11 +158,17 @@ describe('serializeNodeToHtml', () => { expect(elm).toEqualHtml(` -
    shadow top
    +
    + shadow top +
    -
    shadow bottom
    +
    + shadow bottom +
    -
    light dom
    +
    + light dom +
    `); }); @@ -179,7 +199,9 @@ describe('serializeNodeToHtml', () => { expect(elm).toEqualHtml(` -
    test content
    +
    + test content +
    `); @@ -238,7 +260,9 @@ describe('serializeNodeToHtml', () => { elm.setAttribute('title', ''); const html = serializeNodeToHtml(elm, { outerHtml: true, removeEmptyAttributes: false }); - expect(html).toBe(``); + expect(html).toBe( + ``, + ); }); it('remove empty attrs', () => { diff --git a/src/mock-doc/test/shadow-dom-event-bubbling.spec.ts b/packages/mock-doc/src/_test_/shadow-dom-event-bubbling.spec.ts similarity index 97% rename from src/mock-doc/test/shadow-dom-event-bubbling.spec.ts rename to packages/mock-doc/src/_test_/shadow-dom-event-bubbling.spec.ts index 434466f3913..96201fa035f 100644 --- a/src/mock-doc/test/shadow-dom-event-bubbling.spec.ts +++ b/packages/mock-doc/src/_test_/shadow-dom-event-bubbling.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockDocument } from '../document'; import { MockWindow } from '../window'; diff --git a/src/mock-doc/test/storage.spec.ts b/packages/mock-doc/src/_test_/storage.spec.ts similarity index 94% rename from src/mock-doc/test/storage.spec.ts rename to packages/mock-doc/src/_test_/storage.spec.ts index e24ec4cc06b..8f0df27823f 100644 --- a/src/mock-doc/test/storage.spec.ts +++ b/packages/mock-doc/src/_test_/storage.spec.ts @@ -1,3 +1,5 @@ +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockWindow } from '../window'; describe('storage', () => { @@ -9,9 +11,11 @@ describe('storage', () => { it('localStorage should return proper values', () => { expect(win.localStorage.getItem('key')).toEqual(null); + // @ts-ignore win.localStorage.setItem('key', null); expect(win.localStorage.getItem('key')).toEqual('null'); + // @ts-ignore win.localStorage.setItem('key', undefined); expect(win.localStorage.getItem('key')).toEqual('null'); diff --git a/src/mock-doc/test/token-list.spec.ts b/packages/mock-doc/src/_test_/token-list.spec.ts similarity index 91% rename from src/mock-doc/test/token-list.spec.ts rename to packages/mock-doc/src/_test_/token-list.spec.ts index 6d9e4182980..4e7348c946f 100644 --- a/src/mock-doc/test/token-list.spec.ts +++ b/packages/mock-doc/src/_test_/token-list.spec.ts @@ -1,6 +1,8 @@ -import { MockTokenList } from '../token-list'; +import { describe, it, expect, beforeEach } from '@stencil/vitest'; + import { MockDocument } from '../document'; import { MockElement } from '../node'; +import { MockTokenList } from '../token-list'; describe('token-list', () => { let tokenList: MockTokenList; @@ -13,7 +15,9 @@ describe('token-list', () => { it('add and remove tokens', () => { tokenList.add('one'); tokenList.add('two', 'three'); + // @ts-ignore tokenList.add(null); + // @ts-ignore tokenList.add(undefined); tokenList.add(1 as any, 2 as any); expect(tokenList.toString()).toEqual('one two three null undefined 1 2'); @@ -22,6 +26,7 @@ describe('token-list', () => { expect(tokenList.contains('two')).toBe(true); expect(tokenList.contains('three')).toBe(true); expect(tokenList.contains('null')).toBe(true); + // @ts-ignore expect(tokenList.contains(null)).toBe(true); expect(tokenList.contains('undefined')).toBe(true); expect(tokenList.contains('1')).toBe(true); @@ -29,7 +34,9 @@ describe('token-list', () => { tokenList.remove('one'); tokenList.remove('two', 'three'); + // @ts-ignore tokenList.remove(null); + // @ts-ignore tokenList.remove(undefined); tokenList.remove(1 as any, 2 as any); diff --git a/packages/mock-doc/src/attribute.ts b/packages/mock-doc/src/attribute.ts new file mode 100644 index 00000000000..257aeca5c0f --- /dev/null +++ b/packages/mock-doc/src/attribute.ts @@ -0,0 +1,232 @@ +import { XLINK_NS } from './constants'; + +const attrHandler = { + get(obj: any, prop: string) { + if (prop in obj) { + return obj[prop]; + } + if (typeof prop !== 'symbol' && !isNaN(prop as any)) { + return (obj as MockAttributeMap).__items[prop as any]; + } + return undefined; + }, +}; + +export const createAttributeProxy = (caseInsensitive: boolean) => + new Proxy(new MockAttributeMap(caseInsensitive), attrHandler); + +export class MockAttributeMap { + __items: MockAttr[] = []; + + constructor(public caseInsensitive = false) {} + + get length() { + return this.__items.length; + } + + item(index: number) { + return this.__items[index] || null; + } + + setNamedItem(attr: MockAttr) { + attr.namespaceURI = null; + this.setNamedItemNS(attr); + } + + setNamedItemNS(attr: MockAttr) { + if (attr != null && attr.value != null) { + attr.value = String(attr.value); + } + + const existingAttr = this.__items.find( + (a) => a.localName === attr.localName && a.namespaceURI === attr.namespaceURI, + ); + if (existingAttr != null) { + existingAttr.value = attr.value; + } else { + this.__items.push(attr); + } + } + + getNamedItem(attrName: string) { + if (this.caseInsensitive) { + attrName = attrName.toLowerCase(); + } + return this.getNamedItemNS(null, attrName); + } + + getNamedItemNS(namespaceURI: string | null, attrName: string) { + namespaceURI = getNamespaceURI(namespaceURI); + return ( + this.__items.find( + (attr) => + attr.localName === attrName && getNamespaceURI(attr.namespaceURI) === namespaceURI, + ) || null + ); + } + + removeNamedItem(attr: MockAttr) { + this.removeNamedItemNS(attr); + } + + removeNamedItemNS(attr: MockAttr) { + for (let i = 0, ii = this.__items.length; i < ii; i++) { + if ( + this.__items[i].localName === attr.localName && + this.__items[i].namespaceURI === attr.namespaceURI + ) { + this.__items.splice(i, 1); + break; + } + } + } + + [Symbol.iterator]() { + let i = 0; + + return { + next: () => ({ + done: i === this.length, + value: this.item(i++), + }), + }; + } + + get [Symbol.toStringTag]() { + return 'MockAttributeMap'; + } +} + +function getNamespaceURI(namespaceURI: string | null) { + return namespaceURI === XLINK_NS ? null : namespaceURI; +} + +export function cloneAttributes(srcAttrs: MockAttributeMap, sortByName = false) { + // Use createAttributeProxy to ensure numeric indexing works (e.g., attrs[0]) + const dstAttrs = createAttributeProxy(srcAttrs.caseInsensitive); + if (srcAttrs != null) { + const attrLen = srcAttrs.length; + + if (sortByName && attrLen > 1) { + const sortedAttrs: MockAttr[] = []; + for (let i = 0; i < attrLen; i++) { + const srcAttr = srcAttrs.item(i); + const dstAttr = new MockAttr( + srcAttr.localName, + srcAttr.value, + srcAttr.namespaceURI, + srcAttr.prefix, + ); + sortedAttrs.push(dstAttr); + } + + sortedAttrs.sort(sortAttributes).forEach((attr) => { + dstAttrs.setNamedItemNS(attr); + }); + } else { + for (let i = 0; i < attrLen; i++) { + const srcAttr = srcAttrs.item(i); + const dstAttr = new MockAttr( + srcAttr.localName, + srcAttr.value, + srcAttr.namespaceURI, + srcAttr.prefix, + ); + dstAttrs.setNamedItemNS(dstAttr); + } + } + } + return dstAttrs; +} + +function sortAttributes(a: MockAttr, b: MockAttr) { + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; +} + +export class MockAttr { + private _localName: string; + private _prefix: string | null; + private _value: string; + private _namespaceURI: string | null; + + constructor( + attrName: string, + attrValue: string, + namespaceURI: string | null = null, + prefix: string | null = null, + ) { + // If prefix provided, use it directly with localName = attrName + // Otherwise, parse prefix from attrName if it contains ':' + if (prefix != null) { + this._prefix = prefix; + this._localName = attrName; + } else if (attrName.includes(':')) { + const [parsedPrefix, ...rest] = attrName.split(':'); + this._prefix = parsedPrefix; + this._localName = rest.join(':'); + } else { + this._prefix = null; + this._localName = attrName; + } + this._value = String(attrValue); + this._namespaceURI = namespaceURI; + } + + get name() { + return this._prefix != null ? `${this._prefix}:${this._localName}` : this._localName; + } + set name(value) { + if (value.includes(':')) { + const [prefix, ...rest] = value.split(':'); + this._prefix = prefix; + this._localName = rest.join(':'); + } else { + this._prefix = null; + this._localName = value; + } + } + + get localName() { + return this._localName; + } + set localName(value) { + this._localName = value; + } + + get prefix() { + return this._prefix; + } + set prefix(value) { + this._prefix = value; + } + + get value() { + return this._value; + } + set value(value) { + this._value = String(value); + } + + get nodeName() { + return this.name; + } + set nodeName(value) { + this.name = value; + } + + get nodeValue() { + return this._value; + } + set nodeValue(value) { + this._value = String(value); + } + + get namespaceURI() { + return this._namespaceURI; + } + set namespaceURI(namespaceURI) { + this._namespaceURI = namespaceURI; + } +} diff --git a/src/mock-doc/comment-node.ts b/packages/mock-doc/src/comment-node.ts similarity index 100% rename from src/mock-doc/comment-node.ts rename to packages/mock-doc/src/comment-node.ts diff --git a/src/mock-doc/console.ts b/packages/mock-doc/src/console.ts similarity index 100% rename from src/mock-doc/console.ts rename to packages/mock-doc/src/console.ts diff --git a/packages/mock-doc/src/constants.ts b/packages/mock-doc/src/constants.ts new file mode 100644 index 00000000000..c9b0f76f27f --- /dev/null +++ b/packages/mock-doc/src/constants.ts @@ -0,0 +1,41 @@ +// Hydration marker IDs (used by serialize-node.ts) +export const CONTENT_REF_ID = 'r'; +export const ORG_LOCATION_ID = 'o'; +export const SLOT_NODE_ID = 's'; +export const TEXT_NODE_ID = 't'; +export const HYDRATE_ID = 's-id'; + +// Standard XML namespaces +export const XLINK_NS = 'http://www.w3.org/1999/xlink'; +const XML_NS = 'http://www.w3.org/XML/1998/namespace'; +const XMLNS_NS = 'http://www.w3.org/2000/xmlns/'; + +/** + * Get the standard prefix for a namespace URI. + * Returns null if namespace has no standard prefix. + * + * @param namespaceURI - the namespace URI to look up + * @returns the standard prefix or null if not found + */ +export function getPrefixForNamespace(namespaceURI: string | null): string | null { + if (namespaceURI === XLINK_NS) return 'xlink'; + if (namespaceURI === XML_NS) return 'xml'; + if (namespaceURI === XMLNS_NS) return 'xmlns'; + return null; +} + +export const enum NODE_TYPES { + ELEMENT_NODE = 1, + TEXT_NODE = 3, + COMMENT_NODE = 8, + DOCUMENT_NODE = 9, + DOCUMENT_TYPE_NODE = 10, + DOCUMENT_FRAGMENT_NODE = 11, +} + +export const enum NODE_NAMES { + COMMENT_NODE = '#comment', + DOCUMENT_NODE = '#document', + DOCUMENT_FRAGMENT_NODE = '#document-fragment', + TEXT_NODE = '#text', +} diff --git a/src/mock-doc/css-style-declaration.ts b/packages/mock-doc/src/css-style-declaration.ts similarity index 100% rename from src/mock-doc/css-style-declaration.ts rename to packages/mock-doc/src/css-style-declaration.ts diff --git a/src/mock-doc/css-style-sheet.ts b/packages/mock-doc/src/css-style-sheet.ts similarity index 98% rename from src/mock-doc/css-style-sheet.ts rename to packages/mock-doc/src/css-style-sheet.ts index 9c74ae60ce1..67a4f4b6a23 100644 --- a/src/mock-doc/css-style-sheet.ts +++ b/packages/mock-doc/src/css-style-sheet.ts @@ -9,7 +9,7 @@ class MockCSSRule { export class MockCSSStyleSheet { ownerNode?: MockStyleElement; type = 'text/css'; - parentStyleSheet: MockCSSStyleSheet = null; + parentStyleSheet: MockCSSStyleSheet | null = null; cssRules: MockCSSRule[] = []; constructor(ownerNode?: MockStyleElement) { diff --git a/src/mock-doc/custom-element-registry.ts b/packages/mock-doc/src/custom-element-registry.ts similarity index 84% rename from src/mock-doc/custom-element-registry.ts rename to packages/mock-doc/src/custom-element-registry.ts index ac0c19e1511..fcbc31338f6 100644 --- a/src/mock-doc/custom-element-registry.ts +++ b/packages/mock-doc/src/custom-element-registry.ts @@ -2,8 +2,8 @@ import { NODE_TYPES } from './constants'; import { MockHTMLElement, MockNode } from './node'; export class MockCustomElementRegistry implements CustomElementRegistry { - private __registry: Map; - private __whenDefined: Map; + private __registry!: Map; + private __whenDefined!: Map; constructor(private win: Window) {} @@ -67,19 +67,25 @@ export class MockCustomElementRegistry implements CustomElementRegistry { return undefined; } - getName(cstr: CustomElementConstructor) { - for (const [tagName, def] of this.__registry.entries()) { - if (def.cstr === cstr) { - return tagName; + getName(cstr: CustomElementConstructor): string | null { + if (this.__registry != null) { + for (const [tagName, def] of this.__registry.entries()) { + if (def.cstr === cstr) { + return tagName; + } } } - return undefined; + return null; } upgrade(_rootNode: any) { // } + initialize(_root: ShadowRoot) { + // + } + clear() { if (this.__registry != null) { this.__registry.clear(); @@ -94,7 +100,7 @@ export class MockCustomElementRegistry implements CustomElementRegistry { tagName = tagName.toLowerCase(); if (this.__registry != null && this.__registry.has(tagName) === true) { - return Promise.resolve(this.__registry.get(tagName).cstr); + return Promise.resolve(this.__registry.get(tagName)!.cstr); } return new Promise((resolve) => { @@ -113,7 +119,11 @@ export class MockCustomElementRegistry implements CustomElementRegistry { } } -export function createCustomElement(customElements: MockCustomElementRegistry, ownerDocument: any, tagName: string) { +export function createCustomElement( + customElements: MockCustomElementRegistry, + ownerDocument: any, + tagName: string, +) { const Cstr = customElements.get(tagName); if (Cstr != null) { @@ -172,7 +182,11 @@ export function connectNode(ownerDocument: any, node: MockNode) { if (node.nodeType === NODE_TYPES.ELEMENT_NODE) { if (ownerDocument != null && node.nodeName.includes('-')) { const win = ownerDocument.defaultView as Window; - if (win != null && typeof (node as any).connectedCallback === 'function' && node.isConnected) { + if ( + win != null && + typeof (node as any).connectedCallback === 'function' && + node.isConnected + ) { fireConnectedCallback(node); } @@ -208,7 +222,10 @@ function fireConnectedCallback(node: any) { export function disconnectNode(node: MockNode) { if (node.nodeType === NODE_TYPES.ELEMENT_NODE) { - if (node.nodeName.includes('-') === true && typeof (node as any).disconnectedCallback === 'function') { + if ( + node.nodeName.includes('-') === true && + typeof (node as any).disconnectedCallback === 'function' + ) { if (tempDisableCallbacks.has(node.ownerDocument) === false) { try { (node as any).disconnectedCallback(); @@ -221,7 +238,12 @@ export function disconnectNode(node: MockNode) { } } -export function attributeChanged(node: MockNode, attrName: string, oldValue: string | null, newValue: string | null) { +export function attributeChanged( + node: MockNode, + attrName: string, + oldValue: string | null, + newValue: string | null, +) { attrName = attrName.toLowerCase(); const observedAttributes = (node as any).constructor.observedAttributes as string[]; @@ -238,7 +260,10 @@ export function attributeChanged(node: MockNode, attrName: string, oldValue: str } export function checkAttributeChanged(node: MockNode) { - return node.nodeName.includes('-') === true && typeof (node as any).attributeChangedCallback === 'function'; + return ( + node.nodeName.includes('-') === true && + typeof (node as any).attributeChangedCallback === 'function' + ); } const tempDisableCallbacks = new Set(); diff --git a/src/mock-doc/dataset.ts b/packages/mock-doc/src/dataset.ts similarity index 100% rename from src/mock-doc/dataset.ts rename to packages/mock-doc/src/dataset.ts diff --git a/src/mock-doc/document-fragment.ts b/packages/mock-doc/src/document-fragment.ts similarity index 100% rename from src/mock-doc/document-fragment.ts rename to packages/mock-doc/src/document-fragment.ts diff --git a/src/mock-doc/document-type-node.ts b/packages/mock-doc/src/document-type-node.ts similarity index 100% rename from src/mock-doc/document-type-node.ts rename to packages/mock-doc/src/document-type-node.ts diff --git a/src/mock-doc/document.ts b/packages/mock-doc/src/document.ts similarity index 77% rename from src/mock-doc/document.ts rename to packages/mock-doc/src/document.ts index fdfd999aa02..25bab1009ac 100644 --- a/src/mock-doc/document.ts +++ b/packages/mock-doc/src/document.ts @@ -1,3 +1,5 @@ +import nwsapi from 'nwsapi'; + import { MockAttr } from './attribute'; import { MockComment } from './comment-node'; import { NODE_NAMES, NODE_TYPES } from './constants'; @@ -8,12 +10,35 @@ import { resetEventListeners } from './event'; import { MockElement, MockHTMLElement, MockTextNode, resetElement } from './node'; import { parseHtmlToFragment } from './parse-html'; import { parseDocumentUtil } from './parse-util'; +import { MockTreeWalker } from './tree-walker'; import { MockWindow } from './window'; +/** + * Interface for nwsapi instance methods we use. + */ +interface NwsapiInstance { + configure(config: { LOGERRORS?: boolean; VERBOSITY?: boolean }): void; + match(selector: string, element: unknown): boolean; + first(selector: string, context: unknown): unknown | null; + select(selector: string, context: unknown): unknown[]; + closest(selector: string, element: unknown): unknown | null; +} + export class MockDocument extends MockHTMLElement { defaultView: any; cookie: string; referrer: string; + /** + * Returns 'CSS1Compat' for standards mode (the default). + * Required by nwsapi for quirks mode detection. + */ + readonly compatMode = 'CSS1Compat'; + /** + * Returns the MIME type of the document. + * Required by nwsapi for HTML document detection. + */ + readonly contentType = 'text/html'; + #nwsapi: NwsapiInstance | null = null; constructor(html: string | boolean | null = null, win: any = null) { super(null, null); @@ -42,6 +67,37 @@ export class MockDocument extends MockHTMLElement { } } + /** + * Get the nwsapi instance for this document. + * Lazily creates one if it doesn't exist. + * Creates a window if the document doesn't have one. + * @returns the nwsapi instance + */ + _getDOMSelector(): NwsapiInstance { + if (!this.#nwsapi) { + // Ensure we have a window for nwsapi + if (!this.defaultView) { + const win = new MockWindow(false); + (win as { document: unknown }).document = this; + this.defaultView = win; + } + // nwsapi expects a global-like object with document property + this.#nwsapi = nwsapi({ document: this }); + this.#nwsapi.configure({ + LOGERRORS: false, + VERBOSITY: false, + }); + } + return this.#nwsapi; + } + + /** + * Clear the nwsapi cache. Call this when the document structure changes significantly. + */ + _clearDOMSelector(): void { + this.#nwsapi = null; + } + override get dir() { return this.documentElement.dir; } @@ -66,7 +122,9 @@ export class MockDocument extends MockHTMLElement { } get baseURI() { - const baseNode = this.head.childNodes.find((node) => node.nodeName === 'BASE') as MockBaseElement; + const baseNode = this.head.childNodes.find( + (node) => node.nodeName === 'BASE', + ) as MockBaseElement; if (baseNode) { return baseNode.href; } @@ -217,6 +275,22 @@ export class MockDocument extends MockHTMLElement { return new MockDocumentTypeNode(this); } + /** + * Creates a TreeWalker for traversing the document tree. + * This is a simplified implementation for dom-selector compatibility. + * @param root - the root node for the tree walker + * @param whatToShow - a bitmask specifying which nodes to show + * @param filter - an optional node filter + * @returns a new TreeWalker instance + */ + createTreeWalker( + root: MockElement, + whatToShow = 0xffffffff, + filter: NodeFilter | null = null, + ): TreeWalker { + return new MockTreeWalker(root, whatToShow, filter); + } + getElementById(id: string) { return getElementById(this, id); } @@ -243,12 +317,12 @@ export class MockDocument extends MockHTMLElement { } } -export function createDocument(html: string | boolean = null): Document { +export function createDocument(html: string | boolean | null = null): Document { return new MockWindow(html).document; } export function createFragment(html?: string): DocumentFragment { - return parseHtmlToFragment(html, null); + return parseHtmlToFragment(html ?? '', null); } export function resetDocument(doc: Document) { @@ -274,16 +348,16 @@ export function resetDocument(doc: Document) { try { (doc as any).nodeName = NODE_NAMES.DOCUMENT_NODE; - } catch (e) {} + } catch {} try { (doc as any).nodeType = NODE_TYPES.DOCUMENT_NODE; - } catch (e) {} + } catch {} try { (doc as any).cookie = ''; - } catch (e) {} + } catch {} try { (doc as any).referrer = ''; - } catch (e) {} + } catch {} } } @@ -296,9 +370,11 @@ const DOC_KEY_KEEPERS = new Set([ 'childNodes', '_childNodes', '_shadowRoot', + 'compatMode', + 'contentType', ]); -export function getElementById(elm: MockElement, id: string): MockElement { +export function getElementById(elm: MockElement, id: string): MockElement | null { const children = elm.children; for (let i = 0, ii = children.length; i < ii; i++) { const childElm = children[i]; @@ -325,7 +401,7 @@ function getElementsByName(elm: MockElement, elmName: string, foundElms: MockEle return foundElms; } -export function setOwnerDocument(elm: MockElement, ownerDocument: any) { +function setOwnerDocument(elm: MockElement, ownerDocument: any) { for (let i = 0, ii = elm.childNodes.length; i < ii; i++) { elm.childNodes[i].ownerDocument = ownerDocument; diff --git a/packages/mock-doc/src/element.ts b/packages/mock-doc/src/element.ts new file mode 100644 index 00000000000..72c29233661 --- /dev/null +++ b/packages/mock-doc/src/element.ts @@ -0,0 +1,815 @@ +import { cloneAttributes } from './attribute'; +import { NODE_TYPES } from './constants'; +import { getStyleElementText, MockCSSStyleSheet, setStyleElementText } from './css-style-sheet'; +import { createCustomElement } from './custom-element-registry'; +import { MockDocumentFragment } from './document-fragment'; +import { MockElement, MockHTMLElement, MockNode } from './node'; + +export function createElement(ownerDocument: any, tagName: string): any { + if (typeof tagName !== 'string' || tagName === '' || !/^[a-z0-9-_:]+$/i.test(tagName)) { + throw new Error(`The tag name provided (${tagName}) is not a valid name.`); + } + tagName = tagName.toLowerCase(); + + switch (tagName) { + case 'a': + return new MockAnchorElement(ownerDocument); + + case 'base': + return new MockBaseElement(ownerDocument); + + case 'button': + return new MockButtonElement(ownerDocument); + + case 'canvas': + return new MockCanvasElement(ownerDocument); + + case 'form': + return new MockFormElement(ownerDocument); + + case 'img': + return new MockImageElement(ownerDocument); + + case 'input': + return new MockInputElement(ownerDocument); + + case 'label': + return new MockLabelElement(ownerDocument); + + case 'link': + return new MockLinkElement(ownerDocument); + + case 'meta': + return new MockMetaElement(ownerDocument); + + case 'script': + return new MockScriptElement(ownerDocument); + + case 'slot': + return new MockSlotElement(ownerDocument); + + case 'slot-fb': + return new MockHTMLElement(ownerDocument, tagName); + + case 'style': + return new MockStyleElement(ownerDocument); + + case 'template': + return new MockTemplateElement(ownerDocument); + + case 'title': + return new MockTitleElement(ownerDocument); + + case 'ul': + return new MockUListElement(ownerDocument); + } + + if (ownerDocument != null && tagName.includes('-')) { + const win = ownerDocument.defaultView; + if (win != null && win.customElements != null) { + return createCustomElement(win.customElements, ownerDocument, tagName); + } + } + + return new MockHTMLElement(ownerDocument, tagName); +} + +export function createElementNS(ownerDocument: any, namespaceURI: string, tagName: string) { + if (namespaceURI === 'http://www.w3.org/1999/xhtml') { + return createElement(ownerDocument, tagName); + } else if (namespaceURI === 'http://www.w3.org/2000/svg') { + switch (tagName.toLowerCase()) { + case 'text': + case 'tspan': + case 'tref': + case 'altglyph': + case 'textpath': + return new MockSVGTextContentElement(ownerDocument, tagName); + case 'circle': + case 'ellipse': + case 'image': + case 'line': + case 'path': + case 'polygon': + case 'polyline': + case 'rect': + case 'use': + return new MockSVGGraphicsElement(ownerDocument, tagName); + case 'svg': + return new MockSVGSVGElement(ownerDocument, tagName); + default: + return new MockSVGElement(ownerDocument, tagName); + } + } else { + return new MockElement(ownerDocument, tagName, namespaceURI); + } +} + +export class MockAnchorElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'a'); + } + + get href() { + return fullUrl(this, 'href'); + } + set href(value: string) { + this.setAttribute('href', value); + } + get pathname() { + if (!this.href) { + return ''; + } + return new URL(this.href).pathname; + } +} + +export class MockButtonElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'button'); + } + + get labels() { + return getLabelsForElement(this); + } +} +patchPropAttributes( + MockButtonElement.prototype, + { + type: String, + }, + { + type: 'submit', + }, +); + +Object.defineProperty(MockButtonElement.prototype, 'form', { + get(this: MockElement) { + return this.hasAttribute('form') ? this.getAttribute('form') : null; + }, +}); + +export class MockImageElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'img'); + } + + override get draggable() { + return this.getAttributeNS(null, 'draggable') !== 'false'; + } + override set draggable(value: boolean) { + this.setAttributeNS(null, 'draggable', value); + } + + get src() { + return fullUrl(this, 'src'); + } + set src(value: string) { + this.setAttribute('src', value); + } +} +patchPropAttributes(MockImageElement.prototype, { + height: Number, + width: Number, +}); + +export class MockInputElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'input'); + } + + get list() { + const listId = this.getAttribute('list'); + if (listId) { + return (this.ownerDocument as Document).getElementById(listId); + } + return null; + } + + get labels() { + return getLabelsForElement(this); + } +} + +patchPropAttributes( + MockInputElement.prototype, + { + accept: String, + autocomplete: String, + autofocus: Boolean, + capture: String, + checked: Boolean, + disabled: Boolean, + form: String, + formaction: String, + formenctype: String, + formmethod: String, + formnovalidate: String, + formtarget: String, + height: Number, + inputmode: String, + max: String, + maxLength: Number, + min: String, + minLength: Number, + multiple: Boolean, + name: String, + pattern: String, + placeholder: String, + required: Boolean, + readOnly: Boolean, + size: Number, + spellCheck: Boolean, + src: String, + step: String, + type: String, + value: String, + width: Number, + }, + { + type: 'text', + }, +); + +export class MockFormElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'form'); + } +} +patchPropAttributes(MockFormElement.prototype, { + name: String, +}); + +class MockLabelElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'label'); + } + + get htmlFor() { + return this.getAttributeNS(null, 'for') || ''; + } + set htmlFor(value: string) { + this.setAttributeNS(null, 'for', value); + } + + get control(): MockHTMLElement | null { + const forAttr = this.htmlFor; + if (forAttr) { + // Label references an element by ID via the "for" attribute + return this.ownerDocument?.getElementById(forAttr) ?? null; + } + // If no "for" attribute, look for the first labelable descendant + const labelableSelector = + 'button, input:not([type="hidden"]), meter, output, progress, select, textarea'; + return this.querySelector(labelableSelector) as unknown as MockHTMLElement | null; + } +} + +export class MockLinkElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'link'); + } + + get href() { + return fullUrl(this, 'href'); + } + set href(value: string) { + this.setAttribute('href', value); + } +} +patchPropAttributes(MockLinkElement.prototype, { + crossorigin: String, + media: String, + rel: String, + type: String, +}); + +export class MockMetaElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'meta'); + } +} +patchPropAttributes(MockMetaElement.prototype, { + charset: String, + content: String, + name: String, +}); + +export class MockScriptElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'script'); + } + + get src() { + return fullUrl(this, 'src'); + } + set src(value: string) { + this.setAttribute('src', value); + } +} +patchPropAttributes(MockScriptElement.prototype, { + type: String, +}); + +export class MockDOMMatrix { + static fromMatrix() { + return new MockDOMMatrix(); + } + a: number = 1; + b: number = 0; + c: number = 0; + d: number = 1; + e: number = 0; + f: number = 0; + m11: number = 1; + m12: number = 0; + m13: number = 0; + m14: number = 0; + m21: number = 0; + m22: number = 1; + m23: number = 0; + m24: number = 0; + m31: number = 0; + m32: number = 0; + m33: number = 1; + m34: number = 0; + m41: number = 0; + m42: number = 0; + m43: number = 0; + m44: number = 1; + is2D: boolean = true; + isIdentity: boolean = true; + inverse() { + return new MockDOMMatrix(); + } + flipX() { + return new MockDOMMatrix(); + } + flipY() { + return new MockDOMMatrix(); + } + multiply() { + return new MockDOMMatrix(); + } + rotate() { + return new MockDOMMatrix(); + } + rotateAxisAngle() { + return new MockDOMMatrix(); + } + rotateFromVector() { + return new MockDOMMatrix(); + } + scale() { + return new MockDOMMatrix(); + } + scaleNonUniform() { + return new MockDOMMatrix(); + } + skewX() { + return new MockDOMMatrix(); + } + skewY() { + return new MockDOMMatrix(); + } + toJSON() {} + toString() {} + transformPoint() { + return new MockDOMPoint(); + } + translate() { + return new MockDOMMatrix(); + } +} + +export class MockDOMPoint { + w: number = 1; + x: number = 0; + y: number = 0; + z: number = 0; + toJSON() {} + matrixTransform() { + return new MockDOMMatrix(); + } +} + +export class MockSVGRect { + height: number = 10; + width: number = 10; + x: number = 0; + y: number = 0; +} + +export class MockStyleElement extends MockHTMLElement { + sheet: MockCSSStyleSheet; + + constructor(ownerDocument: any) { + super(ownerDocument, 'style'); + this.sheet = new MockCSSStyleSheet(this); + } + + override get innerHTML() { + return getStyleElementText(this); + } + override set innerHTML(value: string) { + setStyleElementText(this, value); + } + + override get innerText() { + return getStyleElementText(this); + } + override set innerText(value: string) { + setStyleElementText(this, value); + } + + override get textContent() { + return getStyleElementText(this); + } + override set textContent(value: string) { + setStyleElementText(this, value); + } +} +export class MockSVGElement extends MockElement { + override __namespaceURI = 'http://www.w3.org/2000/svg'; + + // SVGElement properties and methods + get ownerSVGElement(): SVGSVGElement { + return null; + } + get viewportElement(): SVGElement { + return null; + } + onunload() { + /**/ + } + + // SVGGeometryElement properties and methods + get pathLength(): number { + return 0; + } + + isPointInFill(_pt: DOMPoint): boolean { + return false; + } + isPointInStroke(_pt: DOMPoint): boolean { + return false; + } + getTotalLength(): number { + return 0; + } +} + +class MockSVGGraphicsElement extends MockSVGElement { + getBBox(_options?: { + clipped: boolean; + fill: boolean; + markers: boolean; + stroke: boolean; + }): MockSVGRect { + return new MockSVGRect(); + } + getCTM(): MockDOMMatrix { + return new MockDOMMatrix(); + } + getScreenCTM(): MockDOMMatrix { + return new MockDOMMatrix(); + } +} + +export class MockSVGSVGElement extends MockSVGGraphicsElement { + createSVGPoint(): MockDOMPoint { + return new MockDOMPoint(); + } +} + +export class MockSVGTextContentElement extends MockSVGGraphicsElement { + getComputedTextLength(): number { + return 0; + } +} + +export class MockBaseElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'base'); + } + + get href() { + return fullUrl(this, 'href'); + } + set href(value: string) { + this.setAttribute('href', value); + } +} + +export class MockTemplateElement extends MockHTMLElement { + content: MockDocumentFragment; + + constructor(ownerDocument: any) { + super(ownerDocument, 'template'); + this.content = new MockDocumentFragment(ownerDocument); + } + + override get innerHTML() { + return this.content.innerHTML; + } + override set innerHTML(html: string) { + this.content.innerHTML = html; + } + + override cloneNode(deep?: boolean) { + const cloned = new MockTemplateElement(null); + cloned.attributes = cloneAttributes(this.attributes); + + const styleCssText = this.getAttribute('style'); + if (styleCssText != null && styleCssText.length > 0) { + cloned.setAttribute('style', styleCssText); + } + + cloned.content = this.content.cloneNode(deep); + + if (deep) { + for (let i = 0, ii = this.childNodes.length; i < ii; i++) { + const clonedChildNode = this.childNodes[i].cloneNode(true); + cloned.appendChild(clonedChildNode); + } + } + + return cloned; + } +} + +export class MockTitleElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'title'); + } + + get text() { + return this.textContent; + } + set text(value: string) { + this.textContent = value; + } +} + +export class MockUListElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'ul'); + } +} + +class MockSlotElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'slot'); + } + + assignedNodes(opts?: { flatten: boolean }): (MockNode | Node)[] { + let nodesToReturn: (MockNode | Node)[] = []; + + const ownerHost = (this.getRootNode() as any).host as MockElement; + if (!ownerHost) return nodesToReturn; + + if (ownerHost.childNodes.length) { + // try to find lightDOM nodes matching this slot's name (or lack of) + if ((this as any).name) { + nodesToReturn = ownerHost.childNodes.filter( + (n) => + n.nodeType === NODE_TYPES.ELEMENT_NODE && + (n as MockElement).getAttribute('slot') === (this as any).name, + ); + } else { + // find elements that do not have a slot attribute or + // any other type of node + nodesToReturn = ownerHost.childNodes.filter( + (n) => + (n.nodeType === NODE_TYPES.ELEMENT_NODE && !(n as MockElement).getAttribute('slot')) || + n.nodeType !== NODE_TYPES.ELEMENT_NODE, + ); + } + if (nodesToReturn.length) return nodesToReturn; + } + + // no flatten option? Return whatever's in this slot (without nested slots) + if (!opts?.flatten) return this.childNodes.filter((n) => !(n instanceof MockSlotElement)); + + // flatten option? Return all nodes in this slot (including anything within nested slots) + return this.childNodes.reduce( + (acc, node) => { + if (node instanceof MockSlotElement) { + acc.push(...node.assignedNodes(opts)); + } else { + acc.push(node); + } + return acc; + }, + [] as (MockNode | Node)[], + ); + } + + assignedElements(opts?: { flatten: boolean }): (Element | MockHTMLElement)[] { + let elesToReturn: (Element | MockHTMLElement)[] = []; + + const ownerHost = (this.getRootNode() as any).host as MockElement; + if (!ownerHost) return elesToReturn; + + if (ownerHost.children.length) { + // try to find lightDOM elements matching this slot's name (or lack of) + if ((this as any).name) { + elesToReturn = ownerHost.children.filter( + (n) => (n as MockElement).getAttribute('slot') == (this as any).name, + ); + } else { + elesToReturn = ownerHost.children.filter((n) => !(n as MockElement).getAttribute('slot')); + } + if (elesToReturn.length) return elesToReturn; + } + + // no flatten option? Return whatever elements are in this slot (without nested slots) + if (!opts?.flatten) return this.children.filter((n) => !(n instanceof MockSlotElement)); + + // flatten option? Return all elements in this slot (including anything within nested slots) + return this.children.reduce( + (acc, node) => { + if (node instanceof MockSlotElement) { + acc.push(...node.assignedElements(opts)); + } else { + acc.push(node); + } + return acc; + }, + [] as (MockElement | Element)[], + ); + } +} + +patchPropAttributes(MockSlotElement.prototype, { + name: String, +}); + +type CanvasContext = '2d' | 'webgl' | 'webgl2' | 'bitmaprenderer'; +class CanvasRenderingContext { + context: CanvasContext; + contextAttributes: WebGLContextAttributes; + constructor(context: CanvasContext, contextAttributes?: WebGLContextAttributes) { + this.context = context; + this.contextAttributes = contextAttributes; + } + fillRect() { + return; + } + clearRect() {} + getImageData(_: number, __: number, w: number, h: number) { + return { + data: new Array(w * h * 4), + }; + } + toDataURL() { + return 'data:,'; // blank image + } + putImageData() {} + createImageData(): ImageData { + return {} as ImageData; + } + setTransform() {} + drawImage() {} + save() {} + fillText() {} + restore() {} + beginPath() {} + moveTo() {} + lineTo() {} + closePath() {} + stroke() {} + translate() {} + scale() {} + rotate() {} + arc() {} + fill() {} + measureText() { + return { width: 0 }; + } + transform() {} + rect() {} + clip() {} +} + +export class MockCanvasElement extends MockHTMLElement { + constructor(ownerDocument: any) { + super(ownerDocument, 'canvas'); + } + getContext( + context: CanvasContext, + contextAttributes?: WebGLContextAttributes, + ): CanvasRenderingContext { + return new CanvasRenderingContext(context, contextAttributes); + } +} + +function fullUrl(elm: MockElement, attrName: string) { + const val = elm.getAttribute(attrName) || ''; + if (elm.ownerDocument != null) { + const win = elm.ownerDocument.defaultView as Window; + if (win != null) { + const loc = win.location; + if (loc != null) { + try { + const url = new URL(val, loc.href); + return url.href; + } catch {} + } + } + } + return val.replace(/'|"/g, '').trim(); +} + +function getLabelsForElement(elm: MockHTMLElement): MockHTMLElement[] { + const labels: MockHTMLElement[] = []; + const id = elm.id; + const doc = elm.ownerDocument; + + if (doc) { + // Find labels with "for" attribute matching this element's ID + if (id) { + const allLabels = doc.getElementsByTagName('label'); + for (let i = 0; i < allLabels.length; i++) { + const label = allLabels[i] as MockLabelElement; + if (label.htmlFor === id) { + labels.push(label); + } + } + } + + // Find labels that contain this element as a descendant + let parent = elm.parentNode as MockHTMLElement | null; + while (parent) { + if (parent.nodeName === 'LABEL' && !labels.includes(parent)) { + labels.push(parent); + } + parent = parent.parentNode as MockHTMLElement | null; + } + } + + return labels; +} + +function patchPropAttributes(prototype: any, attrs: any, defaults: any = {}) { + Object.keys(attrs).forEach((propName) => { + const attr = attrs[propName]; + const defaultValue = defaults[propName]; + + if (attr === Boolean) { + Object.defineProperty(prototype, propName, { + get(this: MockElement) { + return this.hasAttribute(propName); + }, + set(this: MockElement, value: boolean) { + if (value) { + this.setAttribute(propName, ''); + } else { + this.removeAttribute(propName); + } + }, + }); + } else if (attr === Number) { + Object.defineProperty(prototype, propName, { + get(this: MockElement) { + const value = this.getAttribute(propName); + return value ? parseInt(value, 10) : defaultValue === undefined ? 0 : defaultValue; + }, + set(this: MockElement, value: boolean) { + this.setAttribute(propName, value); + }, + }); + } else { + Object.defineProperty(prototype, propName, { + get(this: MockElement) { + return this.hasAttribute(propName) ? this.getAttribute(propName) : defaultValue || ''; + }, + set(this: MockElement, value: boolean) { + this.setAttribute(propName, value); + }, + }); + } + }); +} + +MockElement.prototype.cloneNode = function (this: MockElement, deep?: boolean) { + // because we're creating elements, which extending specific HTML base classes there + // is a MockElement circular reference that bundling has trouble dealing with so + // the fix is to add cloneNode() to MockElement's prototype after the HTML classes + const cloned = createElement(this.ownerDocument, this.nodeName); + cloned.attributes = cloneAttributes(this.attributes); + + const styleCssText = this.getAttribute('style'); + if (styleCssText != null && styleCssText.length > 0) { + cloned.setAttribute('style', styleCssText); + } + + if (deep) { + for (let i = 0, ii = this.childNodes.length; i < ii; i++) { + const clonedChildNode = this.childNodes[i].cloneNode(true); + cloned.appendChild(clonedChildNode); + } + } + + return cloned; +}; diff --git a/src/mock-doc/event.ts b/packages/mock-doc/src/event.ts similarity index 80% rename from src/mock-doc/event.ts rename to packages/mock-doc/src/event.ts index 4665cdbc8a5..e4240aacb44 100644 --- a/src/mock-doc/event.ts +++ b/packages/mock-doc/src/event.ts @@ -8,10 +8,10 @@ export class MockEvent { cancelBubble = false; cancelable = false; composed = false; - currentTarget: MockElement = null; + currentTarget: MockElement | null = null; defaultPrevented = false; - srcElement: MockElement = null; - target: MockElement = null; + srcElement: MockElement | null = null; + target: MockElement | null = null; timeStamp: number; type: string; @@ -40,7 +40,8 @@ export class MockEvent { } /** - * @ref https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath + * Get the composed path of event propagation. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath * @returns a composed path of the event */ composedPath(): MockElement[] { @@ -116,7 +117,7 @@ export class MockMouseEvent extends MockEvent { metaKey = false; button = 0; buttons = 0; - relatedTarget: EventTarget = null; + relatedTarget: EventTarget | null = null; constructor(type: string, mouseEventInitDic?: MouseEventInit) { super(type); @@ -127,7 +128,7 @@ export class MockMouseEvent extends MockEvent { } } -export class MockUIEvent extends MockEvent { +class MockUIEvent extends MockEvent { detail: number | null = null; view: MockWindow | null = null; @@ -152,7 +153,7 @@ export class MockFocusEvent extends MockUIEvent { } } -export class MockEventListener { +class MockEventListener { type: string; handler: (ev?: any) => void; @@ -162,6 +163,13 @@ export class MockEventListener { } } +/** + * Add an event listener to an element. + * + * @param elm - the element to add the listener to + * @param type - the event type to listen for + * @param handler - the event handler function + */ export function addEventListener(elm: any, type: string, handler: any) { const target: EventTarget = elm; @@ -172,6 +180,13 @@ export function addEventListener(elm: any, type: string, handler: any) { target.__listeners.push(new MockEventListener(type, handler)); } +/** + * Remove an event listener from an element. + * + * @param elm - the element to remove the listener from + * @param type - the event type to remove + * @param handler - the event handler function to remove + */ export function removeEventListener(elm: any, type: string, handler: any) { const target: EventTarget = elm; @@ -184,12 +199,23 @@ export function removeEventListener(elm: any, type: string, handler: any) { } } +/** + * Reset all event listeners on a target. + * + * @param target - the target to reset listeners on + */ export function resetEventListeners(target: any) { if (target != null && (target as EventTarget).__listeners != null) { - (target as EventTarget).__listeners = null; + (target as EventTarget).__listeners = null as any; } } +/** + * Trigger an event on an element and bubble through the DOM tree. + * + * @param elm - the element to trigger the event on + * @param ev - the mock event to trigger + */ function triggerEventListener(elm: any, ev: MockEvent) { if (elm == null || ev.cancelBubble === true) { return; @@ -223,6 +249,13 @@ function triggerEventListener(elm: any, ev: MockEvent) { } } +/** + * Get the next event target for event bubbling. + * + * @param elm - the current element + * @param ev - the mock event being bubbled + * @returns the next target element or null + */ function getNextEventTarget(elm: any, ev: MockEvent) { // If current element has a parent, bubble to parent if (elm.parentElement) { @@ -242,12 +275,19 @@ function getNextEventTarget(elm: any, ev: MockEvent) { return null; } +/** + * Dispatch an event on a target element. + * + * @param currentTarget - the element to dispatch the event on + * @param ev - the mock event to dispatch + * @returns true (always returns true for compatibility) + */ export function dispatchEvent(currentTarget: any, ev: MockEvent) { ev.target = currentTarget; triggerEventListener(currentTarget, ev); return true; } -export interface EventTarget { +interface EventTarget { __listeners: MockEventListener[]; } diff --git a/packages/mock-doc/src/global.ts b/packages/mock-doc/src/global.ts new file mode 100644 index 00000000000..9537a170c58 --- /dev/null +++ b/packages/mock-doc/src/global.ts @@ -0,0 +1,194 @@ +import { MockCSSStyleSheet } from './css-style-sheet'; +import { MockDocumentFragment } from './document-fragment'; +import { + MockAnchorElement, + MockBaseElement, + MockButtonElement, + MockCanvasElement, + MockFormElement, + MockImageElement, + MockInputElement, + MockLinkElement, + MockMetaElement, + MockScriptElement, + MockStyleElement, + MockTemplateElement, + MockTitleElement, + MockUListElement, +} from './element'; +import { + MockCustomEvent, + MockEvent, + MockFocusEvent, + MockKeyboardEvent, + MockMouseEvent, +} from './event'; +import { MockHeaders } from './headers'; +import { MockDOMParser } from './parser'; +import { MockRequest, MockResponse } from './request-response'; +import { MockWindow } from './window'; + +export function setupGlobal(gbl: any) { + if (gbl?.window == null) { + const win: any = (gbl.window = new MockWindow()); + + WINDOW_FUNCTIONS.forEach((fnName) => { + if (!(fnName in gbl)) { + gbl[fnName] = win[fnName].bind(win); + } + }); + + WINDOW_PROPS.forEach((propName) => { + if (!(propName in gbl)) { + Object.defineProperty(gbl, propName, { + get() { + return win[propName]; + }, + set(val: any) { + win[propName] = val; + }, + configurable: true, + enumerable: true, + }); + } + }); + + GLOBAL_CONSTRUCTORS.forEach(([cstrName]) => { + gbl[cstrName] = win[cstrName]; + }); + } + + return gbl.window; +} + +export function teardownGlobal(gbl: any) { + const win = gbl.window as Window; + if (win && typeof win.close === 'function') { + win.close(); + } +} + +export function patchWindow(winToBePatched: any) { + const mockWin: any = new MockWindow(false); + + WINDOW_FUNCTIONS.forEach((fnName) => { + if (typeof winToBePatched[fnName] !== 'function') { + winToBePatched[fnName] = mockWin[fnName].bind(mockWin); + } + }); + + WINDOW_PROPS.forEach((propName) => { + if (winToBePatched === undefined) { + Object.defineProperty(winToBePatched, propName, { + get() { + return mockWin[propName]; + }, + set(val: any) { + mockWin[propName] = val; + }, + configurable: true, + enumerable: true, + }); + } + }); +} + +export function addGlobalsToWindowPrototype(mockWinPrototype: any) { + GLOBAL_CONSTRUCTORS.forEach(([cstrName, Cstr]) => { + Object.defineProperty(mockWinPrototype, cstrName, { + get() { + return this['__' + cstrName] || Cstr; + }, + set(cstr: any) { + this['__' + cstrName] = cstr; + }, + configurable: true, + enumerable: true, + }); + }); +} + +const WINDOW_FUNCTIONS = [ + 'addEventListener', + 'alert', + 'blur', + 'cancelAnimationFrame', + 'cancelIdleCallback', + 'clearInterval', + 'clearTimeout', + 'close', + 'confirm', + 'dispatchEvent', + 'focus', + 'getComputedStyle', + 'matchMedia', + 'open', + 'prompt', + 'removeEventListener', + 'requestAnimationFrame', + 'requestIdleCallback', + 'URL', +]; + +const WINDOW_PROPS = [ + 'customElements', + 'devicePixelRatio', + 'document', + 'history', + 'innerHeight', + 'innerWidth', + 'localStorage', + 'location', + 'navigator', + 'pageXOffset', + 'pageYOffset', + 'performance', + 'screenLeft', + 'screenTop', + 'screenX', + 'screenY', + 'scrollX', + 'scrollY', + 'sessionStorage', + 'CSS', + 'CustomEvent', + 'Event', + 'Element', + 'HTMLElement', + 'Node', + 'NodeList', + 'FocusEvent', + 'KeyboardEvent', + 'MouseEvent', + 'CSSStyleSheet', +]; + +const GLOBAL_CONSTRUCTORS: [string, any][] = [ + ['CSSStyleSheet', MockCSSStyleSheet], + ['CustomEvent', MockCustomEvent], + ['DocumentFragment', MockDocumentFragment], + ['DOMParser', MockDOMParser], + ['Event', MockEvent], + ['FocusEvent', MockFocusEvent], + ['Headers', MockHeaders], + ['KeyboardEvent', MockKeyboardEvent], + ['MouseEvent', MockMouseEvent], + ['Request', MockRequest], + ['Response', MockResponse], + ['ShadowRoot', MockDocumentFragment], + + ['HTMLAnchorElement', MockAnchorElement], + ['HTMLBaseElement', MockBaseElement], + ['HTMLButtonElement', MockButtonElement], + ['HTMLCanvasElement', MockCanvasElement], + ['HTMLFormElement', MockFormElement], + ['HTMLImageElement', MockImageElement], + ['HTMLInputElement', MockInputElement], + ['HTMLLinkElement', MockLinkElement], + ['HTMLMetaElement', MockMetaElement], + ['HTMLScriptElement', MockScriptElement], + ['HTMLStyleElement', MockStyleElement], + ['HTMLTemplateElement', MockTemplateElement], + ['HTMLTitleElement', MockTitleElement], + ['HTMLUListElement', MockUListElement], +]; diff --git a/src/mock-doc/headers.ts b/packages/mock-doc/src/headers.ts similarity index 98% rename from src/mock-doc/headers.ts rename to packages/mock-doc/src/headers.ts index cbfd55edb71..391ac263301 100644 --- a/src/mock-doc/headers.ts +++ b/packages/mock-doc/src/headers.ts @@ -37,7 +37,7 @@ export class MockHeaders { entries(): any { const entries: string[][] = []; for (const kv of this.keys()) { - entries.push([kv, this.get(kv)]); + entries.push([kv, this.get(kv) ?? '']); } let index = -1; return { diff --git a/src/mock-doc/history.ts b/packages/mock-doc/src/history.ts similarity index 100% rename from src/mock-doc/history.ts rename to packages/mock-doc/src/history.ts diff --git a/packages/mock-doc/src/index.ts b/packages/mock-doc/src/index.ts new file mode 100644 index 00000000000..4c69be97ed7 --- /dev/null +++ b/packages/mock-doc/src/index.ts @@ -0,0 +1,14 @@ +export { cloneAttributes, MockAttr, MockAttributeMap } from './attribute'; +export { MockComment } from './comment-node'; +export { NODE_TYPES } from './constants'; +export { createDocument, createFragment, MockDocument, resetDocument } from './document'; +export { MockCustomEvent, MockKeyboardEvent, MockMouseEvent } from './event'; +export { patchWindow, setupGlobal, teardownGlobal } from './global'; +export { MockHeaders } from './headers'; +export { MockElement, MockHTMLElement, MockNode, MockTextNode } from './node'; +export { parseHtmlToDocument, parseHtmlToFragment } from './parse-html'; +export { MockRequest, MockResponse } from './request-response'; +export type { MockRequestInfo, MockRequestInit, MockResponseInit } from './request-response'; +export { serializeNodeToHtml } from './serialize-node'; +export type { SerializeNodeToHtmlOptions } from './serialize-node'; +export { cloneDocument, cloneWindow, constrainTimeouts, MockWindow } from './window'; diff --git a/src/mock-doc/intersection-observer.ts b/packages/mock-doc/src/intersection-observer.ts similarity index 84% rename from src/mock-doc/intersection-observer.ts rename to packages/mock-doc/src/intersection-observer.ts index 8b72da588db..4c96bcea17d 100644 --- a/src/mock-doc/intersection-observer.ts +++ b/packages/mock-doc/src/intersection-observer.ts @@ -1,8 +1,4 @@ export class MockIntersectionObserver { - constructor() { - /**/ - } - disconnect() { /**/ } diff --git a/src/mock-doc/location.ts b/packages/mock-doc/src/location.ts similarity index 100% rename from src/mock-doc/location.ts rename to packages/mock-doc/src/location.ts diff --git a/src/mock-doc/navigator.ts b/packages/mock-doc/src/navigator.ts similarity index 100% rename from src/mock-doc/navigator.ts rename to packages/mock-doc/src/navigator.ts diff --git a/src/mock-doc/node.ts b/packages/mock-doc/src/node.ts similarity index 78% rename from src/mock-doc/node.ts rename to packages/mock-doc/src/node.ts index f874c182ebf..0bc87f2602b 100644 --- a/src/mock-doc/node.ts +++ b/packages/mock-doc/src/node.ts @@ -1,7 +1,12 @@ import { createAttributeProxy, MockAttr, MockAttributeMap } from './attribute'; -import { NODE_NAMES, NODE_TYPES } from './constants'; +import { getPrefixForNamespace, NODE_NAMES, NODE_TYPES } from './constants'; import { createCSSStyleDeclaration, MockCSSStyleDeclaration } from './css-style-declaration'; -import { attributeChanged, checkAttributeChanged, connectNode, disconnectNode } from './custom-element-registry'; +import { + attributeChanged, + checkAttributeChanged, + connectNode, + disconnectNode, +} from './custom-element-registry'; import { dataset } from './dataset'; import { addEventListener, @@ -12,9 +17,13 @@ import { resetEventListeners, } from './event'; import { parseFragmentUtil } from './parse-util'; -import { matches, selectAll, selectOne } from './selector'; -import { NON_ESCAPABLE_CONTENT, serializeNodeToHtml, SerializeNodeToHtmlOptions } from './serialize-node'; +import { + NON_ESCAPABLE_CONTENT, + serializeNodeToHtml, + SerializeNodeToHtmlOptions, +} from './serialize-node'; import { MockTokenList } from './token-list'; +import type { MockDocument } from './document'; export class MockNode { private _nodeValue: string | null; @@ -24,7 +33,12 @@ export class MockNode { parentNode: MockNode | null; private _childNodes: MockNode[] = []; - constructor(ownerDocument: any, nodeType: number, nodeName: string | null, nodeValue: string | null) { + constructor( + ownerDocument: any, + nodeType: number, + nodeName: string | null, + nodeValue: string | null, + ) { this.ownerDocument = ownerDocument; this.nodeType = nodeType; this.nodeName = nodeName; @@ -65,7 +79,10 @@ export class MockNode { const firstChild = this.firstChild; items.forEach((item) => { const isNode = typeof item === 'object' && item !== null && 'nodeType' in item; - this.insertBefore(isNode ? item : this.ownerDocument.createTextNode(String(item)), firstChild); + this.insertBefore( + isNode ? item : this.ownerDocument.createTextNode(String(item)), + firstChild, + ); }); } @@ -73,10 +90,86 @@ export class MockNode { throw new Error(`invalid node type to clone: ${this.nodeType}, deep: ${deep}`); } - compareDocumentPosition(_other: MockNode) { - // unimplemented + compareDocumentPosition(other: MockNode): number { // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition - return -1; + const DOCUMENT_POSITION_DISCONNECTED = 1; + const DOCUMENT_POSITION_PRECEDING = 2; + const DOCUMENT_POSITION_FOLLOWING = 4; + const DOCUMENT_POSITION_CONTAINS = 8; + const DOCUMENT_POSITION_CONTAINED_BY = 16; + + if (this === other) { + return 0; + } + + // Check if either node is disconnected + let thisRoot: MockNode = this; + while (thisRoot.parentNode) { + thisRoot = thisRoot.parentNode; + } + + let otherRoot: MockNode = other; + while (otherRoot.parentNode) { + otherRoot = otherRoot.parentNode; + } + + if (thisRoot !== otherRoot) { + // Disconnected - return disconnected with arbitrary but consistent ordering + return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_PRECEDING; + } + + // Check if one contains the other + let node: MockNode | null = other; + while (node) { + if (node === this) { + return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING; + } + node = node.parentNode; + } + + node = this as MockNode; + while (node) { + if (node === other) { + return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING; + } + node = node.parentNode; + } + + // Determine document order by walking the tree + const getAncestors = (n: MockNode): MockNode[] => { + const ancestors: MockNode[] = []; + while (n) { + ancestors.unshift(n); + n = n.parentNode as MockNode; + } + return ancestors; + }; + + const thisAncestors = getAncestors(this); + const otherAncestors = getAncestors(other); + + // Find the common ancestor and compare positions + let i = 0; + while (thisAncestors[i] === otherAncestors[i]) { + i++; + } + + // Find which comes first among siblings + const commonParent = thisAncestors[i - 1]; + const thisChild = thisAncestors[i]; + const otherChild = otherAncestors[i]; + + for (const child of commonParent.childNodes) { + if (child === thisChild) { + return DOCUMENT_POSITION_FOLLOWING; + } + if (child === otherChild) { + return DOCUMENT_POSITION_PRECEDING; + } + } + + // Should not reach here + return DOCUMENT_POSITION_DISCONNECTED; } get firstChild(): MockNode | null { @@ -186,7 +279,11 @@ export class MockNode { remove() { if (this.parentNode != null) { - (this as any).__parentNode ? (this as any).__parentNode.removeChild(this) : this.parentNode.removeChild(this); + if ((this as any).__parentNode) { + (this as any).__parentNode.removeChild(this); + } else { + this.parentNode.removeChild(this); + } } } @@ -243,6 +340,7 @@ type MockElementInternals = Record; export class MockElement extends MockNode { __namespaceURI: string | null; + __localName: string | null; __attributeMap: MockAttributeMap | null | undefined; __shadowRoot: ShadowRoot | null | undefined; __style: MockCSSStyleDeclaration | null | undefined; @@ -264,8 +362,15 @@ export class MockElement extends MockNode { } constructor(ownerDocument: any, nodeName: string | null, namespaceURI: string | null = null) { - super(ownerDocument, NODE_TYPES.ELEMENT_NODE, typeof nodeName === 'string' ? nodeName : null, null); + super( + ownerDocument, + NODE_TYPES.ELEMENT_NODE, + typeof nodeName === 'string' ? nodeName : null, + null, + ); this.__namespaceURI = namespaceURI; + // Store original case-sensitive local name (important for SVG elements like foreignObject) + this.__localName = typeof nodeName === 'string' ? nodeName : null; this.__shadowRoot = null; this.__attributeMap = null; } @@ -276,6 +381,7 @@ export class MockElement extends MockNode { attachShadow(_opts: ShadowRootInit) { const shadowRoot = this.ownerDocument.createDocumentFragment(); + shadowRoot.mode = _opts.mode ?? 'open'; shadowRoot.delegatesFocus = _opts.delegatesFocus ?? false; this.shadowRoot = shadowRoot; return shadowRoot; @@ -292,7 +398,12 @@ export class MockElement extends MockNode { try { dispatchEvent( this, - new MockFocusEvent('blur', { relatedTarget: null, bubbles: true, cancelable: true, composed: true }), + new MockFocusEvent('blur', { + relatedTarget: null, + bubbles: true, + cancelable: true, + composed: true, + }), ); } finally { unmarkAsDispatching(this, 'blur'); @@ -300,16 +411,16 @@ export class MockElement extends MockNode { } get localName() { - /** - * The `localName` of an element should be always given, however the way - * MockDoc is constructed, it won't allow us to guarantee that. Let's throw - * and error we get into the situation where we don't have a `nodeName` set. - * - */ - if (!this.nodeName) { - throw new Error(`Can't compute elements localName without nodeName`); + // Use stored localName if available, otherwise derive from nodeName + const name = this.__localName ?? this.nodeName; + if (!name) { + throw new Error(`Can't get element's localName - not set`); + } + // HTML elements have lowercase localName, SVG/XML elements preserve case + if (this.__namespaceURI === 'http://www.w3.org/1999/xhtml' || this.__namespaceURI === null) { + return name.toLowerCase(); } - return this.nodeName.toLocaleLowerCase(); + return name; } get namespaceURI() { @@ -384,7 +495,10 @@ export class MockElement extends MockNode { } click() { - dispatchEvent(this, new MockEvent('click', { bubbles: true, cancelable: true, composed: true })); + dispatchEvent( + this, + new MockEvent('click', { bubbles: true, cancelable: true, composed: true }), + ); } override cloneNode(_deep?: boolean): MockElement { @@ -393,15 +507,12 @@ export class MockElement extends MockNode { return null; } + closest(selector: K): HTMLElementTagNameMap[K] | null; + closest(selector: K): SVGElementTagNameMap[K] | null; + closest(selector: string): E | null; closest(selector: string) { - let elm = this; - while (elm != null) { - if (elm.matches(selector)) { - return elm; - } - elm = elm.parentNode as any; - } - return null; + const doc = (this.ownerDocument ?? this) as MockDocument; + return doc._getDOMSelector().closest(selector, this as unknown as Element); } get dataset() { @@ -426,7 +537,12 @@ export class MockElement extends MockNode { focus(_options?: { preventScroll?: boolean }) { dispatchEvent( this, - new MockFocusEvent('focus', { relatedTarget: null, bubbles: true, cancelable: true, composed: true }), + new MockFocusEvent('focus', { + relatedTarget: null, + bubbles: true, + cancelable: true, + composed: true, + }), ); } @@ -460,6 +576,17 @@ export class MockElement extends MockNode { return new MockAttr(attrName, this.getAttribute(attrName)); } + getAttributeNames(): string[] { + const attrNames: string[] = []; + for (let i = 0; i < this.attributes.length; i++) { + const attr = this.attributes.item(i); + if (attr) { + attrNames.push(attr.name); + } + } + return attrNames; + } + getBoundingClientRect() { return { bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0 }; } @@ -535,7 +662,10 @@ export class MockElement extends MockNode { setTextContent(this, value); } - insertAdjacentElement(position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', elm: MockHTMLElement) { + insertAdjacentElement( + position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', + elm: MockHTMLElement, + ) { if (position === 'beforebegin' && this.parentNode) { insertBefore(this.parentNode, elm, this); } else if (position === 'afterbegin') { @@ -548,7 +678,10 @@ export class MockElement extends MockNode { return elm; } - insertAdjacentHTML(position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', html: string) { + insertAdjacentHTML( + position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', + html: string, + ) { const frag = parseFragmentUtil(this.ownerDocument, html); if (position === 'beforebegin') { while (frag.childNodes.length > 0) { @@ -567,13 +700,20 @@ export class MockElement extends MockNode { } else if (position === 'afterend') { while (frag.childNodes.length > 0) { if (this.parentNode) { - insertBefore(this.parentNode, frag.childNodes[frag.childNodes.length - 1], this.nextSibling); + insertBefore( + this.parentNode, + frag.childNodes[frag.childNodes.length - 1], + this.nextSibling, + ); } } } } - insertAdjacentText(position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', text: string) { + insertAdjacentText( + position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', + text: string, + ) { const elm = this.ownerDocument.createTextNode(text); if (position === 'beforebegin' && this.parentNode) { insertBefore(this.parentNode, elm, this); @@ -621,7 +761,8 @@ export class MockElement extends MockNode { } matches(selector: string) { - return matches(selector, this); + const doc = (this.ownerDocument ?? this) as MockDocument; + return doc._getDOMSelector().match(selector, this as unknown as Element); } get nextElementSibling() { @@ -678,12 +819,27 @@ export class MockElement extends MockNode { return results; } + // Overloads for tag name with optional class/id/attribute/pseudo selectors + querySelector( + selectors: K | `${K}.${string}` | `${K}#${string}` | `${K}[${string}` | `${K}:${string}`, + ): HTMLElementTagNameMap[K] | null; + querySelector( + selectors: K | `${K}.${string}` | `${K}#${string}` | `${K}[${string}` | `${K}:${string}`, + ): SVGElementTagNameMap[K] | null; + querySelector(selectors: string): E | null; querySelector(selector: string) { - return selectOne(selector, this); + const doc = (this.ownerDocument ?? this) as MockDocument; + // Use select()[0] instead of first() to ensure proper document ordering + // for comma-separated selectors + return doc._getDOMSelector().select(selector, this as unknown as Element)[0] ?? null; } + querySelectorAll(selectors: K): HTMLElementTagNameMap[K][]; + querySelectorAll(selectors: K): SVGElementTagNameMap[K][]; + querySelectorAll(selectors: string): E[]; querySelectorAll(selector: string) { - return selectAll(selector, this); + const doc = (this.ownerDocument ?? this) as MockDocument; + return doc._getDOMSelector().select(selector, this as unknown as Element); } removeAttribute(attrName: string) { @@ -749,7 +905,21 @@ export class MockElement extends MockNode { setAttributeNS(namespaceURI: string | null, attrName: string, value: any) { const attributes = this.attributes; - let attr = attributes.getNamedItemNS(namespaceURI, attrName); + + // Parse localName and prefix from attrName + let localName: string; + let prefix: string | null; + if (attrName.includes(':')) { + const [parsedPrefix, ...rest] = attrName.split(':'); + prefix = parsedPrefix; + localName = rest.join(':'); + } else { + localName = attrName; + // Get standard prefix from namespace if not provided in attrName + prefix = getPrefixForNamespace(namespaceURI); + } + + let attr = attributes.getNamedItemNS(namespaceURI, localName); const checkAttrChanged = checkAttributeChanged(this); if (attr != null) { @@ -764,11 +934,11 @@ export class MockElement extends MockNode { attr.value = value; } } else { - attr = new MockAttr(attrName, value, namespaceURI); + attr = new MockAttr(localName, value, namespaceURI, prefix); attributes.__items.push(attr); if (checkAttrChanged === true) { - attributeChanged(this, attrName, null, attr.value); + attributeChanged(this, attr.name, null, attr.value); } } } @@ -1163,7 +1333,7 @@ export class MockHTMLElement extends MockElement { override __namespaceURI = 'http://www.w3.org/1999/xhtml'; constructor(ownerDocument: any, nodeName: string | null) { - super(ownerDocument, typeof nodeName === 'string' ? nodeName.toUpperCase() : null); + super(ownerDocument, nodeName ? nodeName.toUpperCase() : null); } override get tagName() { @@ -1266,7 +1436,7 @@ const currentlyDispatching = new WeakMap>(); * @param eventType - The type of event that is currently dispatching. * @returns True if the element is currently dispatching the event, false otherwise. */ -export function isCurrentlyDispatching(target: any, eventType: string): boolean { +function isCurrentlyDispatching(target: any, eventType: string): boolean { const dispatchingEvents = currentlyDispatching.get(target); return dispatchingEvents != null && dispatchingEvents.has(eventType); } @@ -1275,7 +1445,7 @@ export function isCurrentlyDispatching(target: any, eventType: string): boolean * @param target - The element that is currently dispatching an event. * @param eventType - The type of event that is currently dispatching. */ -export function markAsDispatching(target: any, eventType: string): void { +function markAsDispatching(target: any, eventType: string): void { let dispatchingEvents = currentlyDispatching.get(target); if (dispatchingEvents == null) { dispatchingEvents = new Set(); @@ -1288,7 +1458,7 @@ export function markAsDispatching(target: any, eventType: string): void { * @param target - The element that is currently dispatching an event. * @param eventType - The type of event that is currently dispatching. */ -export function unmarkAsDispatching(target: any, eventType: string): void { +function unmarkAsDispatching(target: any, eventType: string): void { const dispatchingEvents = currentlyDispatching.get(target); if (dispatchingEvents != null) { dispatchingEvents.delete(eventType); diff --git a/packages/mock-doc/src/nwsapi.d.ts b/packages/mock-doc/src/nwsapi.d.ts new file mode 100644 index 00000000000..c1aa2115fd1 --- /dev/null +++ b/packages/mock-doc/src/nwsapi.d.ts @@ -0,0 +1,24 @@ +declare module 'nwsapi' { + interface NwsapiConfig { + LOGERRORS?: boolean; + VERBOSITY?: boolean; + IDS_DUPES?: boolean; + MIXEDCASE?: boolean; + ESCAPECHR?: boolean; + } + + interface NwsapiInstance { + configure(config: NwsapiConfig): void; + match(selector: string, element: unknown, context?: unknown): boolean; + first(selector: string, context: unknown): unknown | null; + select(selector: string, context: unknown): unknown[]; + closest(selector: string, element: unknown): unknown | null; + } + + interface NwsapiGlobal { + document: unknown; + } + + function nwsapi(global: NwsapiGlobal): NwsapiInstance; + export default nwsapi; +} diff --git a/src/mock-doc/parse-html.ts b/packages/mock-doc/src/parse-html.ts similarity index 91% rename from src/mock-doc/parse-html.ts rename to packages/mock-doc/src/parse-html.ts index f4722edbe68..8a65d41d6c1 100644 --- a/src/mock-doc/parse-html.ts +++ b/packages/mock-doc/src/parse-html.ts @@ -3,7 +3,7 @@ import { parseDocumentUtil, parseFragmentUtil } from './parse-util'; let sharedDocument: MockDocument; -export function parseHtmlToDocument(html: string, ownerDocument: MockDocument = null) { +export function parseHtmlToDocument(html: string, ownerDocument: MockDocument | null = null) { if (ownerDocument == null) { if (sharedDocument == null) { sharedDocument = new MockDocument(); @@ -14,7 +14,7 @@ export function parseHtmlToDocument(html: string, ownerDocument: MockDocument = return parseDocumentUtil(ownerDocument, html); } -export function parseHtmlToFragment(html: string, ownerDocument: MockDocument = null) { +export function parseHtmlToFragment(html: string, ownerDocument: MockDocument | null = null) { if (ownerDocument == null) { if (sharedDocument == null) { sharedDocument = new MockDocument(); diff --git a/src/mock-doc/parse-util.ts b/packages/mock-doc/src/parse-util.ts similarity index 94% rename from src/mock-doc/parse-util.ts rename to packages/mock-doc/src/parse-util.ts index 80125d56e4a..1bf3bcc65d7 100644 --- a/src/mock-doc/parse-util.ts +++ b/packages/mock-doc/src/parse-util.ts @@ -67,11 +67,13 @@ function getParser(ownerDocument: MockDocument) { const elm = ownerDocument.createElementNS(namespaceURI, tagName); for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; + // Construct qualified name with prefix if present + const qualifiedName = attr.prefix ? `${attr.prefix}:${attr.name}` : attr.name; if (attr.namespace == null || attr.namespace === 'http://www.w3.org/1999/xhtml') { - elm.setAttribute(attr.name, attr.value); + elm.setAttribute(qualifiedName, attr.value); } else { - elm.setAttributeNS(attr.namespace, attr.name, attr.value); + elm.setAttributeNS(attr.namespace, qualifiedName, attr.value); } } @@ -147,9 +149,10 @@ function getParser(ownerDocument: MockDocument) { adoptAttributes(recipient: MockElement, attrs: Token.Attribute[]) { for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; + const qualifiedName = attr.prefix ? `${attr.prefix}:${attr.name}` : attr.name; if (recipient.hasAttributeNS(attr.namespace, attr.name) === false) { - recipient.setAttributeNS(attr.namespace, attr.name, attr.value); + recipient.setAttributeNS(attr.namespace, qualifiedName, attr.value); } } }, diff --git a/src/mock-doc/parser.ts b/packages/mock-doc/src/parser.ts similarity index 92% rename from src/mock-doc/parser.ts rename to packages/mock-doc/src/parser.ts index b68184fe55d..67f9b511cd9 100644 --- a/src/mock-doc/parser.ts +++ b/packages/mock-doc/src/parser.ts @@ -1,7 +1,7 @@ import { MockDocument } from './document'; import { parseHtmlToDocument } from './parse-html'; -export type DOMParserSupportedType = +type DOMParserSupportedType = | 'text/html' | 'text/xml' | 'application/xml' diff --git a/src/mock-doc/performance.ts b/packages/mock-doc/src/performance.ts similarity index 94% rename from src/mock-doc/performance.ts rename to packages/mock-doc/src/performance.ts index c895945817a..32ca2ffc396 100644 --- a/src/mock-doc/performance.ts +++ b/packages/mock-doc/src/performance.ts @@ -4,6 +4,7 @@ export class MockPerformance implements Performance { timeOrigin: number; eventCounts: EventCounts; + interactionCount = 0; constructor() { this.timeOrigin = Date.now(); @@ -92,7 +93,7 @@ export class MockPerformance implements Performance { export function resetPerformance(perf: Performance) { if (perf != null) { try { - (perf as MockPerformance).timeOrigin = Date.now(); - } catch (e) {} + (perf as unknown as MockPerformance).timeOrigin = Date.now(); + } catch {} } } diff --git a/src/mock-doc/request-response.ts b/packages/mock-doc/src/request-response.ts similarity index 90% rename from src/mock-doc/request-response.ts rename to packages/mock-doc/src/request-response.ts index 15ffcf64cac..5e013b5d025 100644 --- a/src/mock-doc/request-response.ts +++ b/packages/mock-doc/src/request-response.ts @@ -51,10 +51,12 @@ export class MockRequest { } get url() { + const baseUrl = + typeof location !== 'undefined' && location.href ? location.href : 'http://localhost/'; if (typeof this._url === 'string') { - return new URL(this._url, location.href).href; + return new URL(this._url, baseUrl).href; } - return new URL('/', location.href).href; + return new URL('/', baseUrl).href; } set url(value: string) { this._url = value; @@ -89,7 +91,7 @@ export interface MockResponseInit { } export class MockResponse { - private _body: string; + private _body: string | undefined; headers: MockHeaders; ok = true; status = 200; diff --git a/src/mock-doc/resize-observer.ts b/packages/mock-doc/src/resize-observer.ts similarity index 84% rename from src/mock-doc/resize-observer.ts rename to packages/mock-doc/src/resize-observer.ts index 2fdadd7f16a..823f132b0cb 100644 --- a/src/mock-doc/resize-observer.ts +++ b/packages/mock-doc/src/resize-observer.ts @@ -1,8 +1,4 @@ export class MockResizeObserver { - constructor() { - /**/ - } - disconnect() { /**/ } diff --git a/src/mock-doc/serialize-node.ts b/packages/mock-doc/src/serialize-node.ts similarity index 87% rename from src/mock-doc/serialize-node.ts rename to packages/mock-doc/src/serialize-node.ts index f4801d8a22e..0f515043651 100644 --- a/src/mock-doc/serialize-node.ts +++ b/packages/mock-doc/src/serialize-node.ts @@ -1,3 +1,4 @@ +import { cloneAttributes } from './attribute'; import { CONTENT_REF_ID, HYDRATE_ID, @@ -5,8 +6,7 @@ import { SLOT_NODE_ID, TEXT_NODE_ID, XLINK_NS, -} from '../runtime/runtime-constants'; -import { cloneAttributes } from './attribute'; +} from './constants'; import { NODE_TYPES } from './constants'; import { type MockDocument } from './document'; import { type MockNode } from './node'; @@ -30,14 +30,22 @@ function normalizeSerializationOptions(opts: Partial indentSpaces: typeof opts.indentSpaces !== 'number' ? 0 : opts.indentSpaces, newLines: typeof opts.newLines !== 'boolean' ? false : opts.newLines, }), - approximateLineWidth: typeof opts.approximateLineWidth !== 'number' ? -1 : opts.approximateLineWidth, - removeEmptyAttributes: typeof opts.removeEmptyAttributes !== 'boolean' ? true : opts.removeEmptyAttributes, - removeAttributeQuotes: typeof opts.removeAttributeQuotes !== 'boolean' ? false : opts.removeAttributeQuotes, + approximateLineWidth: + typeof opts.approximateLineWidth !== 'number' ? -1 : opts.approximateLineWidth, + removeEmptyAttributes: + typeof opts.removeEmptyAttributes !== 'boolean' ? true : opts.removeEmptyAttributes, + removeAttributeQuotes: + typeof opts.removeAttributeQuotes !== 'boolean' ? false : opts.removeAttributeQuotes, removeBooleanAttributeQuotes: - typeof opts.removeBooleanAttributeQuotes !== 'boolean' ? false : opts.removeBooleanAttributeQuotes, - removeHtmlComments: typeof opts.removeHtmlComments !== 'boolean' ? false : opts.removeHtmlComments, + typeof opts.removeBooleanAttributeQuotes !== 'boolean' + ? false + : opts.removeBooleanAttributeQuotes, + removeHtmlComments: + typeof opts.removeHtmlComments !== 'boolean' ? false : opts.removeHtmlComments, serializeShadowRoot: - typeof opts.serializeShadowRoot === 'undefined' ? 'declarative-shadow-dom' : opts.serializeShadowRoot, + typeof opts.serializeShadowRoot === 'undefined' + ? 'declarative-shadow-dom' + : opts.serializeShadowRoot, fullDocument: typeof opts.fullDocument !== 'boolean' ? true : opts.fullDocument, } as const; } @@ -53,7 +61,10 @@ function normalizeSerializationOptions(opts: Partial * @param serializationOptions options to control serialization behavior * @returns an html string */ -export function serializeNodeToHtml(elm: Node | MockNode, serializationOptions: SerializeNodeToHtmlOptions = {}) { +export function serializeNodeToHtml( + elm: Node | MockNode, + serializationOptions: SerializeNodeToHtmlOptions = {}, +) { const opts = normalizeSerializationOptions(serializationOptions); const output: SerializeOutput = { currentLineWidth: 0, @@ -89,7 +100,7 @@ const shadowRootTag = 'mock:shadow-root'; * @param node the node to serialize * @param opts options to control serialization behavior * @param output keeps track of the current line width and indentation - * @returns a generator that yields the serialized HTML in chunks + * @yields the serialized HTML in chunks */ function* streamToHtml( node: Node | MockNode, @@ -137,7 +148,7 @@ function* streamToHtml( /** * If the node is a shadow root, we want to add the `shadowrootmode` attribute */ - ('host' in node || node.nodeName.toLocaleLowerCase() === shadowRootTag) + ('host' in node || node.nodeName!.toLocaleLowerCase() === shadowRootTag) ) { const mode = ` shadowrootmode="open"`; yield mode; @@ -180,6 +191,8 @@ function* streamToHtml( } const attrNamespaceURI = attr.namespaceURI; + // Use localName for namespaced attributes since we add the prefix manually + const attrLocalName = attr.localName ?? attrName; if (attrNamespaceURI == null) { output.currentLineWidth += attrName.length + 1; if ( @@ -193,28 +206,28 @@ function* streamToHtml( yield ' ' + attrName; } } else if (attrNamespaceURI === 'http://www.w3.org/XML/1998/namespace') { - yield ' xml:' + attrName; - output.currentLineWidth += attrName.length + 5; + yield ' xml:' + attrLocalName; + output.currentLineWidth += attrLocalName.length + 5; } else if (attrNamespaceURI === 'http://www.w3.org/2000/xmlns/') { - if (attrName !== 'xmlns') { - yield ' xmlns:' + attrName; - output.currentLineWidth += attrName.length + 7; + if (attrLocalName !== 'xmlns') { + yield ' xmlns:' + attrLocalName; + output.currentLineWidth += attrLocalName.length + 7; } else { - yield ' ' + attrName; - output.currentLineWidth += attrName.length + 1; + yield ' ' + attrLocalName; + output.currentLineWidth += attrLocalName.length + 1; } } else if (attrNamespaceURI === XLINK_NS) { - yield ' xlink:' + attrName; - output.currentLineWidth += attrName.length + 7; + yield ' xlink:' + attrLocalName; + output.currentLineWidth += attrLocalName.length + 7; } else { - yield ' ' + attrNamespaceURI + ':' + attrName; - output.currentLineWidth += attrNamespaceURI.length + attrName.length + 2; + yield ' ' + attrNamespaceURI + ':' + attrLocalName; + output.currentLineWidth += attrNamespaceURI.length + attrLocalName.length + 2; } if (opts.prettyHtml && attrName === 'class') { attrValue = attr.value = attrValue .split(' ') - .filter((t) => t !== '') + .filter((t: string) => t !== '') .sort() .join(' ') .trim(); @@ -305,9 +318,15 @@ function* streamToHtml( // skip over empty text nodes } else { const isWithinWhitespaceSensitiveNode = - opts.newLines || (opts.indentSpaces ?? 0) > 0 ? isWithinWhitespaceSensitive(node) : false; - - if (!isWithinWhitespaceSensitiveNode && (opts.indentSpaces ?? 0) > 0 && ignoreTag === false) { + opts.newLines || (opts.indentSpaces ?? 0) > 0 + ? isWithinWhitespaceSensitive(node) + : false; + + if ( + !isWithinWhitespaceSensitiveNode && + (opts.indentSpaces ?? 0) > 0 && + ignoreTag === false + ) { output.indent = output.indent + (opts.indentSpaces ?? 0); } @@ -321,7 +340,8 @@ function* streamToHtml( * is set on the node. */ const sId = (node as HTMLElement).attributes.getNamedItem(HYDRATE_ID); - const isStencilDeclarativeShadowDOM = childNodes[i].nodeName.toLowerCase() === 'template' && sId; + const isStencilDeclarativeShadowDOM = + childNodes[i].nodeName.toLowerCase() === 'template' && sId; if (isStencilDeclarativeShadowDOM) { yield `\n${' '.repeat(output.indent)}`; continue; @@ -400,7 +420,9 @@ function* streamToHtml( } else { // this text node has text content const isWithinWhitespaceSensitiveNode = - opts.newLines || (opts.indentSpaces ?? 0) > 0 || opts.prettyHtml ? isWithinWhitespaceSensitive(node) : false; + opts.newLines || (opts.indentSpaces ?? 0) > 0 || opts.prettyHtml + ? isWithinWhitespaceSensitive(node) + : false; if (opts.newLines && !isWithinWhitespaceSensitiveNode) { yield '\n'; output.currentLineWidth = 0; @@ -445,7 +467,7 @@ function* streamToHtml( // this element is not a whitespace sensitive one, like
     or  so
                     // any whitespace at the start and end can be cleaned up to just be one space
                     if (/\s/.test(textContent.charAt(0))) {
    -                  textContent = ' ' + textContent.trimLeft();
    +                  textContent = ' ' + textContent.trimStart();
                     }
     
                     textContentLength = textContent.length;
    @@ -456,10 +478,10 @@ function* streamToHtml(
                           opts.approximateLineWidth > 0 &&
                           output.currentLineWidth + textContentLength > opts.approximateLineWidth
                         ) {
    -                      textContent = textContent.trimRight() + '\n';
    +                      textContent = textContent.trimEnd() + '\n';
                           output.currentLineWidth = 0;
                         } else {
    -                      textContent = textContent.trimRight() + ' ';
    +                      textContent = textContent.trimEnd() + ' ';
                         }
                       }
                     }
    @@ -517,7 +539,7 @@ const NBSP_REGEX = /\u00a0/g;
     const DOUBLE_QUOTE_REGEX = /"/g;
     const LT_REGEX = //g;
    -const CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>\/\\-]+$/;
    +const CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>/\\-]+$/;
     
     function getTagName(element: Element) {
       if (element.namespaceURI === 'http://www.w3.org/1999/xhtml') {
    @@ -567,103 +589,72 @@ function getChildNodes(node: Node | MockNode) {
       return ((node as any).__childNodes || node.childNodes) as NodeList;
     }
     
    -// TODO(STENCIL-1299): Audit this list, remove unsupported/deprecated elements
     /*@__PURE__*/ export const NON_ESCAPABLE_CONTENT = new Set([
       'STYLE',
       'SCRIPT',
       'IFRAME',
       'NOSCRIPT',
    -  'XMP',
    -  'NOEMBED',
    -  'NOFRAMES',
    -  'PLAINTEXT',
     ]);
     
    -// TODO(STENCIL-1299): Audit this list, remove unsupported/deprecated elements
     /**
      * A list of whitespace sensitive tag names, such as `code`, `pre`, etc.
      */
    -/*@__PURE__*/ export const WHITESPACE_SENSITIVE = new Set([
    +/*@__PURE__*/ const WHITESPACE_SENSITIVE = new Set([
       'CODE',
       'OUTPUT',
    -  'PLAINTEXT',
       'PRE',
       'SCRIPT',
       'TEMPLATE',
       'TEXTAREA',
     ]);
     
    -// TODO(STENCIL-1299): Audit this list, remove unsupported/deprecated elements
     /*@__PURE__*/ export const EMPTY_ELEMENTS = new Set([
       'area',
       'base',
    -  'basefont',
    -  'bgsound',
       'br',
       'col',
       'embed',
    -  'frame',
       'hr',
       'img',
       'input',
    -  'keygen',
       'link',
       'meta',
       'param',
       'source',
    -  'trace',
       'track',
       'wbr',
     ]);
     
    -// TODO(STENCIL-1299): Audit this list, remove unsupported/deprecated attr
     /*@__PURE__*/ const REMOVE_EMPTY_ATTR = new Set(['class', 'dir', 'id', 'lang', 'name', 'title']);
     
    -// TODO(STENCIL-1299): Audit this list, remove unsupported/deprecated attr
     /*@__PURE__*/ const BOOLEAN_ATTR = new Set([
       'allowfullscreen',
       'async',
       'autofocus',
       'autoplay',
       'checked',
    -  'compact',
       'controls',
    -  'declare',
       'default',
    -  'defaultchecked',
    -  'defaultmuted',
    -  'defaultselected',
       'defer',
       'disabled',
    -  'enabled',
       'formnovalidate',
       'hidden',
    -  'indeterminate',
       'inert',
       'ismap',
       'itemscope',
       'loop',
       'multiple',
       'muted',
    -  'nohref',
       'nomodule',
    -  'noresize',
    -  'noshade',
       'novalidate',
    -  'nowrap',
       'open',
    -  'pauseonexit',
       'readonly',
       'required',
       'reversed',
    -  'scoped',
    -  'seamless',
       'selected',
    +  'shadowrootclonable',
       'shadowrootdelegatesfocus',
    -  'sortable',
    -  'truespeed',
    -  'typemustmatch',
    -  'visible',
    +  'shadowrootserializable',
     ]);
     
     /*@__PURE__*/ const STRUCTURE_ELEMENTS = new Set([
    diff --git a/packages/mock-doc/src/shadow-root.ts b/packages/mock-doc/src/shadow-root.ts
    new file mode 100644
    index 00000000000..0c93435ab6a
    --- /dev/null
    +++ b/packages/mock-doc/src/shadow-root.ts
    @@ -0,0 +1,65 @@
    +import { MockDocumentFragment } from './document-fragment';
    +
    +export class MockShadowRoot extends MockDocumentFragment {
    +  private _mode: 'open' | 'closed' = 'open';
    +  private _delegatesFocus: boolean = false;
    +
    +  get activeElement(): HTMLElement | null {
    +    return null;
    +  }
    +
    +  get cloneable(): boolean {
    +    return false;
    +  }
    +
    +  get delegatesFocus(): boolean {
    +    return this._delegatesFocus;
    +  }
    +
    +  set delegatesFocus(value: boolean) {
    +    this._delegatesFocus = value;
    +  }
    +
    +  get fullscreenElement(): HTMLElement | null {
    +    return null;
    +  }
    +
    +  get host(): HTMLElement | null {
    +    let parent = this.parentElement();
    +    while (parent) {
    +      if (parent.nodeType === 11) {
    +        return parent;
    +      }
    +      parent = parent.parentElement();
    +    }
    +    return null;
    +  }
    +
    +  get mode(): 'open' | 'closed' {
    +    return this._mode;
    +  }
    +
    +  set mode(value: 'open' | 'closed') {
    +    this._mode = value;
    +  }
    +
    +  get pictureInPictureElement(): HTMLElement | null {
    +    return null;
    +  }
    +
    +  get pointerLockElement(): HTMLElement | null {
    +    return null;
    +  }
    +
    +  get serializable(): boolean {
    +    return false;
    +  }
    +
    +  get slotAssignment(): 'named' | 'manual' {
    +    return 'named';
    +  }
    +
    +  get styleSheets(): StyleSheet[] {
    +    return [];
    +  }
    +}
    diff --git a/src/mock-doc/storage.ts b/packages/mock-doc/src/storage.ts
    similarity index 100%
    rename from src/mock-doc/storage.ts
    rename to packages/mock-doc/src/storage.ts
    diff --git a/src/mock-doc/token-list.ts b/packages/mock-doc/src/token-list.ts
    similarity index 93%
    rename from src/mock-doc/token-list.ts
    rename to packages/mock-doc/src/token-list.ts
    index 61de69c95c0..3e8d2655b09 100644
    --- a/src/mock-doc/token-list.ts
    +++ b/packages/mock-doc/src/token-list.ts
    @@ -69,7 +69,9 @@ function validateToken(token: string) {
         throw new Error('The token provided must not be empty.');
       }
       if (/\s/.test(token)) {
    -    throw new Error(`The token provided ('${token}') contains HTML space characters, which are not valid in tokens.`);
    +    throw new Error(
    +      `The token provided ('${token}') contains HTML space characters, which are not valid in tokens.`,
    +    );
       }
     }
     
    diff --git a/packages/mock-doc/src/tree-walker.ts b/packages/mock-doc/src/tree-walker.ts
    new file mode 100644
    index 00000000000..2e6a9e11ecb
    --- /dev/null
    +++ b/packages/mock-doc/src/tree-walker.ts
    @@ -0,0 +1,177 @@
    +import type { MockElement } from './node';
    +
    +/**
    + * NodeFilter constants for use in Node.js environment.
    + */
    +const FILTER_ACCEPT = 1;
    +const FILTER_SKIP = 3;
    +
    +/**
    + * A minimal TreeWalker implementation for dom-selector compatibility.
    + */
    +export class MockTreeWalker implements TreeWalker {
    +  root: Node;
    +  whatToShow: number;
    +  filter: NodeFilter | null;
    +  currentNode: Node;
    +
    +  constructor(root: MockElement, whatToShow: number, filter: NodeFilter | null) {
    +    this.root = root as unknown as Node;
    +    this.whatToShow = whatToShow;
    +    this.filter = filter;
    +    this.currentNode = root as unknown as Node;
    +  }
    +
    +  private acceptNode(node: Node): number {
    +    // Check whatToShow
    +    const nodeType = node.nodeType;
    +    const mask = 1 << (nodeType - 1);
    +    if (!(this.whatToShow & mask)) {
    +      return FILTER_SKIP;
    +    }
    +
    +    // Check filter
    +    if (this.filter) {
    +      if (typeof this.filter === 'function') {
    +        return (this.filter as unknown as (node: Node) => number)(node);
    +      }
    +      return this.filter.acceptNode(node);
    +    }
    +
    +    return FILTER_ACCEPT;
    +  }
    +
    +  parentNode(): Node | null {
    +    let node = this.currentNode;
    +    while (node && node !== this.root) {
    +      node = node.parentNode as Node;
    +      if (node && this.acceptNode(node) === FILTER_ACCEPT) {
    +        this.currentNode = node;
    +        return node;
    +      }
    +    }
    +    return null;
    +  }
    +
    +  firstChild(): Node | null {
    +    return this.traverseChildren('first');
    +  }
    +
    +  lastChild(): Node | null {
    +    return this.traverseChildren('last');
    +  }
    +
    +  nextSibling(): Node | null {
    +    return this.traverseSiblings('next');
    +  }
    +
    +  previousSibling(): Node | null {
    +    return this.traverseSiblings('previous');
    +  }
    +
    +  nextNode(): Node | null {
    +    let node: Node | null = this.currentNode;
    +    let result: number;
    +
    +    while (node) {
    +      // Try first child
    +      if (node.firstChild) {
    +        node = node.firstChild;
    +        result = this.acceptNode(node);
    +        if (result === FILTER_ACCEPT) {
    +          this.currentNode = node;
    +          return node;
    +        }
    +        continue;
    +      }
    +
    +      // Try siblings and ancestors' siblings
    +      while (node) {
    +        if (node === this.root) {
    +          return null;
    +        }
    +        if (node.nextSibling) {
    +          node = node.nextSibling;
    +          result = this.acceptNode(node);
    +          if (result === FILTER_ACCEPT) {
    +            this.currentNode = node;
    +            return node;
    +          }
    +          break;
    +        }
    +        node = node.parentNode as Node;
    +      }
    +    }
    +    return null;
    +  }
    +
    +  previousNode(): Node | null {
    +    let node: Node | null = this.currentNode;
    +    while (node && node !== this.root) {
    +      if (node.previousSibling) {
    +        node = node.previousSibling;
    +        // Go to last descendant
    +        while (node.lastChild) {
    +          node = node.lastChild;
    +        }
    +        if (this.acceptNode(node) === FILTER_ACCEPT) {
    +          this.currentNode = node;
    +          return node;
    +        }
    +      } else {
    +        node = node.parentNode as Node;
    +        if (node && node !== this.root && this.acceptNode(node) === FILTER_ACCEPT) {
    +          this.currentNode = node;
    +          return node;
    +        }
    +      }
    +    }
    +    return null;
    +  }
    +
    +  private traverseChildren(type: 'first' | 'last'): Node | null {
    +    let node: Node | null =
    +      type === 'first' ? this.currentNode.firstChild : this.currentNode.lastChild;
    +    while (node) {
    +      const result = this.acceptNode(node);
    +      if (result === FILTER_ACCEPT) {
    +        this.currentNode = node;
    +        return node;
    +      }
    +      if (result === FILTER_SKIP) {
    +        const child = type === 'first' ? node.firstChild : node.lastChild;
    +        if (child) {
    +          node = child;
    +          continue;
    +        }
    +      }
    +      node = type === 'first' ? node.nextSibling : node.previousSibling;
    +    }
    +    return null;
    +  }
    +
    +  private traverseSiblings(type: 'next' | 'previous'): Node | null {
    +    let node: Node | null = this.currentNode;
    +    while (node && node !== this.root) {
    +      const sibling = type === 'next' ? node.nextSibling : node.previousSibling;
    +      if (sibling) {
    +        node = sibling;
    +        const result = this.acceptNode(node);
    +        if (result === FILTER_ACCEPT) {
    +          this.currentNode = node;
    +          return node;
    +        }
    +        if (result === FILTER_SKIP) {
    +          const child = type === 'next' ? node.firstChild : node.lastChild;
    +          if (child) {
    +            node = child;
    +            continue;
    +          }
    +        }
    +        continue;
    +      }
    +      node = node.parentNode as Node;
    +    }
    +    return null;
    +  }
    +}
    diff --git a/packages/mock-doc/src/window.ts b/packages/mock-doc/src/window.ts
    new file mode 100644
    index 00000000000..712b3964100
    --- /dev/null
    +++ b/packages/mock-doc/src/window.ts
    @@ -0,0 +1,935 @@
    +import { createConsole } from './console';
    +import { MockCustomElementRegistry } from './custom-element-registry';
    +import { MockDocument, resetDocument } from './document';
    +import { MockDocumentFragment } from './document-fragment';
    +import { MockSVGElement } from './element';
    +import {
    +  addEventListener,
    +  dispatchEvent,
    +  MockCustomEvent,
    +  MockEvent,
    +  MockFocusEvent,
    +  MockKeyboardEvent,
    +  MockMouseEvent,
    +  removeEventListener,
    +  resetEventListeners,
    +} from './event';
    +import { addGlobalsToWindowPrototype } from './global';
    +import { MockHistory } from './history';
    +import { MockIntersectionObserver } from './intersection-observer';
    +import { MockLocation } from './location';
    +import { MockNavigator } from './navigator';
    +import { MockElement, MockHTMLElement, MockNode, MockNodeList } from './node';
    +import { MockPerformance, resetPerformance } from './performance';
    +import { MockResizeObserver } from './resize-observer';
    +import { MockShadowRoot } from './shadow-root';
    +import { MockStorage } from './storage';
    +import { MockHeaders } from '.';
    +
    +const nativeClearInterval = globalThis.clearInterval;
    +const nativeClearTimeout = globalThis.clearTimeout;
    +const nativeDOMException = globalThis.DOMException;
    +const nativeSetInterval = globalThis.setInterval;
    +const nativeSetTimeout = globalThis.setTimeout;
    +const nativeURL = globalThis.URL;
    +const nativeWindow = globalThis.window;
    +
    +export class MockWindow {
    +  __timeouts: Set;
    +  __history: MockHistory;
    +  __elementCstr: any;
    +  __charDataCstr: any;
    +  __docTypeCstr: any;
    +  __docCstr: any;
    +  __docFragCstr: any;
    +  __domTokenListCstr: any;
    +  __nodeCstr: any;
    +  __nodeListCstr: any;
    +  __localStorage: MockStorage;
    +  __sessionStorage: MockStorage;
    +  __location: MockLocation;
    +  __navigator: MockNavigator;
    +  __clearInterval: typeof nativeClearInterval;
    +  __clearTimeout: typeof nativeClearTimeout;
    +  __setInterval: typeof nativeSetInterval;
    +  __setTimeout: typeof nativeSetTimeout;
    +  __maxTimeout: number;
    +  __allowInterval: boolean;
    +  URL: typeof URL;
    +
    +  console: Console;
    +  customElements: CustomElementRegistry | null;
    +  document: Document;
    +  performance: Performance;
    +
    +  devicePixelRatio: number;
    +  innerHeight: number;
    +  innerWidth: number;
    +  pageXOffset: number;
    +  pageYOffset: number;
    +  screen: Screen;
    +  screenLeft: number;
    +  screenTop: number;
    +  screenX: number;
    +  screenY: number;
    +  scrollX: number;
    +  scrollY: number;
    +
    +  // event handlers
    +  declare CustomEvent: typeof MockCustomEvent;
    +  declare Event: typeof MockEvent;
    +  declare Headers: typeof MockHeaders;
    +  declare FocusEvent: typeof MockFocusEvent;
    +  declare KeyboardEvent: typeof MockKeyboardEvent;
    +  declare MouseEvent: typeof MockMouseEvent;
    +
    +  constructor(html: string | boolean = null) {
    +    if (html !== false) {
    +      this.document = new MockDocument(html, this) as any;
    +    } else {
    +      this.document = null;
    +    }
    +    this.performance = new MockPerformance();
    +    this.customElements = new MockCustomElementRegistry(this as any);
    +    this.console = createConsole();
    +    resetWindowDefaults(this);
    +    resetWindowDimensions(this);
    +  }
    +
    +  addEventListener(type: string, handler: (ev?: any) => void) {
    +    addEventListener(this, type, handler);
    +  }
    +
    +  alert(msg: string) {
    +    if (this.console) {
    +      this.console.debug(msg);
    +    } else {
    +      console.debug(msg);
    +    }
    +  }
    +
    +  blur(): any {
    +    /**/
    +  }
    +
    +  cancelAnimationFrame(id: any) {
    +    this.__clearTimeout.call(nativeWindow || this, id);
    +  }
    +
    +  cancelIdleCallback(id: any) {
    +    this.__clearTimeout.call(nativeWindow || this, id);
    +  }
    +
    +  get CharacterData() {
    +    if (this.__charDataCstr == null) {
    +      const ownerDocument = this.document;
    +      this.__charDataCstr = class extends MockNode {
    +        constructor() {
    +          super(ownerDocument, 0, 'test', '');
    +          throw new Error('Illegal constructor: cannot construct CharacterData');
    +        }
    +      };
    +    }
    +    return this.__charDataCstr;
    +  }
    +  set CharacterData(charDataCstr: any) {
    +    this.__charDataCstr = charDataCstr;
    +  }
    +
    +  clearInterval(id: any) {
    +    this.__clearInterval.call(nativeWindow || this, id);
    +  }
    +
    +  clearTimeout(id: any) {
    +    this.__clearTimeout.call(nativeWindow || this, id);
    +  }
    +
    +  close() {
    +    resetWindow(this as any);
    +  }
    +
    +  confirm() {
    +    return false;
    +  }
    +
    +  get CSS() {
    +    return {
    +      supports: () => true,
    +    };
    +  }
    +
    +  get Document() {
    +    if (this.__docCstr == null) {
    +      const win = this;
    +      this.__docCstr = class extends MockDocument {
    +        constructor() {
    +          super(false, win);
    +          throw new Error('Illegal constructor: cannot construct Document');
    +        }
    +      };
    +    }
    +    return this.__docCstr;
    +  }
    +  set Document(docCstr: any) {
    +    this.__docCstr = docCstr;
    +  }
    +
    +  get DocumentFragment() {
    +    if (this.__docFragCstr == null) {
    +      const ownerDocument = this.document;
    +      this.__docFragCstr = class extends MockDocumentFragment {
    +        constructor() {
    +          super(ownerDocument);
    +          throw new Error('Illegal constructor: cannot construct DocumentFragment');
    +        }
    +      };
    +    }
    +    return this.__docFragCstr;
    +  }
    +  set DocumentFragment(docFragCstr: any) {
    +    this.__docFragCstr = docFragCstr;
    +  }
    +
    +  get ShadowRoot() {
    +    return MockShadowRoot;
    +  }
    +
    +  get DocumentType() {
    +    if (this.__docTypeCstr == null) {
    +      const ownerDocument = this.document;
    +      this.__docTypeCstr = class extends MockNode {
    +        constructor() {
    +          super(ownerDocument, 0, 'test', '');
    +          throw new Error('Illegal constructor: cannot construct DocumentType');
    +        }
    +      };
    +    }
    +    return this.__docTypeCstr;
    +  }
    +  set DocumentType(docTypeCstr: any) {
    +    this.__docTypeCstr = docTypeCstr;
    +  }
    +
    +  get DOMTokenList() {
    +    if (this.__domTokenListCstr == null) {
    +      this.__domTokenListCstr = class MockDOMTokenList {};
    +    }
    +    return this.__domTokenListCstr;
    +  }
    +  set DOMTokenList(domTokenListCstr: any) {
    +    this.__domTokenListCstr = domTokenListCstr;
    +  }
    +
    +  dispatchEvent(ev: MockEvent) {
    +    return dispatchEvent(this, ev);
    +  }
    +
    +  get Element() {
    +    return MockElement;
    +  }
    +
    +  get DOMException() {
    +    return nativeDOMException;
    +  }
    +
    +  fetch(input: any, init?: any): any {
    +    if (typeof fetch === 'function') {
    +      return fetch(input, init);
    +    }
    +    throw new Error(`fetch() not implemented`);
    +  }
    +
    +  focus(): any {
    +    /**/
    +  }
    +
    +  getComputedStyle(_: any) {
    +    return {
    +      cssText: '',
    +      length: 0,
    +      parentRule: null,
    +      getPropertyPriority(): any {
    +        return null;
    +      },
    +      getPropertyValue(): any {
    +        return '';
    +      },
    +      item(): any {
    +        return null;
    +      },
    +      removeProperty(): any {
    +        return null;
    +      },
    +      setProperty(): any {
    +        return null;
    +      },
    +    } as any;
    +  }
    +
    +  get globalThis() {
    +    return this;
    +  }
    +
    +  get history() {
    +    if (this.__history == null) {
    +      this.__history = new MockHistory();
    +    }
    +    return this.__history;
    +  }
    +  set history(hsty: any) {
    +    this.__history = hsty;
    +  }
    +
    +  get JSON() {
    +    return JSON;
    +  }
    +
    +  get HTMLElement() {
    +    return MockHTMLElement;
    +  }
    +
    +  get SVGElement() {
    +    return MockSVGElement;
    +  }
    +
    +  get IntersectionObserver() {
    +    return MockIntersectionObserver;
    +  }
    +
    +  get ResizeObserver() {
    +    return MockResizeObserver;
    +  }
    +
    +  get localStorage() {
    +    if (this.__localStorage == null) {
    +      this.__localStorage = new MockStorage();
    +    }
    +    return this.__localStorage;
    +  }
    +  set localStorage(locStorage: MockStorage) {
    +    this.__localStorage = locStorage;
    +  }
    +
    +  get location(): MockLocation {
    +    if (this.__location == null) {
    +      this.__location = new MockLocation();
    +    }
    +    return this.__location;
    +  }
    +  set location(val: Location | string) {
    +    if (typeof val === 'string') {
    +      if (this.__location == null) {
    +        this.__location = new MockLocation();
    +      }
    +      this.__location.href = val;
    +    } else {
    +      this.__location = val as any;
    +    }
    +  }
    +
    +  matchMedia(media: string) {
    +    return {
    +      media,
    +      matches: false,
    +      addListener: (_handler: (ev?: any) => void) => {},
    +      removeListener: (_handler: (ev?: any) => void) => {},
    +      addEventListener: (_type: string, _handler: (ev?: any) => void) => {},
    +      removeEventListener: (_type: string, _handler: (ev?: any) => void) => {},
    +      dispatchEvent: (_ev: any) => {},
    +      onchange: null as ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null,
    +    };
    +  }
    +
    +  get Node() {
    +    return MockNode;
    +  }
    +
    +  get NodeList() {
    +    if (this.__nodeListCstr == null) {
    +      const ownerDocument = this.document;
    +      this.__nodeListCstr = class extends MockNodeList {
    +        constructor() {
    +          super(ownerDocument, [], 0);
    +          throw new Error('Illegal constructor: cannot construct NodeList');
    +        }
    +      };
    +    }
    +    return this.__nodeListCstr;
    +  }
    +
    +  get navigator() {
    +    if (this.__navigator == null) {
    +      this.__navigator = new MockNavigator();
    +    }
    +    return this.__navigator;
    +  }
    +  set navigator(nav: any) {
    +    this.__navigator = nav;
    +  }
    +
    +  get parent(): any {
    +    return null;
    +  }
    +
    +  prompt() {
    +    return '';
    +  }
    +
    +  open(): any {
    +    return null;
    +  }
    +
    +  get origin() {
    +    return this.location.origin;
    +  }
    +
    +  removeEventListener(type: string, handler: any) {
    +    removeEventListener(this, type, handler);
    +  }
    +
    +  requestAnimationFrame(callback: (timestamp: number) => void) {
    +    return this.setTimeout(() => {
    +      callback(Date.now());
    +    }, 0) as number;
    +  }
    +
    +  requestIdleCallback(
    +    callback: (deadline: { didTimeout: boolean; timeRemaining: () => number }) => void,
    +  ) {
    +    return this.setTimeout(() => {
    +      callback({
    +        didTimeout: false,
    +        timeRemaining: () => 0,
    +      });
    +    }, 0);
    +  }
    +
    +  scroll(_x?: number, _y?: number) {
    +    /**/
    +  }
    +
    +  scrollBy(_x?: number, _y?: number) {
    +    /**/
    +  }
    +
    +  scrollTo(_x?: number, _y?: number) {
    +    /**/
    +  }
    +
    +  get self() {
    +    return this;
    +  }
    +
    +  get sessionStorage() {
    +    if (this.__sessionStorage == null) {
    +      this.__sessionStorage = new MockStorage();
    +    }
    +    return this.__sessionStorage;
    +  }
    +  set sessionStorage(locStorage: any) {
    +    this.__sessionStorage = locStorage;
    +  }
    +
    +  setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): number {
    +    if (this.__timeouts == null) {
    +      this.__timeouts = new Set();
    +    }
    +
    +    ms = Math.min(ms ?? 0, this.__maxTimeout);
    +
    +    if (this.__allowInterval) {
    +      const intervalId = this.__setInterval(() => {
    +        if (this.__timeouts) {
    +          this.__timeouts.delete(intervalId);
    +
    +          try {
    +            callback(...args);
    +          } catch (e) {
    +            if (this.console) {
    +              this.console.error(e);
    +            } else {
    +              console.error(e);
    +            }
    +          }
    +        }
    +      }, ms) as any;
    +
    +      if (this.__timeouts) {
    +        this.__timeouts.add(intervalId);
    +      }
    +
    +      return intervalId;
    +    }
    +
    +    const timeoutId = this.__setTimeout.call(
    +      nativeWindow || this,
    +      () => {
    +        if (this.__timeouts) {
    +          this.__timeouts.delete(timeoutId);
    +
    +          try {
    +            callback(...args);
    +          } catch (e) {
    +            if (this.console) {
    +              this.console.error(e);
    +            } else {
    +              console.error(e);
    +            }
    +          }
    +        }
    +      },
    +      ms,
    +    ) as any;
    +
    +    if (this.__timeouts) {
    +      this.__timeouts.add(timeoutId);
    +    }
    +
    +    return timeoutId;
    +  }
    +
    +  setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): number {
    +    if (this.__timeouts == null) {
    +      this.__timeouts = new Set();
    +    }
    +
    +    ms = Math.min(ms ?? 0, this.__maxTimeout);
    +
    +    const timeoutId = this.__setTimeout.call(
    +      nativeWindow || this,
    +      () => {
    +        if (this.__timeouts) {
    +          this.__timeouts.delete(timeoutId);
    +
    +          try {
    +            callback(...args);
    +          } catch (e) {
    +            if (this.console) {
    +              this.console.error(e);
    +            } else {
    +              console.error(e);
    +            }
    +          }
    +        }
    +      },
    +      ms,
    +    ) as any as number;
    +
    +    if (this.__timeouts) {
    +      this.__timeouts.add(timeoutId);
    +    }
    +
    +    return timeoutId;
    +  }
    +
    +  get top() {
    +    return this;
    +  }
    +
    +  get window() {
    +    return this;
    +  }
    +
    +  onanimationstart() {
    +    /**/
    +  }
    +  onanimationend() {
    +    /**/
    +  }
    +  onanimationiteration() {
    +    /**/
    +  }
    +  onabort() {
    +    /**/
    +  }
    +  onauxclick() {
    +    /**/
    +  }
    +  onbeforecopy() {
    +    /**/
    +  }
    +  onbeforecut() {
    +    /**/
    +  }
    +  onbeforepaste() {
    +    /**/
    +  }
    +  onblur() {
    +    /**/
    +  }
    +  oncancel() {
    +    /**/
    +  }
    +  oncanplay() {
    +    /**/
    +  }
    +  oncanplaythrough() {
    +    /**/
    +  }
    +  onchange() {
    +    /**/
    +  }
    +  onclick() {
    +    /**/
    +  }
    +  onclose() {
    +    /**/
    +  }
    +  oncontextmenu() {
    +    /**/
    +  }
    +  oncopy() {
    +    /**/
    +  }
    +  oncuechange() {
    +    /**/
    +  }
    +  oncut() {
    +    /**/
    +  }
    +  ondblclick() {
    +    /**/
    +  }
    +  ondrag() {
    +    /**/
    +  }
    +  ondragend() {
    +    /**/
    +  }
    +  ondragenter() {
    +    /**/
    +  }
    +  ondragleave() {
    +    /**/
    +  }
    +  ondragover() {
    +    /**/
    +  }
    +  ondragstart() {
    +    /**/
    +  }
    +  ondrop() {
    +    /**/
    +  }
    +  ondurationchange() {
    +    /**/
    +  }
    +  onemptied() {
    +    /**/
    +  }
    +  onended() {
    +    /**/
    +  }
    +  onerror() {
    +    /**/
    +  }
    +  onfocus() {
    +    /**/
    +  }
    +  onfocusin() {
    +    /**/
    +  }
    +  onfocusout() {
    +    /**/
    +  }
    +  onformdata() {
    +    /**/
    +  }
    +  onfullscreenchange() {
    +    /**/
    +  }
    +  onfullscreenerror() {
    +    /**/
    +  }
    +  ongotpointercapture() {
    +    /**/
    +  }
    +  oninput() {
    +    /**/
    +  }
    +  oninvalid() {
    +    /**/
    +  }
    +  onkeydown() {
    +    /**/
    +  }
    +  onkeypress() {
    +    /**/
    +  }
    +  onkeyup() {
    +    /**/
    +  }
    +  onload() {
    +    /**/
    +  }
    +  onloadeddata() {
    +    /**/
    +  }
    +  onloadedmetadata() {
    +    /**/
    +  }
    +  onloadstart() {
    +    /**/
    +  }
    +  onlostpointercapture() {
    +    /**/
    +  }
    +  onmousedown() {
    +    /**/
    +  }
    +  onmouseenter() {
    +    /**/
    +  }
    +  onmouseleave() {
    +    /**/
    +  }
    +  onmousemove() {
    +    /**/
    +  }
    +  onmouseout() {
    +    /**/
    +  }
    +  onmouseover() {
    +    /**/
    +  }
    +  onmouseup() {
    +    /**/
    +  }
    +  onmousewheel() {
    +    /**/
    +  }
    +  onpaste() {
    +    /**/
    +  }
    +  onpause() {
    +    /**/
    +  }
    +  onplay() {
    +    /**/
    +  }
    +  onplaying() {
    +    /**/
    +  }
    +  onpointercancel() {
    +    /**/
    +  }
    +  onpointerdown() {
    +    /**/
    +  }
    +  onpointerenter() {
    +    /**/
    +  }
    +  onpointerleave() {
    +    /**/
    +  }
    +  onpointermove() {
    +    /**/
    +  }
    +  onpointerout() {
    +    /**/
    +  }
    +  onpointerover() {
    +    /**/
    +  }
    +  onpointerup() {
    +    /**/
    +  }
    +  onprogress() {
    +    /**/
    +  }
    +  onratechange() {
    +    /**/
    +  }
    +  onreset() {
    +    /**/
    +  }
    +  onresize() {
    +    /**/
    +  }
    +  onscroll() {
    +    /**/
    +  }
    +  onsearch() {
    +    /**/
    +  }
    +  onseeked() {
    +    /**/
    +  }
    +  onseeking() {
    +    /**/
    +  }
    +  onselect() {
    +    /**/
    +  }
    +  onselectstart() {
    +    /**/
    +  }
    +  onstalled() {
    +    /**/
    +  }
    +  onsubmit() {
    +    /**/
    +  }
    +  onsuspend() {
    +    /**/
    +  }
    +  ontimeupdate() {
    +    /**/
    +  }
    +  ontoggle() {
    +    /**/
    +  }
    +  onvolumechange() {
    +    /**/
    +  }
    +  onwaiting() {
    +    /**/
    +  }
    +  onwebkitfullscreenchange() {
    +    /**/
    +  }
    +  onwebkitfullscreenerror() {
    +    /**/
    +  }
    +  onwheel() {
    +    /**/
    +  }
    +}
    +
    +addGlobalsToWindowPrototype(MockWindow.prototype);
    +
    +function resetWindowDefaults(win: MockWindow) {
    +  win.__clearInterval = nativeClearInterval;
    +  win.__clearTimeout = nativeClearTimeout;
    +  win.__setInterval = nativeSetInterval;
    +  win.__setTimeout = nativeSetTimeout;
    +  win.__maxTimeout = 60000;
    +  win.__allowInterval = true;
    +  win.URL = nativeURL;
    +}
    +
    +export function createWindow(html: string | boolean = null): Window {
    +  return new MockWindow(html) as any;
    +}
    +
    +export function cloneWindow(
    +  srcWin: Window | MockWindow,
    +  opts: { customElementProxy?: boolean } = {},
    +): MockWindow | null {
    +  if (srcWin == null) {
    +    return null;
    +  }
    +
    +  const clonedWin = new MockWindow(false);
    +  if (!opts.customElementProxy) {
    +    (srcWin as MockWindow).customElements = null;
    +  }
    +
    +  if (srcWin.document != null) {
    +    const clonedDoc = new MockDocument(false, clonedWin);
    +    clonedWin.document = clonedDoc as any;
    +    clonedDoc.documentElement = srcWin.document.documentElement.cloneNode(true) as any;
    +  } else {
    +    clonedWin.document = new MockDocument(null, clonedWin) as any;
    +  }
    +  return clonedWin;
    +}
    +
    +export function cloneDocument(srcDoc: Document) {
    +  if (srcDoc == null || !srcDoc.defaultView) {
    +    return null;
    +  }
    +
    +  const dstWin = cloneWindow(srcDoc.defaultView);
    +  return dstWin?.document || null;
    +}
    +
    +/**
    + * Constrain setTimeout() to 1ms, but still async. Also
    + * only allow setInterval() to fire once, also constrained to 1ms.
    + * @param win the mock window instance to update
    + */
    +export function constrainTimeouts(win: MockWindow) {
    +  win.__allowInterval = false;
    +  win.__maxTimeout = 0;
    +}
    +
    +function resetWindow(win: MockWindow) {
    +  if (win != null) {
    +    if (win.__timeouts) {
    +      win.__timeouts.forEach((timeoutId) => {
    +        nativeClearInterval(timeoutId);
    +        nativeClearTimeout(timeoutId);
    +      });
    +      win.__timeouts.clear();
    +    }
    +    if (win.customElements && (win.customElements as unknown as MockCustomElementRegistry).clear) {
    +      (win.customElements as unknown as MockCustomElementRegistry).clear();
    +    }
    +
    +    resetDocument(win.document);
    +    resetPerformance(win.performance);
    +
    +    for (const key in win) {
    +      if (
    +        win.hasOwnProperty(key) &&
    +        key !== 'document' &&
    +        key !== 'performance' &&
    +        key !== 'customElements'
    +      ) {
    +        delete (win as any)[key];
    +      }
    +    }
    +    resetWindowDefaults(win);
    +    resetWindowDimensions(win);
    +    resetEventListeners(win);
    +
    +    if (win.document != null) {
    +      try {
    +        (win.document as any).defaultView = win;
    +      } catch {}
    +    }
    +
    +    // ensure we don't hold onto nodeFetch values
    +    (win as any).fetch = null;
    +    (win as any).Headers = null;
    +    (win as any).Request = null;
    +    (win as any).Response = null;
    +    (win as any).FetchError = null;
    +  }
    +}
    +
    +function resetWindowDimensions(win: MockWindow) {
    +  try {
    +    win.devicePixelRatio = 1;
    +
    +    win.innerHeight = 768;
    +    win.innerWidth = 1366;
    +
    +    win.pageXOffset = 0;
    +    win.pageYOffset = 0;
    +
    +    win.screenLeft = 0;
    +    win.screenTop = 0;
    +    win.screenX = 0;
    +    win.screenY = 0;
    +    win.scrollX = 0;
    +    win.scrollY = 0;
    +
    +    win.screen = {
    +      availHeight: win.innerHeight,
    +      availLeft: 0,
    +      availTop: 0,
    +      availWidth: win.innerWidth,
    +      colorDepth: 24,
    +      height: win.innerHeight,
    +      keepAwake: false,
    +      orientation: {
    +        angle: 0,
    +        type: 'portrait-primary',
    +      } as any,
    +      pixelDepth: 24,
    +      width: win.innerWidth,
    +    } as any;
    +  } catch {}
    +}
    diff --git a/packages/mock-doc/tsconfig.json b/packages/mock-doc/tsconfig.json
    new file mode 100644
    index 00000000000..e4533edefb1
    --- /dev/null
    +++ b/packages/mock-doc/tsconfig.json
    @@ -0,0 +1,31 @@
    +{
    +  "compilerOptions": {
    +    "allowSyntheticDefaultImports": true,
    +    "alwaysStrict": true,
    +    "declaration": true,
    +    "emitDeclarationOnly": true,
    +    "declarationDir": "dist",
    +    "rootDir": "src",
    +    "outDir": "dist",
    +    "experimentalDecorators": true,
    +    "forceConsistentCasingInFileNames": true,
    +    "jsx": "react",
    +    "jsxFactory": "h",
    +    "jsxFragmentFactory": "Fragment",
    +    "lib": ["dom", "es2021"],
    +    "module": "esnext",
    +    "moduleResolution": "bundler",
    +    "noImplicitAny": true,
    +    "noImplicitOverride": true,
    +    "noImplicitReturns": true,
    +    "strictNullChecks": false,
    +    "strictPropertyInitialization": false,
    +    "resolveJsonModule": true,
    +    "skipLibCheck": true,
    +    "sourceMap": false,
    +    "target": "es2022",
    +    "types": ["node"]
    +  },
    +  "include": ["src/**/*.ts"],
    +  "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "dist"]
    +}
    diff --git a/packages/mock-doc/tsdown.config.ts b/packages/mock-doc/tsdown.config.ts
    new file mode 100644
    index 00000000000..d15d10431a1
    --- /dev/null
    +++ b/packages/mock-doc/tsdown.config.ts
    @@ -0,0 +1,15 @@
    +import { defineConfig } from 'tsdown';
    +
    +export default defineConfig({
    +  entry: ['src/index.ts'],
    +  outDir: 'dist',
    +  format: ['esm'],
    +  platform: 'node',
    +  target: 'node22',
    +  dts: true,
    +  clean: true,
    +  deps: {
    +    neverBundle: [/^node:/],
    +    alwaysBundle: ['nwsapi'],
    +  },
    +});
    diff --git a/packages/mock-doc/vitest.config.ts b/packages/mock-doc/vitest.config.ts
    new file mode 100644
    index 00000000000..062d76faf0a
    --- /dev/null
    +++ b/packages/mock-doc/vitest.config.ts
    @@ -0,0 +1,9 @@
    +import { defineConfig } from 'vitest/config';
    +
    +export default defineConfig({
    +  test: {
    +    // globals: true,
    +    include: ['src/**/*.spec.ts'],
    +    setupFiles: ['./vitest.setup.ts'],
    +  },
    +});
    diff --git a/packages/mock-doc/vitest.setup.ts b/packages/mock-doc/vitest.setup.ts
    new file mode 100644
    index 00000000000..9add12d3212
    --- /dev/null
    +++ b/packages/mock-doc/vitest.setup.ts
    @@ -0,0 +1,13 @@
    +import { beforeEach } from 'vitest';
    +
    +import { setupGlobal } from './src';
    +
    +// Set up mock-doc globals so that Event, CustomEvent, etc. use MockEvent, MockCustomEvent
    +setupGlobal(globalThis || global || {});
    +window.location.href = `http://testing.stenciljs.com/`;
    +
    +// Reset document body between tests
    +beforeEach(() => {
    +  document.body.innerHTML = '';
    +  document.head.innerHTML = '';
    +});
    diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
    new file mode 100644
    index 00000000000..386bd03b50c
    --- /dev/null
    +++ b/pnpm-lock.yaml
    @@ -0,0 +1,10283 @@
    +lockfileVersion: '9.0'
    +
    +settings:
    +  autoInstallPeers: true
    +  excludeLinksFromLockfile: false
    +
    +catalogs:
    +  default:
    +    '@playwright/test':
    +      specifier: ^1.52.0
    +      version: 1.58.2
    +    '@stencil/playwright':
    +      specifier: ~0.4.3
    +      version: 0.4.3
    +    '@stencil/vitest':
    +      specifier: ^1.11.6
    +      version: 1.11.6
    +    '@vitest/browser':
    +      specifier: ^4.1.1
    +      version: 4.1.2
    +    '@vitest/browser-playwright':
    +      specifier: ^4.1.2
    +      version: 4.1.2
    +    playwright:
    +      specifier: ^1.58.0
    +      version: 1.58.2
    +    tsdown:
    +      specifier: ^0.21.7
    +      version: 0.21.7
    +    typescript:
    +      specifier: '>4.0.0'
    +      version: 6.0.2
    +    vitest:
    +      specifier: ^4.1.1
    +      version: 4.1.2
    +    vitest-environment-stencil:
    +      specifier: ^1.11.6
    +      version: 1.11.6
    +
    +overrides:
    +  '@stencil/core': workspace:*
    +
    +importers:
    +
    +  .:
    +    devDependencies:
    +      '@changesets/cli':
    +        specifier: ^2.29.0
    +        version: 2.30.0(@types/node@24.10.13)
    +      '@types/node':
    +        specifier: ^24
    +        version: 24.10.13
    +      cspell:
    +        specifier: ^9.7.0
    +        version: 9.7.0
    +      knip:
    +        specifier: ^6.1.0
    +        version: 6.1.0
    +      oxfmt:
    +        specifier: ^0.42.0
    +        version: 0.42.0
    +      oxlint:
    +        specifier: ^1.57.0
    +        version: 1.57.0
    +
    +  packages/cli:
    +    dependencies:
    +      '@stencil/dev-server':
    +        specifier: workspace:*
    +        version: link:../dev-server
    +      prompts:
    +        specifier: ^2.4.2
    +        version: 2.4.2
    +    devDependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../core
    +      '@types/prompts':
    +        specifier: ^2.4.9
    +        version: 2.4.9
    +      tsdown:
    +        specifier: 'catalog:'
    +        version: 0.21.7(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@tsdown/css@0.21.6)(oxc-resolver@11.19.1)(typescript@6.0.2)
    +      typescript:
    +        specifier: 'catalog:'
    +        version: 6.0.2
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +
    +  packages/core:
    +    dependencies:
    +      '@parcel/watcher':
    +        specifier: ^2.5.1
    +        version: 2.5.6
    +      '@rollup/pluginutils':
    +        specifier: ^5.3.0
    +        version: 5.3.0(rollup@4.57.1)
    +      '@stencil/cli':
    +        specifier: workspace:*
    +        version: link:../cli
    +      '@stencil/dev-server':
    +        specifier: workspace:*
    +        version: link:../dev-server
    +      '@stencil/mock-doc':
    +        specifier: workspace:*
    +        version: link:../mock-doc
    +      browserslist:
    +        specifier: ^4.24.0
    +        version: 4.28.1
    +      chalk:
    +        specifier: ^5.6.2
    +        version: 5.6.2
    +      css-what:
    +        specifier: ^7.0.0
    +        version: 7.0.0
    +      jiti:
    +        specifier: ^2.6.1
    +        version: 2.6.1
    +      lightningcss:
    +        specifier: ^1.32.0
    +        version: 1.32.0
    +      magic-string:
    +        specifier: ^0.30.0
    +        version: 0.30.21
    +      picomatch:
    +        specifier: ^4.0.3
    +        version: 4.0.4
    +      postcss:
    +        specifier: ^8.5.6
    +        version: 8.5.6
    +      postcss-safe-parser:
    +        specifier: ^7.0.1
    +        version: 7.0.1(postcss@8.5.6)
    +      postcss-selector-parser:
    +        specifier: ^7.1.1
    +        version: 7.1.1
    +      resolve:
    +        specifier: ^1.22.0
    +        version: 1.22.11
    +      rolldown:
    +        specifier: ^1.0.0-rc.15
    +        version: 1.0.0-rc.15
    +      semver:
    +        specifier: ^7.7.4
    +        version: 7.7.4
    +      terser:
    +        specifier: 5.37.0
    +        version: 5.37.0
    +      tinyglobby:
    +        specifier: ^0.2.15
    +        version: 0.2.15
    +      typescript:
    +        specifier: 'catalog:'
    +        version: 6.0.2
    +    devDependencies:
    +      '@ionic/prettier-config':
    +        specifier: ^4.0.0
    +        version: 4.0.0(prettier@3.8.1)
    +      '@stencil/vitest':
    +        specifier: 'catalog:'
    +        version: 1.11.6(@stencil/mock-doc@packages+mock-doc)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +      prettier:
    +        specifier: ^3.5.0
    +        version: 3.8.1
    +      tsdown:
    +        specifier: 'catalog:'
    +        version: 0.21.7(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@tsdown/css@0.21.6)(oxc-resolver@11.19.1)(typescript@6.0.2)
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      vitest-environment-stencil:
    +        specifier: 'catalog:'
    +        version: 1.11.6(@stencil/mock-doc@packages+mock-doc)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +
    +  packages/dev-server:
    +    dependencies:
    +      launch-editor:
    +        specifier: ^2.9.1
    +        version: 2.13.0
    +      open:
    +        specifier: ^11.0.0
    +        version: 11.0.0
    +      ws:
    +        specifier: ^8.0.0
    +        version: 8.19.0
    +    devDependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../core
    +      '@tsdown/css':
    +        specifier: ^0.21.6
    +        version: 0.21.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(jiti@2.6.1)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(tsdown@0.21.7)(tsx@4.21.0)(yaml@2.8.3)
    +      '@types/ws':
    +        specifier: ^8.0.0
    +        version: 8.18.1
    +      tsdown:
    +        specifier: 'catalog:'
    +        version: 0.21.7(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@tsdown/css@0.21.6)(oxc-resolver@11.19.1)(typescript@6.0.2)
    +      typescript:
    +        specifier: 'catalog:'
    +        version: 6.0.2
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      vitest-environment-stencil:
    +        specifier: 'catalog:'
    +        version: 1.11.6(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +
    +  packages/mock-doc:
    +    dependencies:
    +      nwsapi:
    +        specifier: ^2.2.23
    +        version: 2.2.23
    +      parse5:
    +        specifier: 7.2.1
    +        version: 7.2.1
    +    devDependencies:
    +      '@stencil/vitest':
    +        specifier: 'catalog:'
    +        version: 1.11.6(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +      tsdown:
    +        specifier: 'catalog:'
    +        version: 0.21.7(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@tsdown/css@0.21.6)(oxc-resolver@11.19.1)(typescript@6.0.2)
    +      typescript:
    +        specifier: 'catalog:'
    +        version: 6.0.2
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +
    +  test/build: {}
    +
    +  test/build/build-size: {}
    +
    +  test/build/build-size/kitchen-sink:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../../packages/core
    +    devDependencies:
    +      rimraf:
    +        specifier: ^6.1.3
    +        version: 6.1.3
    +
    +  test/build/build-size/light:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../../packages/core
    +    devDependencies:
    +      rimraf:
    +        specifier: ^6.1.3
    +        version: 6.1.3
    +
    +  test/build/build-size/shadow:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../../packages/core
    +    devDependencies:
    +      rimraf:
    +        specifier: ^6.1.3
    +        version: 6.1.3
    +
    +  test/build/copy-task:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +    devDependencies:
    +      rimraf:
    +        specifier: ^6.0.1
    +        version: 6.1.3
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +
    +  test/build/docs-json:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +
    +  test/build/docs-readme:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +
    +  test/build/exports:
    +    devDependencies:
    +      '@stencil/cli':
    +        specifier: workspace:*
    +        version: link:../../../packages/cli
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +
    +  test/build/global-style:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +    devDependencies:
    +      rimraf:
    +        specifier: ^6.0.1
    +        version: 6.1.3
    +
    +  test/build/outputs:
    +    devDependencies:
    +      '@stencil/mock-doc':
    +        specifier: workspace:*
    +        version: link:../../../packages/mock-doc
    +      why-is-node-running:
    +        specifier: ^2.2.0
    +        version: 2.3.0
    +
    +  test/build/type-tests:
    +    devDependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +      typescript:
    +        specifier: 'catalog:'
    +        version: 6.0.2
    +
    +  test/integration: {}
    +
    +  test/integration/bundler:
    +    devDependencies:
    +      '@playwright/test':
    +        specifier: 'catalog:'
    +        version: 1.58.2
    +      serve:
    +        specifier: ^14.2.4
    +        version: 14.2.5
    +
    +  test/integration/bundler/component-library:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../../packages/core
    +
    +  test/integration/bundler/vite-bundle-test:
    +    dependencies:
    +      '@stencil-core-tests/bundler-component-library':
    +        specifier: workspace:*
    +        version: link:../component-library
    +    devDependencies:
    +      vite:
    +        specifier: ^6.0.0
    +        version: 6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3)
    +
    +  test/integration/e2e:
    +    devDependencies:
    +      '@playwright/test':
    +        specifier: 'catalog:'
    +        version: 1.58.2
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +      '@stencil/mock-doc':
    +        specifier: workspace:*
    +        version: link:../../../packages/mock-doc
    +      '@stencil/playwright':
    +        specifier: 'catalog:'
    +        version: 0.4.3(@playwright/test@1.58.2)(@stencil/core@packages+core)
    +      '@stencil/react-output-target':
    +        specifier: ^0.0.9
    +        version: 0.0.9(@stencil/core@packages+core)
    +      '@types/file-saver':
    +        specifier: ^2.0.1
    +        version: 2.0.7
    +      '@types/lodash':
    +        specifier: ^4.14.165
    +        version: 4.17.23
    +      '@types/lodash-es':
    +        specifier: ^4.17.4
    +        version: 4.17.12
    +      '@types/video.js':
    +        specifier: ^7.3.11
    +        version: 7.3.58
    +      file-saver:
    +        specifier: ^2.0.2
    +        version: 2.0.5
    +      linaria:
    +        specifier: ^1.3.3
    +        version: 1.4.1(@babel/core@7.29.0)
    +      lodash:
    +        specifier: ^4.17.20
    +        version: 4.17.23
    +      lodash-es:
    +        specifier: ^4.17.15
    +        version: 4.17.23
    +      rollup-plugin-css-only:
    +        specifier: ^2.1.0
    +        version: 2.1.0(rollup@4.57.1)
    +      rollup-plugin-node-builtins:
    +        specifier: ^2.1.2
    +        version: 2.1.2
    +      tsx:
    +        specifier: ^4.19.2
    +        version: 4.21.0
    +      video.js:
    +        specifier: ^7.10.2
    +        version: 7.21.7
    +      why-is-node-running:
    +        specifier: ^2.2.0
    +        version: 2.3.0
    +
    +  test/integration/ionic-app:
    +    dependencies:
    +      '@ionic/core':
    +        specifier: ^7.0.0
    +        version: 7.8.6
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +    devDependencies:
    +      '@playwright/test':
    +        specifier: 'catalog:'
    +        version: 1.58.2
    +      '@stencil/playwright':
    +        specifier: 'catalog:'
    +        version: 0.4.3(@playwright/test@1.58.2)(@stencil/core@packages+core)
    +      '@stencil/vitest':
    +        specifier: 'catalog:'
    +        version: 1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +
    +  test/integration/prerender:
    +    devDependencies:
    +      '@playwright/test':
    +        specifier: 'catalog:'
    +        version: 1.58.2
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +      serve:
    +        specifier: ^14.2.4
    +        version: 14.2.5
    +
    +  test/integration/ssr-wasm:
    +    devDependencies:
    +      '@extism/extism':
    +        specifier: 2.0.0-rc13
    +        version: 2.0.0-rc13
    +      '@extism/js-pdk':
    +        specifier: ^1.0.0
    +        version: 1.1.1
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +
    +  test/perf: {}
    +
    +  test/perf/build-benchmark: {}
    +
    +  test/perf/runtime-benchmark:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +    devDependencies:
    +      playwright:
    +        specifier: 'catalog:'
    +        version: 1.58.2
    +
    +  test/runtime:
    +    dependencies:
    +      '@stencil-core-tests/runtime-external':
    +        specifier: workspace:*
    +        version: link:fixtures/external-base-classes
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../packages/core
    +    devDependencies:
    +      '@stencil/sass':
    +        specifier: ^3.0.12
    +        version: 3.2.3(@stencil/core@packages+core)
    +      '@stencil/vitest':
    +        specifier: 'catalog:'
    +        version: 1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +      '@vitest/browser':
    +        specifier: 'catalog:'
    +        version: 4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      '@vitest/browser-playwright':
    +        specifier: 'catalog:'
    +        version: 4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      bootstrap:
    +        specifier: ^5.3.3
    +        version: 5.3.8(@popperjs/core@2.11.8)
    +      normalize.css:
    +        specifier: ^8.0.1
    +        version: 8.0.1
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +
    +  test/runtime/fixtures/external-base-classes:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../../packages/core
    +
    +  test/special-config: {}
    +
    +  test/special-config/assets-global-style:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +    devDependencies:
    +      '@playwright/test':
    +        specifier: 'catalog:'
    +        version: 1.58.2
    +      '@stencil/playwright':
    +        specifier: 'catalog:'
    +        version: 0.4.3(@playwright/test@1.58.2)(@stencil/core@packages+core)
    +      '@stencil/vitest':
    +        specifier: 'catalog:'
    +        version: 1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +      '@vitest/browser':
    +        specifier: 'catalog:'
    +        version: 4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      '@vitest/browser-playwright':
    +        specifier: 'catalog:'
    +        version: 4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +
    +  test/special-config/external-runtime:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +    devDependencies:
    +      '@stencil/vitest':
    +        specifier: 'catalog:'
    +        version: 1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +      '@vitest/browser':
    +        specifier: 'catalog:'
    +        version: 4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      '@vitest/browser-playwright':
    +        specifier: 'catalog:'
    +        version: 4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +
    +  test/special-config/invisible-prehydration:
    +    devDependencies:
    +      '@playwright/test':
    +        specifier: 'catalog:'
    +        version: 1.58.2
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +      '@stencil/playwright':
    +        specifier: 'catalog:'
    +        version: 0.4.3(@playwright/test@1.58.2)(@stencil/core@packages+core)
    +
    +  test/special-config/slot-patching:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +    devDependencies:
    +      '@stencil/vitest':
    +        specifier: 'catalog:'
    +        version: 1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +      '@vitest/browser':
    +        specifier: 'catalog:'
    +        version: 4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      '@vitest/browser-playwright':
    +        specifier: 'catalog:'
    +        version: 4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      vitest:
    +        specifier: 'catalog:'
    +        version: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +
    +  test/special-config/standalone-devmode:
    +    dependencies:
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../../packages/core
    +
    +  test/ssr:
    +    devDependencies:
    +      '@playwright/test':
    +        specifier: 'catalog:'
    +        version: 1.58.2
    +      '@stencil/core':
    +        specifier: workspace:*
    +        version: link:../../packages/core
    +      '@stencil/playwright':
    +        specifier: 'catalog:'
    +        version: 0.4.3(@playwright/test@1.58.2)(@stencil/core@packages+core)
    +      file-saver:
    +        specifier: ^2.0.5
    +        version: 2.0.5
    +
    +packages:
    +
    +  '@acemir/cssom@0.9.31':
    +    resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}
    +
    +  '@asamuzakjp/css-color@4.1.2':
    +    resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==}
    +
    +  '@asamuzakjp/dom-selector@6.7.8':
    +    resolution: {integrity: sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==}
    +
    +  '@asamuzakjp/nwsapi@2.3.9':
    +    resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
    +
    +  '@babel/code-frame@7.29.0':
    +    resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/compat-data@7.29.0':
    +    resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/core@7.29.0':
    +    resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/generator@7.29.1':
    +    resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/generator@8.0.0-rc.3':
    +    resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +
    +  '@babel/helper-compilation-targets@7.28.6':
    +    resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/helper-define-polyfill-provider@0.6.6':
    +    resolution: {integrity: sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==}
    +    peerDependencies:
    +      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
    +
    +  '@babel/helper-globals@7.28.0':
    +    resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/helper-module-imports@7.28.6':
    +    resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/helper-module-transforms@7.28.6':
    +    resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
    +    engines: {node: '>=6.9.0'}
    +    peerDependencies:
    +      '@babel/core': ^7.0.0
    +
    +  '@babel/helper-plugin-utils@7.28.6':
    +    resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/helper-string-parser@7.27.1':
    +    resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/helper-string-parser@8.0.0-rc.3':
    +    resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +
    +  '@babel/helper-validator-identifier@7.28.5':
    +    resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/helper-validator-identifier@8.0.0-rc.3':
    +    resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +
    +  '@babel/helper-validator-option@7.27.1':
    +    resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/helpers@7.28.6':
    +    resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/parser@7.29.0':
    +    resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
    +    engines: {node: '>=6.0.0'}
    +    hasBin: true
    +
    +  '@babel/parser@8.0.0-rc.3':
    +    resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    hasBin: true
    +
    +  '@babel/plugin-proposal-export-namespace-from@7.18.9':
    +    resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
    +    engines: {node: '>=6.9.0'}
    +    deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.
    +    peerDependencies:
    +      '@babel/core': ^7.0.0-0
    +
    +  '@babel/plugin-syntax-dynamic-import@7.8.3':
    +    resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
    +    peerDependencies:
    +      '@babel/core': ^7.0.0-0
    +
    +  '@babel/plugin-syntax-export-namespace-from@7.8.3':
    +    resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
    +    peerDependencies:
    +      '@babel/core': ^7.0.0-0
    +
    +  '@babel/plugin-transform-modules-commonjs@7.28.6':
    +    resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==}
    +    engines: {node: '>=6.9.0'}
    +    peerDependencies:
    +      '@babel/core': ^7.0.0-0
    +
    +  '@babel/plugin-transform-runtime@7.29.0':
    +    resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==}
    +    engines: {node: '>=6.9.0'}
    +    peerDependencies:
    +      '@babel/core': ^7.0.0-0
    +
    +  '@babel/plugin-transform-template-literals@7.27.1':
    +    resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==}
    +    engines: {node: '>=6.9.0'}
    +    peerDependencies:
    +      '@babel/core': ^7.0.0-0
    +
    +  '@babel/runtime@7.28.6':
    +    resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/template@7.28.6':
    +    resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/traverse@7.29.0':
    +    resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/types@7.29.0':
    +    resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
    +    engines: {node: '>=6.9.0'}
    +
    +  '@babel/types@8.0.0-rc.3':
    +    resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +
    +  '@blazediff/core@1.9.1':
    +    resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==}
    +
    +  '@bufbuild/protobuf@2.11.0':
    +    resolution: {integrity: sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==}
    +
    +  '@changesets/apply-release-plan@7.1.0':
    +    resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==}
    +
    +  '@changesets/assemble-release-plan@6.0.9':
    +    resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==}
    +
    +  '@changesets/changelog-git@0.2.1':
    +    resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
    +
    +  '@changesets/cli@2.30.0':
    +    resolution: {integrity: sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==}
    +    hasBin: true
    +
    +  '@changesets/config@3.1.3':
    +    resolution: {integrity: sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==}
    +
    +  '@changesets/errors@0.2.0':
    +    resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==}
    +
    +  '@changesets/get-dependents-graph@2.1.3':
    +    resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==}
    +
    +  '@changesets/get-release-plan@4.0.15':
    +    resolution: {integrity: sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==}
    +
    +  '@changesets/get-version-range-type@0.4.0':
    +    resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==}
    +
    +  '@changesets/git@3.0.4':
    +    resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==}
    +
    +  '@changesets/logger@0.1.1':
    +    resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==}
    +
    +  '@changesets/parse@0.4.3':
    +    resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==}
    +
    +  '@changesets/pre@2.0.2':
    +    resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==}
    +
    +  '@changesets/read@0.6.7':
    +    resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==}
    +
    +  '@changesets/should-skip-package@0.1.2':
    +    resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==}
    +
    +  '@changesets/types@4.1.0':
    +    resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==}
    +
    +  '@changesets/types@6.1.0':
    +    resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==}
    +
    +  '@changesets/write@0.4.0':
    +    resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
    +
    +  '@cspell/cspell-bundled-dicts@9.7.0':
    +    resolution: {integrity: sha512-s7h1vo++Q3AsfQa3cs0u/KGwm3SYInuIlC4kjlCBWjQmb4KddiZB5O1u0+3TlA7GycHb5M4CR7MDfHUICgJf+w==}
    +    engines: {node: '>=20'}
    +
    +  '@cspell/cspell-json-reporter@9.7.0':
    +    resolution: {integrity: sha512-6xpGXlMtQA3hV2BCAQcPkpx9eI12I0o01i9eRqSSEDKtxuAnnrejbcCpL+5OboAjTp3/BSeNYSnhuWYLkSITWQ==}
    +    engines: {node: '>=20'}
    +
    +  '@cspell/cspell-performance-monitor@9.7.0':
    +    resolution: {integrity: sha512-w1PZIFXuvjnC6mQHyYAFnrsn5MzKnEcEkcK1bj4OG00bAt7WX2VUA/eNNt9c1iHozCQ+FcRYlfbGxuBmNyzSgw==}
    +    engines: {node: '>=20.18'}
    +
    +  '@cspell/cspell-pipe@9.7.0':
    +    resolution: {integrity: sha512-iiisyRpJciU9SOHNSi0ZEK0pqbEMFRatI/R4O+trVKb+W44p4MNGClLVRWPGUmsFbZKPJL3jDtz0wPlG0/JCZA==}
    +    engines: {node: '>=20'}
    +
    +  '@cspell/cspell-resolver@9.7.0':
    +    resolution: {integrity: sha512-uiEgS238mdabDnwavo6HXt8K98jlh/jpm7NONroM9NTr9rzck2VZKD2kXEj85wDNMtRsRXNoywTjwQ8WTB6/+w==}
    +    engines: {node: '>=20'}
    +
    +  '@cspell/cspell-service-bus@9.7.0':
    +    resolution: {integrity: sha512-fkqtaCkg4jY/FotmzjhIavbXuH0AgUJxZk78Ktf4XlhqOZ4wDeUWrCf220bva4mh3TWiLx/ae9lIlpl59Vx6hA==}
    +    engines: {node: '>=20'}
    +
    +  '@cspell/cspell-types@9.7.0':
    +    resolution: {integrity: sha512-Tdfx4eH2uS+gv9V9NCr3Rz+c7RSS6ntXp3Blliud18ibRUlRxO9dTaOjG4iv4x0nAmMeedP1ORkEpeXSkh2QiQ==}
    +    engines: {node: '>=20'}
    +
    +  '@cspell/cspell-worker@9.7.0':
    +    resolution: {integrity: sha512-cjEApFF0aOAa1vTUk+e7xP8ofK7iC7hsRzj1FmvvVQz8PoLWPRaq+1bT89ypPsZQvavqm5sIgb97S60/aW4TVg==}
    +    engines: {node: '>=20.18'}
    +
    +  '@cspell/dict-ada@4.1.1':
    +    resolution: {integrity: sha512-E+0YW9RhZod/9Qy2gxfNZiHJjCYFlCdI69br1eviQQWB8yOTJX0JHXLs79kOYhSW0kINPVUdvddEBe6Lu6CjGQ==}
    +
    +  '@cspell/dict-al@1.1.1':
    +    resolution: {integrity: sha512-sD8GCaZetgQL4+MaJLXqbzWcRjfKVp8x+px3HuCaaiATAAtvjwUQ5/Iubiqwfd1boIh2Y1/3EgM3TLQ7Q8e0wQ==}
    +
    +  '@cspell/dict-aws@4.0.17':
    +    resolution: {integrity: sha512-ORcblTWcdlGjIbWrgKF+8CNEBQiLVKdUOFoTn0KPNkAYnFcdPP0muT4892h7H4Xafh3j72wqB4/loQ6Nti9E/w==}
    +
    +  '@cspell/dict-bash@4.2.2':
    +    resolution: {integrity: sha512-kyWbwtX3TsCf5l49gGQIZkRLaB/P8g73GDRm41Zu8Mv51kjl2H7Au0TsEvHv7jzcsRLS6aUYaZv6Zsvk1fOz+Q==}
    +
    +  '@cspell/dict-companies@3.2.11':
    +    resolution: {integrity: sha512-0cmafbcz2pTHXLd59eLR1gvDvN6aWAOM0+cIL4LLF9GX9yB2iKDNrKsvs4tJRqutoaTdwNFBbV0FYv+6iCtebQ==}
    +
    +  '@cspell/dict-cpp@7.0.2':
    +    resolution: {integrity: sha512-dfbeERiVNeqmo/npivdR6rDiBCqZi3QtjH2Z0HFcXwpdj6i97dX1xaKyK2GUsO/p4u1TOv63Dmj5Vm48haDpuA==}
    +
    +  '@cspell/dict-cryptocurrencies@5.0.5':
    +    resolution: {integrity: sha512-R68hYYF/rtlE6T/dsObStzN5QZw+0aQBinAXuWCVqwdS7YZo0X33vGMfChkHaiCo3Z2+bkegqHlqxZF4TD3rUA==}
    +
    +  '@cspell/dict-csharp@4.0.8':
    +    resolution: {integrity: sha512-qmk45pKFHSxckl5mSlbHxmDitSsGMlk/XzFgt7emeTJWLNSTUK//MbYAkBNRtfzB4uD7pAFiKgpKgtJrTMRnrQ==}
    +
    +  '@cspell/dict-css@4.1.1':
    +    resolution: {integrity: sha512-y/Vgo6qY08e1t9OqR56qjoFLBCpi4QfWMf2qzD1l9omRZwvSMQGRPz4x0bxkkkU4oocMAeztjzCsmLew//c/8w==}
    +
    +  '@cspell/dict-dart@2.3.2':
    +    resolution: {integrity: sha512-sUiLW56t9gfZcu8iR/5EUg+KYyRD83Cjl3yjDEA2ApVuJvK1HhX+vn4e4k4YfjpUQMag8XO2AaRhARE09+/rqw==}
    +
    +  '@cspell/dict-data-science@2.0.13':
    +    resolution: {integrity: sha512-l1HMEhBJkPmw4I2YGVu2eBSKM89K9pVF+N6qIr5Uo5H3O979jVodtuwP8I7LyPrJnC6nz28oxeGRCLh9xC5CVA==}
    +
    +  '@cspell/dict-django@4.1.6':
    +    resolution: {integrity: sha512-SdbSFDGy9ulETqNz15oWv2+kpWLlk8DJYd573xhIkeRdcXOjskRuxjSZPKfW7O3NxN/KEf3gm3IevVOiNuFS+w==}
    +
    +  '@cspell/dict-docker@1.1.17':
    +    resolution: {integrity: sha512-OcnVTIpHIYYKhztNTyK8ShAnXTfnqs43hVH6p0py0wlcwRIXe5uj4f12n7zPf2CeBI7JAlPjEsV0Rlf4hbz/xQ==}
    +
    +  '@cspell/dict-dotnet@5.0.13':
    +    resolution: {integrity: sha512-xPp7jMnFpOri7tzmqmm/dXMolXz1t2bhNqxYkOyMqXhvs08oc7BFs+EsbDY0X7hqiISgeFZGNqn0dOCr+ncPYw==}
    +
    +  '@cspell/dict-elixir@4.0.8':
    +    resolution: {integrity: sha512-CyfphrbMyl4Ms55Vzuj+mNmd693HjBFr9hvU+B2YbFEZprE5AG+EXLYTMRWrXbpds4AuZcvN3deM2XVB80BN/Q==}
    +
    +  '@cspell/dict-en-common-misspellings@2.1.12':
    +    resolution: {integrity: sha512-14Eu6QGqyksqOd4fYPuRb58lK1Va7FQK9XxFsRKnZU8LhL3N+kj7YKDW+7aIaAN/0WGEqslGP6lGbQzNti8Akw==}
    +
    +  '@cspell/dict-en-gb-mit@3.1.22':
    +    resolution: {integrity: sha512-xE5Vg6gGdMkZ1Ep6z9SJMMioGkkT1GbxS5Mm0U3Ey1/H68P0G7cJcyiVr1CARxFbLqKE4QUpoV1o6jz1Z5Yl9Q==}
    +
    +  '@cspell/dict-en_us@4.4.33':
    +    resolution: {integrity: sha512-zWftVqfUStDA37wO1ZNDN1qMJOfcxELa8ucHW8W8wBAZY3TK5Nb6deLogCK/IJi/Qljf30dwwuqqv84Qqle9Tw==}
    +
    +  '@cspell/dict-filetypes@3.0.18':
    +    resolution: {integrity: sha512-yU7RKD/x1IWmDLzWeiItMwgV+6bUcU/af23uS0+uGiFUbsY1qWV/D4rxlAAO6Z7no3J2z8aZOkYIOvUrJq0Rcw==}
    +
    +  '@cspell/dict-flutter@1.1.1':
    +    resolution: {integrity: sha512-UlOzRcH2tNbFhZmHJN48Za/2/MEdRHl2BMkCWZBYs+30b91mWvBfzaN4IJQU7dUZtowKayVIF9FzvLZtZokc5A==}
    +
    +  '@cspell/dict-fonts@4.0.6':
    +    resolution: {integrity: sha512-aR/0csY01dNb0A1tw/UmN9rKgHruUxsYsvXu6YlSBJFu60s26SKr/k1o4LavpHTQ+lznlYMqAvuxGkE4Flliqw==}
    +
    +  '@cspell/dict-fsharp@1.1.1':
    +    resolution: {integrity: sha512-imhs0u87wEA4/cYjgzS0tAyaJpwG7vwtC8UyMFbwpmtw+/bgss+osNfyqhYRyS/ehVCWL17Ewx2UPkexjKyaBA==}
    +
    +  '@cspell/dict-fullstack@3.2.9':
    +    resolution: {integrity: sha512-diZX+usW5aZ4/b2T0QM/H/Wl9aNMbdODa1Jq0ReBr/jazmNeWjd+PyqeVgzd1joEaHY+SAnjrf/i9CwKd2ZtWQ==}
    +
    +  '@cspell/dict-gaming-terms@1.1.2':
    +    resolution: {integrity: sha512-9XnOvaoTBscq0xuD6KTEIkk9hhdfBkkvJAIsvw3JMcnp1214OCGW8+kako5RqQ2vTZR3Tnf3pc57o7VgkM0q1Q==}
    +
    +  '@cspell/dict-git@3.1.0':
    +    resolution: {integrity: sha512-KEt9zGkxqGy2q1nwH4CbyqTSv5nadpn8BAlDnzlRcnL0Xb3LX9xTgSGShKvzb0bw35lHoYyLWN2ZKAqbC4pgGQ==}
    +
    +  '@cspell/dict-golang@6.0.26':
    +    resolution: {integrity: sha512-YKA7Xm5KeOd14v5SQ4ll6afe9VSy3a2DWM7L9uBq4u3lXToRBQ1W5PRa+/Q9udd+DTURyVVnQ+7b9cnOlNxaRg==}
    +
    +  '@cspell/dict-google@1.0.9':
    +    resolution: {integrity: sha512-biL65POqialY0i4g6crj7pR6JnBkbsPovB2WDYkj3H4TuC/QXv7Pu5pdPxeUJA6TSCHI7T5twsO4VSVyRxD9CA==}
    +
    +  '@cspell/dict-haskell@4.0.6':
    +    resolution: {integrity: sha512-ib8SA5qgftExpYNjWhpYIgvDsZ/0wvKKxSP+kuSkkak520iPvTJumEpIE+qPcmJQo4NzdKMN8nEfaeci4OcFAQ==}
    +
    +  '@cspell/dict-html-symbol-entities@4.0.5':
    +    resolution: {integrity: sha512-429alTD4cE0FIwpMucvSN35Ld87HCyuM8mF731KU5Rm4Je2SG6hmVx7nkBsLyrmH3sQukTcr1GaiZsiEg8svPA==}
    +
    +  '@cspell/dict-html@4.0.15':
    +    resolution: {integrity: sha512-GJYnYKoD9fmo2OI0aySEGZOjThnx3upSUvV7mmqUu8oG+mGgzqm82P/f7OqsuvTaInZZwZbo+PwJQd/yHcyFIw==}
    +
    +  '@cspell/dict-java@5.0.12':
    +    resolution: {integrity: sha512-qPSNhTcl7LGJ5Qp6VN71H8zqvRQK04S08T67knMq9hTA8U7G1sTKzLmBaDOFhq17vNX/+rT+rbRYp+B5Nwza1A==}
    +
    +  '@cspell/dict-julia@1.1.1':
    +    resolution: {integrity: sha512-WylJR9TQ2cgwd5BWEOfdO3zvDB+L7kYFm0I9u0s9jKHWQ6yKmfKeMjU9oXxTBxIufhCXm92SKwwVNAC7gjv+yA==}
    +
    +  '@cspell/dict-k8s@1.0.12':
    +    resolution: {integrity: sha512-2LcllTWgaTfYC7DmkMPOn9GsBWsA4DZdlun4po8s2ysTP7CPEnZc1ZfK6pZ2eI4TsZemlUQQ+NZxMe9/QutQxg==}
    +
    +  '@cspell/dict-kotlin@1.1.1':
    +    resolution: {integrity: sha512-J3NzzfgmxRvEeOe3qUXnSJQCd38i/dpF9/t3quuWh6gXM+krsAXP75dY1CzDmS8mrJAlBdVBeAW5eAZTD8g86Q==}
    +
    +  '@cspell/dict-latex@5.1.0':
    +    resolution: {integrity: sha512-qxT4guhysyBt0gzoliXYEBYinkAdEtR2M7goRaUH0a7ltCsoqqAeEV8aXYRIdZGcV77gYSobvu3jJL038tlPAw==}
    +
    +  '@cspell/dict-lorem-ipsum@4.0.5':
    +    resolution: {integrity: sha512-9a4TJYRcPWPBKkQAJ/whCu4uCAEgv/O2xAaZEI0n4y1/l18Yyx8pBKoIX5QuVXjjmKEkK7hi5SxyIsH7pFEK9Q==}
    +
    +  '@cspell/dict-lua@4.0.8':
    +    resolution: {integrity: sha512-N4PkgNDMu9JVsRu7JBS/3E/dvfItRgk9w5ga2dKq+JupP2Y3lojNaAVFhXISh4Y0a6qXDn2clA6nvnavQ/jjLA==}
    +
    +  '@cspell/dict-makefile@1.0.5':
    +    resolution: {integrity: sha512-4vrVt7bGiK8Rx98tfRbYo42Xo2IstJkAF4tLLDMNQLkQ86msDlYSKG1ZCk8Abg+EdNcFAjNhXIiNO+w4KflGAQ==}
    +
    +  '@cspell/dict-markdown@2.0.16':
    +    resolution: {integrity: sha512-976RRqKv6cwhrxdFCQP2DdnBVB86BF57oQtPHy4Zbf4jF/i2Oy29MCrxirnOBalS1W6KQeto7NdfDXRAwkK4PQ==}
    +    peerDependencies:
    +      '@cspell/dict-css': ^4.1.1
    +      '@cspell/dict-html': ^4.0.15
    +      '@cspell/dict-html-symbol-entities': ^4.0.5
    +      '@cspell/dict-typescript': ^3.2.3
    +
    +  '@cspell/dict-monkeyc@1.0.12':
    +    resolution: {integrity: sha512-MN7Vs11TdP5mbdNFQP5x2Ac8zOBm97ARg6zM5Sb53YQt/eMvXOMvrep7+/+8NJXs0jkp70bBzjqU4APcqBFNAw==}
    +
    +  '@cspell/dict-node@5.0.9':
    +    resolution: {integrity: sha512-hO+ga+uYZ/WA4OtiMEyKt5rDUlUyu3nXMf8KVEeqq2msYvAPdldKBGH7lGONg6R/rPhv53Rb+0Y1SLdoK1+7wQ==}
    +
    +  '@cspell/dict-npm@5.2.38':
    +    resolution: {integrity: sha512-21ucGRPYYhr91C2cDBoMPTrcIOStQv33xOqJB0JLoC5LAs2Sfj9EoPGhGb+gIFVHz6Ia7JQWE2SJsOVFJD1wmg==}
    +
    +  '@cspell/dict-php@4.1.1':
    +    resolution: {integrity: sha512-EXelI+4AftmdIGtA8HL8kr4WlUE11OqCSVlnIgZekmTkEGSZdYnkFdiJ5IANSALtlQ1mghKjz+OFqVs6yowgWA==}
    +
    +  '@cspell/dict-powershell@5.0.15':
    +    resolution: {integrity: sha512-l4S5PAcvCFcVDMJShrYD0X6Huv9dcsQPlsVsBGbH38wvuN7gS7+GxZFAjTNxDmTY1wrNi1cCatSg6Pu2BW4rgg==}
    +
    +  '@cspell/dict-public-licenses@2.0.16':
    +    resolution: {integrity: sha512-EQRrPvEOmwhwWezV+W7LjXbIBjiy6y/shrET6Qcpnk3XANTzfvWflf9PnJ5kId/oKWvihFy0za0AV1JHd03pSQ==}
    +
    +  '@cspell/dict-python@4.2.26':
    +    resolution: {integrity: sha512-hbjN6BjlSgZOG2dA2DtvYNGBM5Aq0i0dHaZjMOI9K/9vRicVvKbcCiBSSrR3b+jwjhQL5ff7HwG5xFaaci0GQA==}
    +
    +  '@cspell/dict-r@2.1.1':
    +    resolution: {integrity: sha512-71Ka+yKfG4ZHEMEmDxc6+blFkeTTvgKbKAbwiwQAuKl3zpqs1Y0vUtwW2N4b3LgmSPhV3ODVY0y4m5ofqDuKMw==}
    +
    +  '@cspell/dict-ruby@5.1.1':
    +    resolution: {integrity: sha512-LHrp84oEV6q1ZxPPyj4z+FdKyq1XAKYPtmGptrd+uwHbrF/Ns5+fy6gtSi7pS+uc0zk3JdO9w/tPK+8N1/7WUA==}
    +
    +  '@cspell/dict-rust@4.1.2':
    +    resolution: {integrity: sha512-O1FHrumYcO+HZti3dHfBPUdnDFkI+nbYK3pxYmiM1sr+G0ebOd6qchmswS0Wsc6ZdEVNiPYJY/gZQR6jfW3uOg==}
    +
    +  '@cspell/dict-scala@5.0.9':
    +    resolution: {integrity: sha512-AjVcVAELgllybr1zk93CJ5wSUNu/Zb5kIubymR/GAYkMyBdYFCZ3Zbwn4Zz8GJlFFAbazABGOu0JPVbeY59vGg==}
    +
    +  '@cspell/dict-shell@1.1.2':
    +    resolution: {integrity: sha512-WqOUvnwcHK1X61wAfwyXq04cn7KYyskg90j4lLg3sGGKMW9Sq13hs91pqrjC44Q+lQLgCobrTkMDw9Wyl9nRFA==}
    +
    +  '@cspell/dict-software-terms@5.2.2':
    +    resolution: {integrity: sha512-0CaYd6TAsKtEoA7tNswm1iptEblTzEe3UG8beG2cpSTHk7afWIVMtJLgXDv0f/Li67Lf3Z1Jf3JeXR7GsJ2TRw==}
    +
    +  '@cspell/dict-sql@2.2.1':
    +    resolution: {integrity: sha512-qDHF8MpAYCf4pWU8NKbnVGzkoxMNrFqBHyG/dgrlic5EQiKANCLELYtGlX5auIMDLmTf1inA0eNtv74tyRJ/vg==}
    +
    +  '@cspell/dict-svelte@1.0.7':
    +    resolution: {integrity: sha512-hGZsGqP0WdzKkdpeVLBivRuSNzOTvN036EBmpOwxH+FTY2DuUH7ecW+cSaMwOgmq5JFSdTcbTNFlNC8HN8lhaQ==}
    +
    +  '@cspell/dict-swift@2.0.6':
    +    resolution: {integrity: sha512-PnpNbrIbex2aqU1kMgwEKvCzgbkHtj3dlFLPMqW1vSniop7YxaDTtvTUO4zA++ugYAEL+UK8vYrBwDPTjjvSnA==}
    +
    +  '@cspell/dict-terraform@1.1.3':
    +    resolution: {integrity: sha512-gr6wxCydwSFyyBKhBA2xkENXtVFToheqYYGFvlMZXWjviynXmh+NK/JTvTCk/VHk3+lzbO9EEQKee6VjrAUSbA==}
    +
    +  '@cspell/dict-typescript@3.2.3':
    +    resolution: {integrity: sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==}
    +
    +  '@cspell/dict-vue@3.0.5':
    +    resolution: {integrity: sha512-Mqutb8jbM+kIcywuPQCCaK5qQHTdaByoEO2J9LKFy3sqAdiBogNkrplqUK0HyyRFgCfbJUgjz3N85iCMcWH0JA==}
    +
    +  '@cspell/dict-zig@1.0.0':
    +    resolution: {integrity: sha512-XibBIxBlVosU06+M6uHWkFeT0/pW5WajDRYdXG2CgHnq85b0TI/Ks0FuBJykmsgi2CAD3Qtx8UHFEtl/DSFnAQ==}
    +
    +  '@cspell/dynamic-import@9.7.0':
    +    resolution: {integrity: sha512-Ws36IYvtS/8IN3x6K9dPLvTmaArodRJmzTn2Rkf2NaTnIYWhRuFzsP3SVVO59NN3fXswAEbmz5DSbVUe8bPZHg==}
    +    engines: {node: '>=20'}
    +
    +  '@cspell/filetypes@9.7.0':
    +    resolution: {integrity: sha512-Ln9e/8wGOyTeL3DCCs6kwd18TSpTw3kxsANjTrzLDASrX4cNmAdvc9J5dcIuBHPaqOAnRQxuZbzUlpRh73Y24w==}
    +    engines: {node: '>=20'}
    +
    +  '@cspell/rpc@9.7.0':
    +    resolution: {integrity: sha512-VnZ4ABgQeoS4RwofcePkDP7L6tf3Kh5D7LQKoyRM4R6XtfSsYefym6XKaRl3saGtthH5YyjgNJ0Tgdjen4wAAw==}
    +    engines: {node: '>=20.18'}
    +
    +  '@cspell/strong-weak-map@9.7.0':
    +    resolution: {integrity: sha512-5xbvDASjklrmy88O6gmGXgYhpByCXqOj5wIgyvwZe2l83T1bE+iOfGI4pGzZJ/mN+qTn1DNKq8BPBPtDgb7Q2Q==}
    +    engines: {node: '>=20'}
    +
    +  '@cspell/url@9.7.0':
    +    resolution: {integrity: sha512-ZaaBr0pTvNxmyUbIn+nVPXPr383VqJzfUDMWicgTjJIeo2+T2hOq2kNpgpvTIrWtZrsZnSP8oXms1+sKTjcvkw==}
    +    engines: {node: '>=20'}
    +
    +  '@csstools/color-helpers@6.0.1':
    +    resolution: {integrity: sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==}
    +    engines: {node: '>=20.19.0'}
    +
    +  '@csstools/css-calc@3.1.0':
    +    resolution: {integrity: sha512-JWouqB5za07FUA2iXZWq4gPXNGWXjRwlfwEXNr7cSsGr7OKgzhDVwkJjlsrbqSyFmDGSi1Rt7zs8ln87jX9yRg==}
    +    engines: {node: '>=20.19.0'}
    +    peerDependencies:
    +      '@csstools/css-parser-algorithms': ^4.0.0
    +      '@csstools/css-tokenizer': ^4.0.0
    +
    +  '@csstools/css-color-parser@4.0.1':
    +    resolution: {integrity: sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==}
    +    engines: {node: '>=20.19.0'}
    +    peerDependencies:
    +      '@csstools/css-parser-algorithms': ^4.0.0
    +      '@csstools/css-tokenizer': ^4.0.0
    +
    +  '@csstools/css-parser-algorithms@4.0.0':
    +    resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
    +    engines: {node: '>=20.19.0'}
    +    peerDependencies:
    +      '@csstools/css-tokenizer': ^4.0.0
    +
    +  '@csstools/css-syntax-patches-for-csstree@1.0.27':
    +    resolution: {integrity: sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==}
    +
    +  '@csstools/css-tokenizer@4.0.0':
    +    resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
    +    engines: {node: '>=20.19.0'}
    +
    +  '@emnapi/core@1.8.1':
    +    resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
    +
    +  '@emnapi/core@1.9.2':
    +    resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
    +
    +  '@emnapi/runtime@1.8.1':
    +    resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
    +
    +  '@emnapi/runtime@1.9.2':
    +    resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
    +
    +  '@emnapi/wasi-threads@1.1.0':
    +    resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
    +
    +  '@emnapi/wasi-threads@1.2.1':
    +    resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
    +
    +  '@emotion/is-prop-valid@0.8.8':
    +    resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
    +
    +  '@emotion/memoize@0.7.4':
    +    resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
    +
    +  '@esbuild/aix-ppc64@0.25.12':
    +    resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
    +    engines: {node: '>=18'}
    +    cpu: [ppc64]
    +    os: [aix]
    +
    +  '@esbuild/aix-ppc64@0.27.3':
    +    resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
    +    engines: {node: '>=18'}
    +    cpu: [ppc64]
    +    os: [aix]
    +
    +  '@esbuild/android-arm64@0.25.12':
    +    resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@esbuild/android-arm64@0.27.3':
    +    resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@esbuild/android-arm@0.25.12':
    +    resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm]
    +    os: [android]
    +
    +  '@esbuild/android-arm@0.27.3':
    +    resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
    +    engines: {node: '>=18'}
    +    cpu: [arm]
    +    os: [android]
    +
    +  '@esbuild/android-x64@0.25.12':
    +    resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [android]
    +
    +  '@esbuild/android-x64@0.27.3':
    +    resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [android]
    +
    +  '@esbuild/darwin-arm64@0.25.12':
    +    resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@esbuild/darwin-arm64@0.27.3':
    +    resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@esbuild/darwin-x64@0.25.12':
    +    resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@esbuild/darwin-x64@0.27.3':
    +    resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@esbuild/freebsd-arm64@0.25.12':
    +    resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [freebsd]
    +
    +  '@esbuild/freebsd-arm64@0.27.3':
    +    resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [freebsd]
    +
    +  '@esbuild/freebsd-x64@0.25.12':
    +    resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@esbuild/freebsd-x64@0.27.3':
    +    resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@esbuild/linux-arm64@0.25.12':
    +    resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [linux]
    +
    +  '@esbuild/linux-arm64@0.27.3':
    +    resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [linux]
    +
    +  '@esbuild/linux-arm@0.25.12':
    +    resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
    +    engines: {node: '>=18'}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@esbuild/linux-arm@0.27.3':
    +    resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
    +    engines: {node: '>=18'}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@esbuild/linux-ia32@0.25.12':
    +    resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
    +    engines: {node: '>=18'}
    +    cpu: [ia32]
    +    os: [linux]
    +
    +  '@esbuild/linux-ia32@0.27.3':
    +    resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
    +    engines: {node: '>=18'}
    +    cpu: [ia32]
    +    os: [linux]
    +
    +  '@esbuild/linux-loong64@0.25.12':
    +    resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
    +    engines: {node: '>=18'}
    +    cpu: [loong64]
    +    os: [linux]
    +
    +  '@esbuild/linux-loong64@0.27.3':
    +    resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
    +    engines: {node: '>=18'}
    +    cpu: [loong64]
    +    os: [linux]
    +
    +  '@esbuild/linux-mips64el@0.25.12':
    +    resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
    +    engines: {node: '>=18'}
    +    cpu: [mips64el]
    +    os: [linux]
    +
    +  '@esbuild/linux-mips64el@0.27.3':
    +    resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
    +    engines: {node: '>=18'}
    +    cpu: [mips64el]
    +    os: [linux]
    +
    +  '@esbuild/linux-ppc64@0.25.12':
    +    resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
    +    engines: {node: '>=18'}
    +    cpu: [ppc64]
    +    os: [linux]
    +
    +  '@esbuild/linux-ppc64@0.27.3':
    +    resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
    +    engines: {node: '>=18'}
    +    cpu: [ppc64]
    +    os: [linux]
    +
    +  '@esbuild/linux-riscv64@0.25.12':
    +    resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
    +    engines: {node: '>=18'}
    +    cpu: [riscv64]
    +    os: [linux]
    +
    +  '@esbuild/linux-riscv64@0.27.3':
    +    resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
    +    engines: {node: '>=18'}
    +    cpu: [riscv64]
    +    os: [linux]
    +
    +  '@esbuild/linux-s390x@0.25.12':
    +    resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
    +    engines: {node: '>=18'}
    +    cpu: [s390x]
    +    os: [linux]
    +
    +  '@esbuild/linux-s390x@0.27.3':
    +    resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
    +    engines: {node: '>=18'}
    +    cpu: [s390x]
    +    os: [linux]
    +
    +  '@esbuild/linux-x64@0.25.12':
    +    resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [linux]
    +
    +  '@esbuild/linux-x64@0.27.3':
    +    resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [linux]
    +
    +  '@esbuild/netbsd-arm64@0.25.12':
    +    resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [netbsd]
    +
    +  '@esbuild/netbsd-arm64@0.27.3':
    +    resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [netbsd]
    +
    +  '@esbuild/netbsd-x64@0.25.12':
    +    resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [netbsd]
    +
    +  '@esbuild/netbsd-x64@0.27.3':
    +    resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [netbsd]
    +
    +  '@esbuild/openbsd-arm64@0.25.12':
    +    resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [openbsd]
    +
    +  '@esbuild/openbsd-arm64@0.27.3':
    +    resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [openbsd]
    +
    +  '@esbuild/openbsd-x64@0.25.12':
    +    resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [openbsd]
    +
    +  '@esbuild/openbsd-x64@0.27.3':
    +    resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [openbsd]
    +
    +  '@esbuild/openharmony-arm64@0.25.12':
    +    resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [openharmony]
    +
    +  '@esbuild/openharmony-arm64@0.27.3':
    +    resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [openharmony]
    +
    +  '@esbuild/sunos-x64@0.25.12':
    +    resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [sunos]
    +
    +  '@esbuild/sunos-x64@0.27.3':
    +    resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [sunos]
    +
    +  '@esbuild/win32-arm64@0.25.12':
    +    resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@esbuild/win32-arm64@0.27.3':
    +    resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
    +    engines: {node: '>=18'}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@esbuild/win32-ia32@0.25.12':
    +    resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
    +    engines: {node: '>=18'}
    +    cpu: [ia32]
    +    os: [win32]
    +
    +  '@esbuild/win32-ia32@0.27.3':
    +    resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
    +    engines: {node: '>=18'}
    +    cpu: [ia32]
    +    os: [win32]
    +
    +  '@esbuild/win32-x64@0.25.12':
    +    resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@esbuild/win32-x64@0.27.3':
    +    resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
    +    engines: {node: '>=18'}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@exodus/bytes@1.14.0':
    +    resolution: {integrity: sha512-YiY1OmY6Qhkvmly8vZiD8wZRpW/npGZNg+0Sk8mstxirRHCg6lolHt5tSODCfuNPE/fBsAqRwDJE417x7jDDHA==}
    +    engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
    +    peerDependencies:
    +      '@noble/hashes': ^1.8.0 || ^2.0.0
    +    peerDependenciesMeta:
    +      '@noble/hashes':
    +        optional: true
    +
    +  '@extism/extism@2.0.0-rc13':
    +    resolution: {integrity: sha512-iQ3mrPKOC0WMZ94fuJrKbJmMyz4LQ9Abf8gd4F5ShxKWa+cRKcVzk0EqRQsp5xXsQ2dO3zJTiA6eTc4Ihf7k+A==}
    +
    +  '@extism/js-pdk@1.1.1':
    +    resolution: {integrity: sha512-VZLn/dX0ttA1uKk2PZeR/FL3N+nA1S5Vc7E5gdjkR60LuUIwCZT9cYON245V4HowHlBA7YOegh0TLjkx+wNbrA==}
    +
    +  '@inquirer/external-editor@1.0.3':
    +    resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
    +    engines: {node: '>=18'}
    +    peerDependencies:
    +      '@types/node': '>=18'
    +    peerDependenciesMeta:
    +      '@types/node':
    +        optional: true
    +
    +  '@ionic/core@7.8.6':
    +    resolution: {integrity: sha512-HAYZdEmeJgOdo2kDlZkcCGHb+zs/vjU6iv4skbVBL7y+OnSv/oC2u83Yee8S3/aY0YAxkyBgu7hLTYH13Zc2Aw==}
    +
    +  '@ionic/prettier-config@4.0.0':
    +    resolution: {integrity: sha512-0DqL6CggVdgeJAWOLPUT73rF1VD5p0tVlCpC5GXz5vTIUBxNwsJ5085Q7wXjKiE5Odx3aOHGTcuRWCawFsLFag==}
    +    peerDependencies:
    +      prettier: ^2.4.0 || ^3.0.0
    +
    +  '@jridgewell/gen-mapping@0.3.13':
    +    resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
    +
    +  '@jridgewell/remapping@2.3.5':
    +    resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
    +
    +  '@jridgewell/resolve-uri@3.1.2':
    +    resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
    +    engines: {node: '>=6.0.0'}
    +
    +  '@jridgewell/source-map@0.3.11':
    +    resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
    +
    +  '@jridgewell/sourcemap-codec@1.5.5':
    +    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
    +
    +  '@jridgewell/trace-mapping@0.3.31':
    +    resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
    +
    +  '@manypkg/find-root@1.1.0':
    +    resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
    +
    +  '@manypkg/get-packages@1.1.3':
    +    resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
    +
    +  '@napi-rs/wasm-runtime@1.1.1':
    +    resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
    +
    +  '@napi-rs/wasm-runtime@1.1.2':
    +    resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==}
    +    peerDependencies:
    +      '@emnapi/core': ^1.7.1
    +      '@emnapi/runtime': ^1.7.1
    +
    +  '@napi-rs/wasm-runtime@1.1.3':
    +    resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==}
    +    peerDependencies:
    +      '@emnapi/core': ^1.7.1
    +      '@emnapi/runtime': ^1.7.1
    +
    +  '@nodelib/fs.scandir@2.1.5':
    +    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
    +    engines: {node: '>= 8'}
    +
    +  '@nodelib/fs.stat@2.0.5':
    +    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
    +    engines: {node: '>= 8'}
    +
    +  '@nodelib/fs.walk@1.2.8':
    +    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
    +    engines: {node: '>= 8'}
    +
    +  '@oxc-parser/binding-android-arm-eabi@0.121.0':
    +    resolution: {integrity: sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [android]
    +
    +  '@oxc-parser/binding-android-arm64@0.121.0':
    +    resolution: {integrity: sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@oxc-parser/binding-darwin-arm64@0.121.0':
    +    resolution: {integrity: sha512-A0jNEvv7QMtCO1yk205t3DWU9sWUjQ2KNF0hSVO5W9R9r/R1BIvzG01UQAfmtC0dQm7sCrs5puixurKSfr2bRQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@oxc-parser/binding-darwin-x64@0.121.0':
    +    resolution: {integrity: sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@oxc-parser/binding-freebsd-x64@0.121.0':
    +    resolution: {integrity: sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0':
    +    resolution: {integrity: sha512-PmqPQuqHZyFVWA4ycr0eu4VnTMmq9laOHZd+8R359w6kzuNZPvmmunmNJ8ybkm769A0nCoVp3TJ6dUz7B3FYIQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@oxc-parser/binding-linux-arm-musleabihf@0.121.0':
    +    resolution: {integrity: sha512-vF24htj+MOH+Q7y9A8NuC6pUZu8t/C2Fr/kDOi2OcNf28oogr2xadBPXAbml802E8wRAVfbta6YLDQTearz+jw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@oxc-parser/binding-linux-arm64-gnu@0.121.0':
    +    resolution: {integrity: sha512-wjH8cIG2Lu/3d64iZpbYr73hREMgKAfu7fqpXjgM2S16y2zhTfDIp8EQjxO8vlDtKP5Rc7waZW72lh8nZtWrpA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-parser/binding-linux-arm64-musl@0.121.0':
    +    resolution: {integrity: sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxc-parser/binding-linux-ppc64-gnu@0.121.0':
    +    resolution: {integrity: sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [ppc64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-parser/binding-linux-riscv64-gnu@0.121.0':
    +    resolution: {integrity: sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-parser/binding-linux-riscv64-musl@0.121.0':
    +    resolution: {integrity: sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxc-parser/binding-linux-s390x-gnu@0.121.0':
    +    resolution: {integrity: sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [s390x]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-parser/binding-linux-x64-gnu@0.121.0':
    +    resolution: {integrity: sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-parser/binding-linux-x64-musl@0.121.0':
    +    resolution: {integrity: sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxc-parser/binding-openharmony-arm64@0.121.0':
    +    resolution: {integrity: sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [openharmony]
    +
    +  '@oxc-parser/binding-wasm32-wasi@0.121.0':
    +    resolution: {integrity: sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [wasm32]
    +
    +  '@oxc-parser/binding-win32-arm64-msvc@0.121.0':
    +    resolution: {integrity: sha512-V0pxh4mql4XTt3aiEtRNUeBAUFOw5jzZNxPABLaOKAWrVzSr9+XUaB095lY7jqMf5t8vkfh8NManGB28zanYKw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@oxc-parser/binding-win32-ia32-msvc@0.121.0':
    +    resolution: {integrity: sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [ia32]
    +    os: [win32]
    +
    +  '@oxc-parser/binding-win32-x64-msvc@0.121.0':
    +    resolution: {integrity: sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@oxc-project/types@0.121.0':
    +    resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==}
    +
    +  '@oxc-project/types@0.122.0':
    +    resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
    +
    +  '@oxc-project/types@0.124.0':
    +    resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==}
    +
    +  '@oxc-resolver/binding-android-arm-eabi@11.19.1':
    +    resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==}
    +    cpu: [arm]
    +    os: [android]
    +
    +  '@oxc-resolver/binding-android-arm64@11.19.1':
    +    resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@oxc-resolver/binding-darwin-arm64@11.19.1':
    +    resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@oxc-resolver/binding-darwin-x64@11.19.1':
    +    resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@oxc-resolver/binding-freebsd-x64@11.19.1':
    +    resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1':
    +    resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1':
    +    resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@oxc-resolver/binding-linux-arm64-gnu@11.19.1':
    +    resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-resolver/binding-linux-arm64-musl@11.19.1':
    +    resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1':
    +    resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==}
    +    cpu: [ppc64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1':
    +    resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-resolver/binding-linux-riscv64-musl@11.19.1':
    +    resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxc-resolver/binding-linux-s390x-gnu@11.19.1':
    +    resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==}
    +    cpu: [s390x]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-resolver/binding-linux-x64-gnu@11.19.1':
    +    resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxc-resolver/binding-linux-x64-musl@11.19.1':
    +    resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxc-resolver/binding-openharmony-arm64@11.19.1':
    +    resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==}
    +    cpu: [arm64]
    +    os: [openharmony]
    +
    +  '@oxc-resolver/binding-wasm32-wasi@11.19.1':
    +    resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [wasm32]
    +
    +  '@oxc-resolver/binding-win32-arm64-msvc@11.19.1':
    +    resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@oxc-resolver/binding-win32-ia32-msvc@11.19.1':
    +    resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==}
    +    cpu: [ia32]
    +    os: [win32]
    +
    +  '@oxc-resolver/binding-win32-x64-msvc@11.19.1':
    +    resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@oxfmt/binding-android-arm-eabi@0.42.0':
    +    resolution: {integrity: sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [android]
    +
    +  '@oxfmt/binding-android-arm64@0.42.0':
    +    resolution: {integrity: sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@oxfmt/binding-darwin-arm64@0.42.0':
    +    resolution: {integrity: sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@oxfmt/binding-darwin-x64@0.42.0':
    +    resolution: {integrity: sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@oxfmt/binding-freebsd-x64@0.42.0':
    +    resolution: {integrity: sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@oxfmt/binding-linux-arm-gnueabihf@0.42.0':
    +    resolution: {integrity: sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@oxfmt/binding-linux-arm-musleabihf@0.42.0':
    +    resolution: {integrity: sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@oxfmt/binding-linux-arm64-gnu@0.42.0':
    +    resolution: {integrity: sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxfmt/binding-linux-arm64-musl@0.42.0':
    +    resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxfmt/binding-linux-ppc64-gnu@0.42.0':
    +    resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [ppc64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxfmt/binding-linux-riscv64-gnu@0.42.0':
    +    resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxfmt/binding-linux-riscv64-musl@0.42.0':
    +    resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxfmt/binding-linux-s390x-gnu@0.42.0':
    +    resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [s390x]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxfmt/binding-linux-x64-gnu@0.42.0':
    +    resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxfmt/binding-linux-x64-musl@0.42.0':
    +    resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxfmt/binding-openharmony-arm64@0.42.0':
    +    resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [openharmony]
    +
    +  '@oxfmt/binding-win32-arm64-msvc@0.42.0':
    +    resolution: {integrity: sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@oxfmt/binding-win32-ia32-msvc@0.42.0':
    +    resolution: {integrity: sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [ia32]
    +    os: [win32]
    +
    +  '@oxfmt/binding-win32-x64-msvc@0.42.0':
    +    resolution: {integrity: sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@oxlint/binding-android-arm-eabi@1.57.0':
    +    resolution: {integrity: sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [android]
    +
    +  '@oxlint/binding-android-arm64@1.57.0':
    +    resolution: {integrity: sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@oxlint/binding-darwin-arm64@1.57.0':
    +    resolution: {integrity: sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@oxlint/binding-darwin-x64@1.57.0':
    +    resolution: {integrity: sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@oxlint/binding-freebsd-x64@1.57.0':
    +    resolution: {integrity: sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@oxlint/binding-linux-arm-gnueabihf@1.57.0':
    +    resolution: {integrity: sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@oxlint/binding-linux-arm-musleabihf@1.57.0':
    +    resolution: {integrity: sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@oxlint/binding-linux-arm64-gnu@1.57.0':
    +    resolution: {integrity: sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxlint/binding-linux-arm64-musl@1.57.0':
    +    resolution: {integrity: sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxlint/binding-linux-ppc64-gnu@1.57.0':
    +    resolution: {integrity: sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [ppc64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxlint/binding-linux-riscv64-gnu@1.57.0':
    +    resolution: {integrity: sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxlint/binding-linux-riscv64-musl@1.57.0':
    +    resolution: {integrity: sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxlint/binding-linux-s390x-gnu@1.57.0':
    +    resolution: {integrity: sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [s390x]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxlint/binding-linux-x64-gnu@1.57.0':
    +    resolution: {integrity: sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@oxlint/binding-linux-x64-musl@1.57.0':
    +    resolution: {integrity: sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@oxlint/binding-openharmony-arm64@1.57.0':
    +    resolution: {integrity: sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [openharmony]
    +
    +  '@oxlint/binding-win32-arm64-msvc@1.57.0':
    +    resolution: {integrity: sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@oxlint/binding-win32-ia32-msvc@1.57.0':
    +    resolution: {integrity: sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [ia32]
    +    os: [win32]
    +
    +  '@oxlint/binding-win32-x64-msvc@1.57.0':
    +    resolution: {integrity: sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@parcel/watcher-android-arm64@2.5.6':
    +    resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@parcel/watcher-darwin-arm64@2.5.6':
    +    resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@parcel/watcher-darwin-x64@2.5.6':
    +    resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@parcel/watcher-freebsd-x64@2.5.6':
    +    resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@parcel/watcher-linux-arm-glibc@2.5.6':
    +    resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [arm]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@parcel/watcher-linux-arm-musl@2.5.6':
    +    resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [arm]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@parcel/watcher-linux-arm64-glibc@2.5.6':
    +    resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@parcel/watcher-linux-arm64-musl@2.5.6':
    +    resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@parcel/watcher-linux-x64-glibc@2.5.6':
    +    resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@parcel/watcher-linux-x64-musl@2.5.6':
    +    resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@parcel/watcher-win32-arm64@2.5.6':
    +    resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@parcel/watcher-win32-ia32@2.5.6':
    +    resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [ia32]
    +    os: [win32]
    +
    +  '@parcel/watcher-win32-x64@2.5.6':
    +    resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
    +    engines: {node: '>= 10.0.0'}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@parcel/watcher@2.5.6':
    +    resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
    +    engines: {node: '>= 10.0.0'}
    +
    +  '@playwright/test@1.58.2':
    +    resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
    +    engines: {node: '>=18'}
    +    hasBin: true
    +
    +  '@polka/url@1.0.0-next.29':
    +    resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
    +
    +  '@popperjs/core@2.11.8':
    +    resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
    +
    +  '@quansync/fs@1.0.0':
    +    resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
    +
    +  '@rolldown/binding-android-arm64@1.0.0-rc.12':
    +    resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@rolldown/binding-android-arm64@1.0.0-rc.15':
    +    resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@rolldown/binding-darwin-arm64@1.0.0-rc.12':
    +    resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@rolldown/binding-darwin-arm64@1.0.0-rc.15':
    +    resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@rolldown/binding-darwin-x64@1.0.0-rc.12':
    +    resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@rolldown/binding-darwin-x64@1.0.0-rc.15':
    +    resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@rolldown/binding-freebsd-x64@1.0.0-rc.12':
    +    resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@rolldown/binding-freebsd-x64@1.0.0-rc.15':
    +    resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
    +    resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
    +    resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
    +    resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
    +    resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
    +    resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
    +    resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
    +    resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [ppc64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
    +    resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [ppc64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
    +    resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [s390x]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
    +    resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [s390x]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
    +    resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
    +    resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
    +    resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
    +    resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
    +    resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [openharmony]
    +
    +  '@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
    +    resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [openharmony]
    +
    +  '@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
    +    resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [wasm32]
    +
    +  '@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
    +    resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [wasm32]
    +
    +  '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
    +    resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
    +    resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
    +    resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
    +    resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@rolldown/pluginutils@1.0.0-rc.12':
    +    resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==}
    +
    +  '@rolldown/pluginutils@1.0.0-rc.15':
    +    resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==}
    +
    +  '@rollup/pluginutils@3.1.0':
    +    resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
    +    engines: {node: '>= 8.0.0'}
    +    peerDependencies:
    +      rollup: ^1.20.0||^2.0.0
    +
    +  '@rollup/pluginutils@5.3.0':
    +    resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
    +    engines: {node: '>=14.0.0'}
    +    peerDependencies:
    +      rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
    +    peerDependenciesMeta:
    +      rollup:
    +        optional: true
    +
    +  '@rollup/rollup-android-arm-eabi@4.57.1':
    +    resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==}
    +    cpu: [arm]
    +    os: [android]
    +
    +  '@rollup/rollup-android-arm64@4.57.1':
    +    resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  '@rollup/rollup-darwin-arm64@4.44.0':
    +    resolution: {integrity: sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@rollup/rollup-darwin-arm64@4.57.1':
    +    resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  '@rollup/rollup-darwin-x64@4.44.0':
    +    resolution: {integrity: sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@rollup/rollup-darwin-x64@4.57.1':
    +    resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  '@rollup/rollup-freebsd-arm64@4.57.1':
    +    resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==}
    +    cpu: [arm64]
    +    os: [freebsd]
    +
    +  '@rollup/rollup-freebsd-x64@4.57.1':
    +    resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  '@rollup/rollup-linux-arm-gnueabihf@4.57.1':
    +    resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==}
    +    cpu: [arm]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rollup/rollup-linux-arm-musleabihf@4.57.1':
    +    resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==}
    +    cpu: [arm]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rollup/rollup-linux-arm64-gnu@4.44.0':
    +    resolution: {integrity: sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rollup/rollup-linux-arm64-gnu@4.57.1':
    +    resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rollup/rollup-linux-arm64-musl@4.44.0':
    +    resolution: {integrity: sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rollup/rollup-linux-arm64-musl@4.57.1':
    +    resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rollup/rollup-linux-loong64-gnu@4.57.1':
    +    resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==}
    +    cpu: [loong64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rollup/rollup-linux-loong64-musl@4.57.1':
    +    resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==}
    +    cpu: [loong64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rollup/rollup-linux-ppc64-gnu@4.57.1':
    +    resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==}
    +    cpu: [ppc64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rollup/rollup-linux-ppc64-musl@4.57.1':
    +    resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==}
    +    cpu: [ppc64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rollup/rollup-linux-riscv64-gnu@4.57.1':
    +    resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rollup/rollup-linux-riscv64-musl@4.57.1':
    +    resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rollup/rollup-linux-s390x-gnu@4.57.1':
    +    resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==}
    +    cpu: [s390x]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rollup/rollup-linux-x64-gnu@4.44.0':
    +    resolution: {integrity: sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rollup/rollup-linux-x64-gnu@4.57.1':
    +    resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  '@rollup/rollup-linux-x64-musl@4.44.0':
    +    resolution: {integrity: sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rollup/rollup-linux-x64-musl@4.57.1':
    +    resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  '@rollup/rollup-openbsd-x64@4.57.1':
    +    resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==}
    +    cpu: [x64]
    +    os: [openbsd]
    +
    +  '@rollup/rollup-openharmony-arm64@4.57.1':
    +    resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==}
    +    cpu: [arm64]
    +    os: [openharmony]
    +
    +  '@rollup/rollup-win32-arm64-msvc@4.44.0':
    +    resolution: {integrity: sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@rollup/rollup-win32-arm64-msvc@4.57.1':
    +    resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  '@rollup/rollup-win32-ia32-msvc@4.57.1':
    +    resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==}
    +    cpu: [ia32]
    +    os: [win32]
    +
    +  '@rollup/rollup-win32-x64-gnu@4.57.1':
    +    resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@rollup/rollup-win32-x64-msvc@4.44.0':
    +    resolution: {integrity: sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@rollup/rollup-win32-x64-msvc@4.57.1':
    +    resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  '@standard-schema/spec@1.1.0':
    +    resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
    +
    +  '@stencil/core@4.43.1':
    +    resolution: {integrity: sha512-sohOiS8XMJXQnMsxhCgT6Ex8Kl0e0PAzDH9ExYXpDn/5Wo5rliSmABkqU9/dz/l945O/mpeG10rn6jToZJFjKQ==}
    +    engines: {node: '>=16.0.0', npm: '>=7.10.0'}
    +    hasBin: true
    +
    +  '@stencil/playwright@0.4.3':
    +    resolution: {integrity: sha512-p5VWjPUTStntdLYjnQdoMhh/FpdTbl9oV8wVAFkY9fjstCS7zB/CADYfillDKNtfsb7VGNbD1SkJaQ7ez2yQhA==}
    +    engines: {node: '>=12.0.0', npm: '>=6.0.0'}
    +    peerDependencies:
    +      '@playwright/test': '>=1.50.0'
    +      '@stencil/core': workspace:*
    +
    +  '@stencil/react-output-target@0.0.9':
    +    resolution: {integrity: sha512-t2sSkm/VGftBqewK47eZonaHIXW7CYWlsDy6Ln4jqNVpr93CuPWkg7rsnPiZrJrU1NBuTgA0hC2xoRcAJVm7Sw==}
    +    peerDependencies:
    +      '@stencil/core': workspace:*
    +
    +  '@stencil/sass@3.2.3':
    +    resolution: {integrity: sha512-Wru76NJqa6D79/fDjSuiXoe2U0Ky1j7LLycqn7DV0jCmVO3tiWqXHBUPg0gMXJtxEiIbIJeiH/VpKhjNrBIUkQ==}
    +    engines: {node: '>=12.0.0', npm: '>=6.0.0'}
    +    peerDependencies:
    +      '@stencil/core': workspace:*
    +
    +  '@stencil/vitest@1.11.6':
    +    resolution: {integrity: sha512-8aqZzN2O0bppsGJSqLetgzyATufRRSybKLctKuW93KtHED/WwJyS0U+8D9OqKEjxnopvSNO4QrvLWqntaqhvSw==}
    +    engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
    +    hasBin: true
    +    peerDependencies:
    +      '@playwright/test': '*'
    +      '@stencil/core': workspace:*
    +      '@stencil/mock-doc': '*'
    +      '@vitest/browser-playwright': '*'
    +      '@vitest/browser-preview': '*'
    +      '@vitest/browser-webdriverio': '*'
    +      '@wdio/globals': '*'
    +      happy-dom: '*'
    +      jsdom: '*'
    +      playwright: '*'
    +      vitest: ^4.0.0 || ^3.0.0 || ^2.0.0
    +    peerDependenciesMeta:
    +      '@playwright/test':
    +        optional: true
    +      '@stencil/mock-doc':
    +        optional: true
    +      '@vitest/browser-playwright':
    +        optional: true
    +      '@vitest/browser-preview':
    +        optional: true
    +      '@vitest/browser-webdriverio':
    +        optional: true
    +      '@wdio/globals':
    +        optional: true
    +      happy-dom:
    +        optional: true
    +      jsdom:
    +        optional: true
    +      playwright:
    +        optional: true
    +
    +  '@tsdown/css@0.21.6':
    +    resolution: {integrity: sha512-ltwXCmCE+o42ljAQKtdK5QvYprPP7k+IunVuucWAttef8OdsWriz0D4MbxZbBRqvIH48Z96bC5tkgqn9H6sd7A==}
    +    engines: {node: '>=20.19.0'}
    +    peerDependencies:
    +      postcss: ^8.4.0
    +      postcss-import: ^16.0.0
    +      postcss-modules: ^6.0.0
    +      sass: '*'
    +      sass-embedded: '*'
    +      tsdown: 0.21.6
    +    peerDependenciesMeta:
    +      postcss:
    +        optional: true
    +      postcss-import:
    +        optional: true
    +      postcss-modules:
    +        optional: true
    +      sass:
    +        optional: true
    +      sass-embedded:
    +        optional: true
    +
    +  '@tybys/wasm-util@0.10.1':
    +    resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
    +
    +  '@types/chai@5.2.3':
    +    resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
    +
    +  '@types/deep-eql@4.0.2':
    +    resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
    +
    +  '@types/estree@0.0.39':
    +    resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
    +
    +  '@types/estree@1.0.8':
    +    resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
    +
    +  '@types/file-saver@2.0.7':
    +    resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
    +
    +  '@types/jsesc@2.5.1':
    +    resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==}
    +
    +  '@types/lodash-es@4.17.12':
    +    resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
    +
    +  '@types/lodash@4.17.23':
    +    resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==}
    +
    +  '@types/node@12.20.55':
    +    resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
    +
    +  '@types/node@24.10.13':
    +    resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==}
    +
    +  '@types/prompts@2.4.9':
    +    resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
    +
    +  '@types/video.js@7.3.58':
    +    resolution: {integrity: sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==}
    +
    +  '@types/ws@8.18.1':
    +    resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
    +
    +  '@videojs/http-streaming@2.16.3':
    +    resolution: {integrity: sha512-91CJv5PnFBzNBvyEjt+9cPzTK/xoVixARj2g7ZAvItA+5bx8VKdk5RxCz/PP2kdzz9W+NiDUMPkdmTsosmy69Q==}
    +    engines: {node: '>=8', npm: '>=5'}
    +    peerDependencies:
    +      video.js: ^6 || ^7
    +
    +  '@videojs/vhs-utils@3.0.5':
    +    resolution: {integrity: sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==}
    +    engines: {node: '>=8', npm: '>=5'}
    +
    +  '@videojs/xhr@2.6.0':
    +    resolution: {integrity: sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==}
    +
    +  '@vitest/browser-playwright@4.1.2':
    +    resolution: {integrity: sha512-N0Z2HzMLvMR6k/tWPTS6Q/DaRscrkax/f2f9DIbNQr+Cd1l4W4wTf/I6S983PAMr0tNqqoTL+xNkLh9M5vbkLg==}
    +    peerDependencies:
    +      playwright: '*'
    +      vitest: 4.1.2
    +
    +  '@vitest/browser@4.1.2':
    +    resolution: {integrity: sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ==}
    +    peerDependencies:
    +      vitest: 4.1.2
    +
    +  '@vitest/expect@4.1.2':
    +    resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==}
    +
    +  '@vitest/mocker@4.1.2':
    +    resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==}
    +    peerDependencies:
    +      msw: ^2.4.9
    +      vite: ^6.0.0 || ^7.0.0 || ^8.0.0
    +    peerDependenciesMeta:
    +      msw:
    +        optional: true
    +      vite:
    +        optional: true
    +
    +  '@vitest/pretty-format@4.1.2':
    +    resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==}
    +
    +  '@vitest/runner@4.1.2':
    +    resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==}
    +
    +  '@vitest/snapshot@4.1.2':
    +    resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==}
    +
    +  '@vitest/spy@4.1.2':
    +    resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==}
    +
    +  '@vitest/utils@4.1.2':
    +    resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==}
    +
    +  '@xmldom/xmldom@0.8.11':
    +    resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
    +    engines: {node: '>=10.0.0'}
    +
    +  '@zeit/schemas@2.36.0':
    +    resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==}
    +
    +  abstract-leveldown@0.12.4:
    +    resolution: {integrity: sha512-TOod9d5RDExo6STLMGa+04HGkl+TlMfbDnTyN93/ETJ9DpQ0DaYLqcMZlbXvdc4W3vVo1Qrl+WhSp8zvDsJ+jA==}
    +    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
    +
    +  acorn@8.15.0:
    +    resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
    +    engines: {node: '>=0.4.0'}
    +    hasBin: true
    +
    +  aes-decrypter@3.1.3:
    +    resolution: {integrity: sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==}
    +
    +  agent-base@7.1.4:
    +    resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
    +    engines: {node: '>= 14'}
    +
    +  ajv@8.12.0:
    +    resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
    +
    +  ansi-align@3.0.1:
    +    resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
    +
    +  ansi-colors@4.1.3:
    +    resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
    +    engines: {node: '>=6'}
    +
    +  ansi-regex@4.1.1:
    +    resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
    +    engines: {node: '>=6'}
    +
    +  ansi-regex@5.0.1:
    +    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
    +    engines: {node: '>=8'}
    +
    +  ansi-regex@6.2.2:
    +    resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
    +    engines: {node: '>=12'}
    +
    +  ansi-styles@3.2.1:
    +    resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
    +    engines: {node: '>=4'}
    +
    +  ansi-styles@4.3.0:
    +    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
    +    engines: {node: '>=8'}
    +
    +  ansi-styles@6.2.3:
    +    resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
    +    engines: {node: '>=12'}
    +
    +  ansis@4.2.0:
    +    resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
    +    engines: {node: '>=14'}
    +
    +  arch@2.2.0:
    +    resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
    +
    +  arg@5.0.2:
    +    resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
    +
    +  argparse@1.0.10:
    +    resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
    +
    +  argparse@2.0.1:
    +    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
    +
    +  arr-diff@4.0.0:
    +    resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  arr-flatten@1.1.0:
    +    resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  arr-union@3.1.0:
    +    resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==}
    +    engines: {node: '>=0.10.0'}
    +
    +  array-timsort@1.0.3:
    +    resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
    +
    +  array-union@2.1.0:
    +    resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
    +    engines: {node: '>=8'}
    +
    +  array-unique@0.3.2:
    +    resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  asn1.js@4.10.1:
    +    resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==}
    +
    +  assertion-error@2.0.1:
    +    resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
    +    engines: {node: '>=12'}
    +
    +  assign-symbols@1.0.0:
    +    resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  ast-kit@3.0.0-beta.1:
    +    resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==}
    +    engines: {node: '>=20.19.0'}
    +
    +  at-least-node@1.0.0:
    +    resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
    +    engines: {node: '>= 4.0.0'}
    +
    +  atob@2.1.2:
    +    resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
    +    engines: {node: '>= 4.5.0'}
    +    hasBin: true
    +
    +  available-typed-arrays@1.0.7:
    +    resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
    +    engines: {node: '>= 0.4'}
    +
    +  babel-plugin-polyfill-corejs2@0.4.15:
    +    resolution: {integrity: sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==}
    +    peerDependencies:
    +      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
    +
    +  babel-plugin-polyfill-corejs3@0.13.0:
    +    resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==}
    +    peerDependencies:
    +      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
    +
    +  babel-plugin-polyfill-regenerator@0.6.6:
    +    resolution: {integrity: sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==}
    +    peerDependencies:
    +      '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
    +
    +  babel-plugin-transform-react-remove-prop-types@0.4.24:
    +    resolution: {integrity: sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==}
    +
    +  balanced-match@1.0.2:
    +    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
    +
    +  balanced-match@4.0.3:
    +    resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==}
    +    engines: {node: 20 || >=22}
    +
    +  base@0.11.2:
    +    resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  baseline-browser-mapping@2.9.19:
    +    resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
    +    hasBin: true
    +
    +  better-path-resolve@1.0.0:
    +    resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
    +    engines: {node: '>=4'}
    +
    +  bidi-js@1.0.3:
    +    resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
    +
    +  big.js@5.2.2:
    +    resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
    +
    +  birpc@4.0.0:
    +    resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==}
    +
    +  bl@0.8.2:
    +    resolution: {integrity: sha512-pfqikmByp+lifZCS0p6j6KreV6kNU6Apzpm2nKOk+94cZb/jvle55+JxWiByUQ0Wo/+XnDXEy5MxxKMb6r0VIw==}
    +
    +  bn.js@4.12.3:
    +    resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==}
    +
    +  bn.js@5.2.3:
    +    resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==}
    +
    +  bootstrap@5.3.8:
    +    resolution: {integrity: sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==}
    +    peerDependencies:
    +      '@popperjs/core': ^2.11.8
    +
    +  boxen@7.0.0:
    +    resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==}
    +    engines: {node: '>=14.16'}
    +
    +  brace-expansion@1.1.12:
    +    resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
    +
    +  brace-expansion@5.0.2:
    +    resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
    +    engines: {node: 20 || >=22}
    +
    +  braces@2.3.2:
    +    resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==}
    +    engines: {node: '>=0.10.0'}
    +
    +  braces@3.0.3:
    +    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
    +    engines: {node: '>=8'}
    +
    +  brorand@1.1.0:
    +    resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==}
    +
    +  browserify-aes@1.2.0:
    +    resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==}
    +
    +  browserify-cipher@1.0.1:
    +    resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==}
    +
    +  browserify-des@1.0.2:
    +    resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==}
    +
    +  browserify-fs@1.0.0:
    +    resolution: {integrity: sha512-8LqHRPuAEKvyTX34R6tsw4bO2ro6j9DmlYBhiYWHRM26Zv2cBw1fJOU0NeUQ0RkXkPn/PFBjhA0dm4AgaBurTg==}
    +
    +  browserify-rsa@4.1.1:
    +    resolution: {integrity: sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==}
    +    engines: {node: '>= 0.10'}
    +
    +  browserify-sign@4.2.5:
    +    resolution: {integrity: sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==}
    +    engines: {node: '>= 0.10'}
    +
    +  browserslist@4.28.1:
    +    resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
    +    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
    +    hasBin: true
    +
    +  buffer-es6@4.9.3:
    +    resolution: {integrity: sha512-Ibt+oXxhmeYJSsCkODPqNpPmyegefiD8rfutH1NYGhMZQhSp95Rz7haemgnJ6dxa6LT+JLLbtgOMORRluwKktw==}
    +
    +  buffer-from@1.1.2:
    +    resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
    +
    +  buffer-xor@1.0.3:
    +    resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==}
    +
    +  bundle-name@4.1.0:
    +    resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
    +    engines: {node: '>=18'}
    +
    +  bytes@3.0.0:
    +    resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
    +    engines: {node: '>= 0.8'}
    +
    +  bytes@3.1.2:
    +    resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
    +    engines: {node: '>= 0.8'}
    +
    +  cac@7.0.0:
    +    resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
    +    engines: {node: '>=20.19.0'}
    +
    +  cache-base@1.0.1:
    +    resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  call-bind-apply-helpers@1.0.2:
    +    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
    +    engines: {node: '>= 0.4'}
    +
    +  call-bind@1.0.8:
    +    resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
    +    engines: {node: '>= 0.4'}
    +
    +  call-bound@1.0.4:
    +    resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
    +    engines: {node: '>= 0.4'}
    +
    +  caller-callsite@2.0.0:
    +    resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==}
    +    engines: {node: '>=4'}
    +
    +  caller-path@2.0.0:
    +    resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==}
    +    engines: {node: '>=4'}
    +
    +  callsites@2.0.0:
    +    resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==}
    +    engines: {node: '>=4'}
    +
    +  callsites@3.1.0:
    +    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
    +    engines: {node: '>=6'}
    +
    +  camelcase@5.3.1:
    +    resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
    +    engines: {node: '>=6'}
    +
    +  camelcase@7.0.1:
    +    resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==}
    +    engines: {node: '>=14.16'}
    +
    +  caniuse-lite@1.0.30001769:
    +    resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==}
    +
    +  chai@6.2.2:
    +    resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
    +    engines: {node: '>=18'}
    +
    +  chalk-template@0.4.0:
    +    resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==}
    +    engines: {node: '>=12'}
    +
    +  chalk-template@1.1.2:
    +    resolution: {integrity: sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==}
    +    engines: {node: '>=14.16'}
    +
    +  chalk@4.1.2:
    +    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
    +    engines: {node: '>=10'}
    +
    +  chalk@5.0.1:
    +    resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==}
    +    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
    +
    +  chalk@5.6.2:
    +    resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
    +    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
    +
    +  chardet@2.1.1:
    +    resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
    +
    +  chokidar@4.0.3:
    +    resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
    +    engines: {node: '>= 14.16.0'}
    +
    +  cipher-base@1.0.7:
    +    resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==}
    +    engines: {node: '>= 0.10'}
    +
    +  class-utils@0.3.6:
    +    resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  clear-module@4.1.2:
    +    resolution: {integrity: sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==}
    +    engines: {node: '>=8'}
    +
    +  cli-boxes@3.0.0:
    +    resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
    +    engines: {node: '>=10'}
    +
    +  clipboardy@3.0.0:
    +    resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==}
    +    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
    +
    +  cliui@5.0.0:
    +    resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==}
    +
    +  clone@0.1.19:
    +    resolution: {integrity: sha512-IO78I0y6JcSpEPHzK4obKdsL7E7oLdRVDVOLwr2Hkbjsb+Eoz0dxW6tef0WizoKu0gLC4oZSZuEF4U2K6w1WQw==}
    +
    +  collection-visit@1.0.0:
    +    resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  color-convert@1.9.3:
    +    resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
    +
    +  color-convert@2.0.1:
    +    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
    +    engines: {node: '>=7.0.0'}
    +
    +  color-name@1.1.3:
    +    resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
    +
    +  color-name@1.1.4:
    +    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
    +
    +  colorjs.io@0.5.2:
    +    resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
    +
    +  commander@14.0.3:
    +    resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
    +    engines: {node: '>=20'}
    +
    +  commander@2.20.3:
    +    resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
    +
    +  comment-json@4.6.2:
    +    resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==}
    +    engines: {node: '>= 6'}
    +
    +  component-emitter@1.3.1:
    +    resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
    +
    +  compressible@2.0.18:
    +    resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
    +    engines: {node: '>= 0.6'}
    +
    +  compression@1.8.1:
    +    resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
    +    engines: {node: '>= 0.8.0'}
    +
    +  concat-map@0.0.1:
    +    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
    +
    +  concat-stream@1.6.2:
    +    resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
    +    engines: {'0': node >= 0.8}
    +
    +  confbox@0.1.8:
    +    resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
    +
    +  confbox@0.2.4:
    +    resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
    +
    +  content-disposition@0.5.2:
    +    resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==}
    +    engines: {node: '>= 0.6'}
    +
    +  convert-source-map@2.0.0:
    +    resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
    +
    +  copy-descriptor@0.1.1:
    +    resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  core-js-compat@3.48.0:
    +    resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==}
    +
    +  core-js@3.48.0:
    +    resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==}
    +
    +  core-util-is@1.0.3:
    +    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
    +
    +  cosmiconfig@5.2.1:
    +    resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==}
    +    engines: {node: '>=4'}
    +
    +  create-ecdh@4.0.4:
    +    resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==}
    +
    +  create-hash@1.2.0:
    +    resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==}
    +
    +  create-hmac@1.1.7:
    +    resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==}
    +
    +  cross-spawn@7.0.6:
    +    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
    +    engines: {node: '>= 8'}
    +
    +  crypto-browserify@3.12.1:
    +    resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==}
    +    engines: {node: '>= 0.10'}
    +
    +  cspell-config-lib@9.7.0:
    +    resolution: {integrity: sha512-pguh8A3+bSJ1OOrKCiQan8bvaaY125de76OEFz7q1Pq309lIcDrkoL/W4aYbso/NjrXaIw6OjkgPMGRBI/IgGg==}
    +    engines: {node: '>=20'}
    +
    +  cspell-dictionary@9.7.0:
    +    resolution: {integrity: sha512-k/Wz0so32+0QEqQe21V9m4BNXM5ZN6lz3Ix/jLCbMxFIPl6wT711ftjOWIEMFhvUOP0TWXsbzcuE9mKtS5mTig==}
    +    engines: {node: '>=20'}
    +
    +  cspell-gitignore@9.7.0:
    +    resolution: {integrity: sha512-MtoYuH4ah4K6RrmaF834npMcRsTKw0658mC6yvmBacUQOmwB/olqyuxF3fxtbb55HDb7cXDQ35t1XuwwGEQeZw==}
    +    engines: {node: '>=20'}
    +    hasBin: true
    +
    +  cspell-glob@9.7.0:
    +    resolution: {integrity: sha512-LUeAoEsoCJ+7E3TnUmWBscpVQOmdwBejMlFn0JkXy6LQzxrybxXBKf65RSdIv1o5QtrhQIMa358xXYQG0sv/tA==}
    +    engines: {node: '>=20'}
    +
    +  cspell-grammar@9.7.0:
    +    resolution: {integrity: sha512-oEYME+7MJztfVY1C06aGcJgEYyqBS/v/ETkQGPzf/c6ObSAPRcUbVtsXZgnR72Gru9aBckc70xJcD6bELdoWCA==}
    +    engines: {node: '>=20'}
    +    hasBin: true
    +
    +  cspell-io@9.7.0:
    +    resolution: {integrity: sha512-V7x0JHAUCcJPRCH8c0MQkkaKmZD2yotxVyrNEx2SZTpvnKrYscLEnUUTWnGJIIf9znzISqw116PLnYu2c+zd6Q==}
    +    engines: {node: '>=20'}
    +
    +  cspell-lib@9.7.0:
    +    resolution: {integrity: sha512-aTx/aLRpnuY1RJnYAu+A8PXfm1oIUdvAQ4W9E66bTgp1LWI+2G2++UtaPxRIgI0olxE9vcXqUnKpjOpO+5W9bQ==}
    +    engines: {node: '>=20'}
    +
    +  cspell-trie-lib@9.7.0:
    +    resolution: {integrity: sha512-a2YqmcraL3g6I/4gY7SYWEZfP73oLluUtxO7wxompk/kOG2K1FUXyQfZXaaR7HxVv10axT1+NrjhOmXpfbI6LA==}
    +    engines: {node: '>=20'}
    +    peerDependencies:
    +      '@cspell/cspell-types': 9.7.0
    +
    +  cspell@9.7.0:
    +    resolution: {integrity: sha512-ftxOnkd+scAI7RZ1/ksgBZRr0ouC7QRKtPQhD/PbLTKwAM62sSvRhE1bFsuW3VKBn/GilWzTjkJ40WmnDqH5iQ==}
    +    engines: {node: '>=20.18'}
    +    hasBin: true
    +
    +  css-tree@3.2.1:
    +    resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
    +    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
    +
    +  css-what@7.0.0:
    +    resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==}
    +    engines: {node: '>= 6'}
    +
    +  cssesc@3.0.0:
    +    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
    +    engines: {node: '>=4'}
    +    hasBin: true
    +
    +  cssstyle@5.3.7:
    +    resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==}
    +    engines: {node: '>=20'}
    +
    +  data-urls@7.0.0:
    +    resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
    +    engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
    +
    +  debug@2.6.9:
    +    resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
    +    peerDependencies:
    +      supports-color: '*'
    +    peerDependenciesMeta:
    +      supports-color:
    +        optional: true
    +
    +  debug@4.4.3:
    +    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
    +    engines: {node: '>=6.0'}
    +    peerDependencies:
    +      supports-color: '*'
    +    peerDependenciesMeta:
    +      supports-color:
    +        optional: true
    +
    +  decamelize@1.2.0:
    +    resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  decimal.js@10.6.0:
    +    resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
    +
    +  decode-uri-component@0.2.2:
    +    resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
    +    engines: {node: '>=0.10'}
    +
    +  deep-extend@0.6.0:
    +    resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
    +    engines: {node: '>=4.0.0'}
    +
    +  deepmerge@4.3.1:
    +    resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
    +    engines: {node: '>=0.10.0'}
    +
    +  default-browser-id@5.0.1:
    +    resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
    +    engines: {node: '>=18'}
    +
    +  default-browser@5.5.0:
    +    resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
    +    engines: {node: '>=18'}
    +
    +  deferred-leveldown@0.2.0:
    +    resolution: {integrity: sha512-+WCbb4+ez/SZ77Sdy1iadagFiVzMB89IKOBhglgnUkVxOxRWmmFsz8UDSNWh4Rhq+3wr/vMFlYj+rdEwWUDdng==}
    +    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
    +
    +  define-data-property@1.1.4:
    +    resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
    +    engines: {node: '>= 0.4'}
    +
    +  define-lazy-prop@3.0.0:
    +    resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
    +    engines: {node: '>=12'}
    +
    +  define-property@0.2.5:
    +    resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  define-property@1.0.0:
    +    resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  define-property@2.0.2:
    +    resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  defu@6.1.4:
    +    resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
    +
    +  des.js@1.1.0:
    +    resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==}
    +
    +  detect-indent@6.1.0:
    +    resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
    +    engines: {node: '>=8'}
    +
    +  detect-libc@2.1.2:
    +    resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
    +    engines: {node: '>=8'}
    +
    +  diffie-hellman@5.0.3:
    +    resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
    +
    +  dir-glob@3.0.1:
    +    resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
    +    engines: {node: '>=8'}
    +
    +  dom-walk@0.1.2:
    +    resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
    +
    +  dts-resolver@2.1.3:
    +    resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==}
    +    engines: {node: '>=20.19.0'}
    +    peerDependencies:
    +      oxc-resolver: '>=11.0.0'
    +    peerDependenciesMeta:
    +      oxc-resolver:
    +        optional: true
    +
    +  dunder-proto@1.0.1:
    +    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
    +    engines: {node: '>= 0.4'}
    +
    +  eastasianwidth@0.2.0:
    +    resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
    +
    +  electron-to-chromium@1.5.286:
    +    resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
    +
    +  elliptic@6.6.1:
    +    resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==}
    +
    +  emoji-regex@7.0.3:
    +    resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==}
    +
    +  emoji-regex@8.0.0:
    +    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
    +
    +  emoji-regex@9.2.2:
    +    resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
    +
    +  emojis-list@3.0.0:
    +    resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
    +    engines: {node: '>= 4'}
    +
    +  empathic@2.0.0:
    +    resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
    +    engines: {node: '>=14'}
    +
    +  enhanced-resolve@4.5.0:
    +    resolution: {integrity: sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==}
    +    engines: {node: '>=6.9.0'}
    +
    +  enquirer@2.4.1:
    +    resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
    +    engines: {node: '>=8.6'}
    +
    +  entities@4.5.0:
    +    resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
    +    engines: {node: '>=0.12'}
    +
    +  entities@6.0.1:
    +    resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
    +    engines: {node: '>=0.12'}
    +
    +  env-paths@4.0.0:
    +    resolution: {integrity: sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==}
    +    engines: {node: '>=20'}
    +
    +  errno@0.1.8:
    +    resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
    +    hasBin: true
    +
    +  error-ex@1.3.4:
    +    resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
    +
    +  es-define-property@1.0.1:
    +    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
    +    engines: {node: '>= 0.4'}
    +
    +  es-errors@1.3.0:
    +    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
    +    engines: {node: '>= 0.4'}
    +
    +  es-module-lexer@2.0.0:
    +    resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
    +
    +  es-object-atoms@1.1.1:
    +    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
    +    engines: {node: '>= 0.4'}
    +
    +  esbuild@0.25.12:
    +    resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
    +    engines: {node: '>=18'}
    +    hasBin: true
    +
    +  esbuild@0.27.3:
    +    resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
    +    engines: {node: '>=18'}
    +    hasBin: true
    +
    +  escalade@3.2.0:
    +    resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
    +    engines: {node: '>=6'}
    +
    +  esprima@4.0.1:
    +    resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
    +    engines: {node: '>=4'}
    +    hasBin: true
    +
    +  estree-walker@0.6.1:
    +    resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==}
    +
    +  estree-walker@1.0.1:
    +    resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
    +
    +  estree-walker@2.0.2:
    +    resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
    +
    +  estree-walker@3.0.3:
    +    resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
    +
    +  evp_bytestokey@1.0.3:
    +    resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==}
    +
    +  execa@5.1.1:
    +    resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
    +    engines: {node: '>=10'}
    +
    +  expand-brackets@2.1.4:
    +    resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  expect-type@1.3.0:
    +    resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
    +    engines: {node: '>=12.0.0'}
    +
    +  exsolve@1.0.8:
    +    resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
    +
    +  extend-shallow@2.0.1:
    +    resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
    +    engines: {node: '>=0.10.0'}
    +
    +  extend-shallow@3.0.2:
    +    resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==}
    +    engines: {node: '>=0.10.0'}
    +
    +  extendable-error@0.1.7:
    +    resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==}
    +
    +  extglob@2.0.4:
    +    resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  fast-deep-equal@3.1.3:
    +    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
    +
    +  fast-equals@6.0.0:
    +    resolution: {integrity: sha512-PFhhIGgdM79r5Uztdj9Zb6Tt1zKafqVfdMGwVca1z5z6fbX7DmsySSuJd8HiP6I1j505DCS83cLxo5rmSNeVEA==}
    +    engines: {node: '>=6.0.0'}
    +
    +  fast-glob@3.3.3:
    +    resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
    +    engines: {node: '>=8.6.0'}
    +
    +  fast-json-stable-stringify@2.1.0:
    +    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
    +
    +  fastq@1.20.1:
    +    resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
    +
    +  fd-package-json@2.0.0:
    +    resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==}
    +
    +  fdir@6.5.0:
    +    resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
    +    engines: {node: '>=12.0.0'}
    +    peerDependencies:
    +      picomatch: ^3 || ^4
    +    peerDependenciesMeta:
    +      picomatch:
    +        optional: true
    +
    +  file-saver@2.0.5:
    +    resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
    +
    +  fill-range@4.0.0:
    +    resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  fill-range@7.1.1:
    +    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
    +    engines: {node: '>=8'}
    +
    +  find-up@3.0.0:
    +    resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
    +    engines: {node: '>=6'}
    +
    +  find-up@4.1.0:
    +    resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
    +    engines: {node: '>=8'}
    +
    +  find-up@8.0.0:
    +    resolution: {integrity: sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==}
    +    engines: {node: '>=20'}
    +
    +  find-yarn-workspace-root@1.2.1:
    +    resolution: {integrity: sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==}
    +
    +  flatted@3.4.2:
    +    resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
    +
    +  for-each@0.3.5:
    +    resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
    +    engines: {node: '>= 0.4'}
    +
    +  for-in@1.0.2:
    +    resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  foreach@2.0.6:
    +    resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==}
    +
    +  formatly@0.3.0:
    +    resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==}
    +    engines: {node: '>=18.3.0'}
    +    hasBin: true
    +
    +  fragment-cache@0.2.1:
    +    resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  fs-extra@4.0.3:
    +    resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==}
    +
    +  fs-extra@7.0.1:
    +    resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
    +    engines: {node: '>=6 <7 || >=8'}
    +
    +  fs-extra@8.1.0:
    +    resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
    +    engines: {node: '>=6 <7 || >=8'}
    +
    +  fs-extra@9.1.0:
    +    resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
    +    engines: {node: '>=10'}
    +
    +  fs.realpath@1.0.0:
    +    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
    +
    +  fsevents@2.3.2:
    +    resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
    +    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
    +    os: [darwin]
    +
    +  fsevents@2.3.3:
    +    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
    +    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
    +    os: [darwin]
    +
    +  function-bind@1.1.2:
    +    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
    +
    +  fwd-stream@1.0.4:
    +    resolution: {integrity: sha512-q2qaK2B38W07wfPSQDKMiKOD5Nzv2XyuvQlrmh1q0pxyHNanKHq8lwQ6n9zHucAwA5EbzRJKEgds2orn88rYTg==}
    +
    +  gensequence@8.0.8:
    +    resolution: {integrity: sha512-omMVniXEXpdx/vKxGnPRoO2394Otlze28TyxECbFVyoSpZ9H3EO7lemjcB12OpQJzRW4e5tt/dL1rOxry6aMHg==}
    +    engines: {node: '>=20'}
    +
    +  gensync@1.0.0-beta.2:
    +    resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
    +    engines: {node: '>=6.9.0'}
    +
    +  get-caller-file@2.0.5:
    +    resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
    +    engines: {node: 6.* || 8.* || >= 10.*}
    +
    +  get-intrinsic@1.3.0:
    +    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
    +    engines: {node: '>= 0.4'}
    +
    +  get-proto@1.0.1:
    +    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
    +    engines: {node: '>= 0.4'}
    +
    +  get-stream@6.0.1:
    +    resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
    +    engines: {node: '>=10'}
    +
    +  get-tsconfig@4.13.7:
    +    resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
    +
    +  get-value@2.0.6:
    +    resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  glob-parent@5.1.2:
    +    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
    +    engines: {node: '>= 6'}
    +
    +  glob@13.0.6:
    +    resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
    +    engines: {node: 18 || 20 || >=22}
    +
    +  glob@7.2.3:
    +    resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
    +    deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
    +
    +  global-directory@5.0.0:
    +    resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==}
    +    engines: {node: '>=20'}
    +
    +  global@4.4.0:
    +    resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
    +
    +  globby@11.1.0:
    +    resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
    +    engines: {node: '>=10'}
    +
    +  gopd@1.2.0:
    +    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
    +    engines: {node: '>= 0.4'}
    +
    +  graceful-fs@4.2.11:
    +    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
    +
    +  has-flag@4.0.0:
    +    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
    +    engines: {node: '>=8'}
    +
    +  has-property-descriptors@1.0.2:
    +    resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
    +
    +  has-symbols@1.1.0:
    +    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
    +    engines: {node: '>= 0.4'}
    +
    +  has-tostringtag@1.0.2:
    +    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
    +    engines: {node: '>= 0.4'}
    +
    +  has-value@0.3.1:
    +    resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==}
    +    engines: {node: '>=0.10.0'}
    +
    +  has-value@1.0.0:
    +    resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  has-values@0.1.4:
    +    resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  has-values@1.0.0:
    +    resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  hash-base@3.0.5:
    +    resolution: {integrity: sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==}
    +    engines: {node: '>= 0.10'}
    +
    +  hash-base@3.1.2:
    +    resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==}
    +    engines: {node: '>= 0.8'}
    +
    +  hash.js@1.1.7:
    +    resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
    +
    +  hasown@2.0.2:
    +    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
    +    engines: {node: '>= 0.4'}
    +
    +  hmac-drbg@1.0.1:
    +    resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
    +
    +  hookable@6.1.0:
    +    resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==}
    +
    +  html-encoding-sniffer@6.0.0:
    +    resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
    +    engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
    +
    +  http-proxy-agent@7.0.2:
    +    resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
    +    engines: {node: '>= 14'}
    +
    +  https-proxy-agent@7.0.6:
    +    resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
    +    engines: {node: '>= 14'}
    +
    +  human-id@4.1.3:
    +    resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==}
    +    hasBin: true
    +
    +  human-signals@2.1.0:
    +    resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
    +    engines: {node: '>=10.17.0'}
    +
    +  iconv-lite@0.7.2:
    +    resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  idb-wrapper@1.7.2:
    +    resolution: {integrity: sha512-zfNREywMuf0NzDo9mVsL0yegjsirJxHpKHvWcyRozIqQy89g0a3U+oBPOCN4cc0oCiOuYgZHimzaW/R46G1Mpg==}
    +
    +  ignore@5.3.2:
    +    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
    +    engines: {node: '>= 4'}
    +
    +  immutable@5.1.4:
    +    resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
    +
    +  import-fresh@2.0.0:
    +    resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==}
    +    engines: {node: '>=4'}
    +
    +  import-fresh@3.3.1:
    +    resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
    +    engines: {node: '>=6'}
    +
    +  import-meta-resolve@4.2.0:
    +    resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
    +
    +  import-without-cache@0.2.5:
    +    resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==}
    +    engines: {node: '>=20.19.0'}
    +
    +  indexof@0.0.1:
    +    resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==}
    +
    +  individual@2.0.0:
    +    resolution: {integrity: sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==}
    +
    +  inflight@1.0.6:
    +    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
    +    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
    +
    +  inherits@2.0.4:
    +    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
    +
    +  ini@1.3.8:
    +    resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
    +
    +  ini@6.0.0:
    +    resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==}
    +    engines: {node: ^20.17.0 || >=22.9.0}
    +
    +  ionicons@7.4.0:
    +    resolution: {integrity: sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==}
    +
    +  is-accessor-descriptor@1.0.1:
    +    resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==}
    +    engines: {node: '>= 0.10'}
    +
    +  is-arrayish@0.2.1:
    +    resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
    +
    +  is-buffer@1.1.6:
    +    resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
    +
    +  is-callable@1.2.7:
    +    resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
    +    engines: {node: '>= 0.4'}
    +
    +  is-core-module@2.16.1:
    +    resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
    +    engines: {node: '>= 0.4'}
    +
    +  is-data-descriptor@1.0.1:
    +    resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==}
    +    engines: {node: '>= 0.4'}
    +
    +  is-descriptor@0.1.7:
    +    resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==}
    +    engines: {node: '>= 0.4'}
    +
    +  is-descriptor@1.0.3:
    +    resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==}
    +    engines: {node: '>= 0.4'}
    +
    +  is-directory@0.3.1:
    +    resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  is-docker@2.2.1:
    +    resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
    +    engines: {node: '>=8'}
    +    hasBin: 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
    +
    +  is-extendable@0.1.1:
    +    resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  is-extendable@1.0.1:
    +    resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  is-extglob@2.1.1:
    +    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  is-fullwidth-code-point@2.0.0:
    +    resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
    +    engines: {node: '>=4'}
    +
    +  is-fullwidth-code-point@3.0.0:
    +    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
    +    engines: {node: '>=8'}
    +
    +  is-function@1.0.2:
    +    resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==}
    +
    +  is-glob@4.0.3:
    +    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  is-in-ssh@1.0.0:
    +    resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==}
    +    engines: {node: '>=20'}
    +
    +  is-inside-container@1.0.0:
    +    resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
    +    engines: {node: '>=14.16'}
    +    hasBin: true
    +
    +  is-number@3.0.0:
    +    resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  is-number@7.0.0:
    +    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
    +    engines: {node: '>=0.12.0'}
    +
    +  is-object@0.1.2:
    +    resolution: {integrity: sha512-GkfZZlIZtpkFrqyAXPQSRBMsaHAw+CgoKe2HXAkjd/sfoI9+hS8PT4wg2rJxdQyUKr7N2vHJbg7/jQtE5l5vBQ==}
    +
    +  is-plain-object@2.0.4:
    +    resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
    +    engines: {node: '>=0.10.0'}
    +
    +  is-port-reachable@4.0.0:
    +    resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==}
    +    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
    +
    +  is-potential-custom-element-name@1.0.1:
    +    resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
    +
    +  is-safe-filename@0.1.1:
    +    resolution: {integrity: sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==}
    +    engines: {node: '>=20'}
    +
    +  is-stream@2.0.1:
    +    resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
    +    engines: {node: '>=8'}
    +
    +  is-subdir@1.2.0:
    +    resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
    +    engines: {node: '>=4'}
    +
    +  is-typed-array@1.1.15:
    +    resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
    +    engines: {node: '>= 0.4'}
    +
    +  is-windows@1.0.2:
    +    resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  is-wsl@2.2.0:
    +    resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
    +    engines: {node: '>=8'}
    +
    +  is-wsl@3.1.1:
    +    resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
    +    engines: {node: '>=16'}
    +
    +  is@0.2.7:
    +    resolution: {integrity: sha512-ajQCouIvkcSnl2iRdK70Jug9mohIHVX9uKpoWnl115ov0R5mzBvRrXxrnHbsA+8AdwCwc/sfw7HXmd4I5EJBdQ==}
    +
    +  isarray@0.0.1:
    +    resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
    +
    +  isarray@1.0.0:
    +    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
    +
    +  isarray@2.0.5:
    +    resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
    +
    +  isbuffer@0.0.0:
    +    resolution: {integrity: sha512-xU+NoHp+YtKQkaM2HsQchYn0sltxMxew0HavMfHbjnucBoTSGbw745tL+Z7QBANleWM1eEQMenEpi174mIeS4g==}
    +
    +  isexe@2.0.0:
    +    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
    +
    +  isobject@2.1.0:
    +    resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  isobject@3.0.1:
    +    resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  jiti@2.6.1:
    +    resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
    +    hasBin: true
    +
    +  js-tokens@4.0.0:
    +    resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
    +
    +  js-yaml@3.14.2:
    +    resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
    +    hasBin: true
    +
    +  js-yaml@4.1.1:
    +    resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
    +    hasBin: true
    +
    +  jsdom@28.0.0:
    +    resolution: {integrity: sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==}
    +    engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
    +    peerDependencies:
    +      canvas: ^3.0.0
    +    peerDependenciesMeta:
    +      canvas:
    +        optional: true
    +
    +  jsesc@3.1.0:
    +    resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
    +    engines: {node: '>=6'}
    +    hasBin: true
    +
    +  json-parse-better-errors@1.0.2:
    +    resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
    +
    +  json-schema-traverse@1.0.0:
    +    resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
    +
    +  json5@1.0.2:
    +    resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
    +    hasBin: true
    +
    +  json5@2.2.3:
    +    resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
    +    engines: {node: '>=6'}
    +    hasBin: true
    +
    +  jsonfile@4.0.0:
    +    resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
    +
    +  jsonfile@6.2.0:
    +    resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
    +
    +  keycode@2.2.1:
    +    resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==}
    +
    +  kind-of@3.2.2:
    +    resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  kind-of@4.0.0:
    +    resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  kind-of@6.0.3:
    +    resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  kleur@3.0.3:
    +    resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
    +    engines: {node: '>=6'}
    +
    +  knip@6.1.0:
    +    resolution: {integrity: sha512-n5eVbJP7HXmwTsiJcELWJe2O1ESxyCTNxJzRTIECDYDTM465qnqk7fL2dv6ae3NUFvFWorZvGlh9mcwxwJ5Xgw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    hasBin: true
    +
    +  launch-editor@2.13.0:
    +    resolution: {integrity: sha512-u+9asUHMJ99lA15VRMXw5XKfySFR9dGXwgsgS14YTbUq3GITP58mIM32At90P5fZ+MUId5Yw+IwI/yKub7jnCQ==}
    +
    +  level-blobs@0.1.7:
    +    resolution: {integrity: sha512-n0iYYCGozLd36m/Pzm206+brIgXP8mxPZazZ6ZvgKr+8YwOZ8/PPpYC5zMUu2qFygRN8RO6WC/HH3XWMW7RMVg==}
    +
    +  level-filesystem@1.2.0:
    +    resolution: {integrity: sha512-PhXDuCNYpngpxp3jwMT9AYBMgOvB6zxj3DeuIywNKmZqFj2djj9XfT2XDVslfqmo0Ip79cAd3SBy3FsfOZPJ1g==}
    +
    +  level-fix-range@1.0.2:
    +    resolution: {integrity: sha512-9llaVn6uqBiSlBP+wKiIEoBa01FwEISFgHSZiyec2S0KpyLUkGR4afW/FCZ/X8y+QJvzS0u4PGOlZDdh1/1avQ==}
    +
    +  level-fix-range@2.0.0:
    +    resolution: {integrity: sha512-WrLfGWgwWbYPrHsYzJau+5+te89dUbENBg3/lsxOs4p2tYOhCHjbgXxBAj4DFqp3k/XBwitcRXoCh8RoCogASA==}
    +
    +  level-hooks@4.5.0:
    +    resolution: {integrity: sha512-fxLNny/vL/G4PnkLhWsbHnEaRi+A/k8r5EH/M77npZwYL62RHi2fV0S824z3QdpAk6VTgisJwIRywzBHLK4ZVA==}
    +
    +  level-js@2.2.4:
    +    resolution: {integrity: sha512-lZtjt4ZwHE00UMC1vAb271p9qzg8vKlnDeXfIesH3zL0KxhHRDjClQLGLWhyR0nK4XARnd4wc/9eD1ffd4PshQ==}
    +    deprecated: Superseded by browser-level (https://github.com/Level/community#faq)
    +
    +  level-peek@1.0.6:
    +    resolution: {integrity: sha512-TKEzH5TxROTjQxWMczt9sizVgnmJ4F3hotBI48xCTYvOKd/4gA/uY0XjKkhJFo6BMic8Tqjf6jFMLWeg3MAbqQ==}
    +
    +  level-sublevel@5.2.3:
    +    resolution: {integrity: sha512-tO8jrFp+QZYrxx/Gnmjawuh1UBiifpvKNAcm4KCogesWr1Nm2+ckARitf+Oo7xg4OHqMW76eAqQ204BoIlscjA==}
    +
    +  levelup@0.18.6:
    +    resolution: {integrity: sha512-uB0auyRqIVXx+hrpIUtol4VAPhLRcnxcOsd2i2m6rbFIDarO5dnrupLOStYYpEcu8ZT087Z9HEuYw1wjr6RL6Q==}
    +    deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
    +
    +  lightningcss-android-arm64@1.32.0:
    +    resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  lightningcss-darwin-arm64@1.32.0:
    +    resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  lightningcss-darwin-x64@1.32.0:
    +    resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  lightningcss-freebsd-x64@1.32.0:
    +    resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [x64]
    +    os: [freebsd]
    +
    +  lightningcss-linux-arm-gnueabihf@1.32.0:
    +    resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [arm]
    +    os: [linux]
    +
    +  lightningcss-linux-arm64-gnu@1.32.0:
    +    resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  lightningcss-linux-arm64-musl@1.32.0:
    +    resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  lightningcss-linux-x64-gnu@1.32.0:
    +    resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [glibc]
    +
    +  lightningcss-linux-x64-musl@1.32.0:
    +    resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: [musl]
    +
    +  lightningcss-win32-arm64-msvc@1.32.0:
    +    resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  lightningcss-win32-x64-msvc@1.32.0:
    +    resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
    +    engines: {node: '>= 12.0.0'}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  lightningcss@1.32.0:
    +    resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
    +    engines: {node: '>= 12.0.0'}
    +
    +  lilconfig@3.1.3:
    +    resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
    +    engines: {node: '>=14'}
    +
    +  linaria@1.4.1:
    +    resolution: {integrity: sha512-KP3Dt+aH0XLJ6vUBjVu4/u0kkpChU8RRTGw6zA94SgjVIWG+2i6Dhnbd+ZlXCumbS5xonley/TNQLr3WQEUaCw==}
    +    hasBin: true
    +    peerDependencies:
    +      '@babel/core': '>=7'
    +
    +  loader-utils@1.4.2:
    +    resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==}
    +    engines: {node: '>=4.0.0'}
    +
    +  local-pkg@1.1.2:
    +    resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
    +    engines: {node: '>=14'}
    +
    +  locate-path@3.0.0:
    +    resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
    +    engines: {node: '>=6'}
    +
    +  locate-path@5.0.0:
    +    resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
    +    engines: {node: '>=8'}
    +
    +  locate-path@8.0.0:
    +    resolution: {integrity: sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==}
    +    engines: {node: '>=20'}
    +
    +  lodash-es@4.17.23:
    +    resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
    +
    +  lodash.debounce@4.0.8:
    +    resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
    +
    +  lodash.startcase@4.4.0:
    +    resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
    +
    +  lodash@4.17.23:
    +    resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
    +
    +  lru-cache@11.2.7:
    +    resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
    +    engines: {node: 20 || >=22}
    +
    +  lru-cache@5.1.1:
    +    resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
    +
    +  ltgt@2.2.1:
    +    resolution: {integrity: sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==}
    +
    +  m3u8-parser@4.8.0:
    +    resolution: {integrity: sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==}
    +
    +  magic-string@0.30.21:
    +    resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
    +
    +  map-cache@0.2.2:
    +    resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  map-visit@1.0.0:
    +    resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
    +    engines: {node: '>=0.10.0'}
    +
    +  math-intrinsics@1.1.0:
    +    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
    +    engines: {node: '>= 0.4'}
    +
    +  md5.js@1.3.5:
    +    resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
    +
    +  mdn-data@2.27.1:
    +    resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
    +
    +  memory-fs@0.5.0:
    +    resolution: {integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==}
    +    engines: {node: '>=4.3.0 <5.0.0 || >=5.10'}
    +
    +  merge-stream@2.0.0:
    +    resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
    +
    +  merge2@1.4.1:
    +    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
    +    engines: {node: '>= 8'}
    +
    +  micromatch@3.1.10:
    +    resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  micromatch@4.0.8:
    +    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
    +    engines: {node: '>=8.6'}
    +
    +  miller-rabin@4.0.1:
    +    resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==}
    +    hasBin: true
    +
    +  mime-db@1.33.0:
    +    resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==}
    +    engines: {node: '>= 0.6'}
    +
    +  mime-db@1.52.0:
    +    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
    +    engines: {node: '>= 0.6'}
    +
    +  mime-types@2.1.18:
    +    resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==}
    +    engines: {node: '>= 0.6'}
    +
    +  mimic-fn@2.1.0:
    +    resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
    +    engines: {node: '>=6'}
    +
    +  min-document@2.19.2:
    +    resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==}
    +
    +  minimalistic-assert@1.0.1:
    +    resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
    +
    +  minimalistic-crypto-utils@1.0.1:
    +    resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==}
    +
    +  minimatch@10.2.2:
    +    resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==}
    +    engines: {node: 18 || 20 || >=22}
    +
    +  minimatch@3.1.2:
    +    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
    +
    +  minimist@1.2.8:
    +    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
    +
    +  minipass@7.1.3:
    +    resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
    +    engines: {node: '>=16 || 14 >=14.17'}
    +
    +  mixin-deep@1.3.2:
    +    resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  mkdirp@0.5.6:
    +    resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
    +    hasBin: true
    +
    +  mlly@1.8.0:
    +    resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
    +
    +  mpd-parser@0.22.1:
    +    resolution: {integrity: sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==}
    +    hasBin: true
    +
    +  mri@1.2.0:
    +    resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
    +    engines: {node: '>=4'}
    +
    +  mrmime@2.0.1:
    +    resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
    +    engines: {node: '>=10'}
    +
    +  ms@2.0.0:
    +    resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
    +
    +  ms@2.1.3:
    +    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
    +
    +  mux.js@6.0.1:
    +    resolution: {integrity: sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==}
    +    engines: {node: '>=8', npm: '>=5'}
    +    hasBin: true
    +
    +  nanoid@3.3.11:
    +    resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
    +    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
    +    hasBin: true
    +
    +  nanomatch@1.2.13:
    +    resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  negotiator@0.6.4:
    +    resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
    +    engines: {node: '>= 0.6'}
    +
    +  node-addon-api@7.1.1:
    +    resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
    +
    +  node-releases@2.0.27:
    +    resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
    +
    +  normalize-path@3.0.0:
    +    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  normalize.css@8.0.1:
    +    resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==}
    +
    +  npm-run-path@4.0.1:
    +    resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
    +    engines: {node: '>=8'}
    +
    +  nwsapi@2.2.23:
    +    resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==}
    +
    +  object-copy@0.1.0:
    +    resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  object-keys@0.2.0:
    +    resolution: {integrity: sha512-XODjdR2pBh/1qrjPcbSeSgEtKbYo7LqYNq64/TPuCf7j9SfDD3i21yatKoIy39yIWNvVM59iutfQQpCv1RfFzA==}
    +    deprecated: Please update to the latest object-keys
    +
    +  object-keys@0.4.0:
    +    resolution: {integrity: sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==}
    +
    +  object-visit@1.0.1:
    +    resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  object.pick@1.3.0:
    +    resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  obug@2.1.1:
    +    resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
    +
    +  octal@1.0.0:
    +    resolution: {integrity: sha512-nnda7W8d+A3vEIY+UrDQzzboPf1vhs4JYVhff5CDkq9QNoZY7Xrxeo/htox37j9dZf7yNHevZzqtejWgy1vCqQ==}
    +
    +  on-headers@1.1.0:
    +    resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
    +    engines: {node: '>= 0.8'}
    +
    +  once@1.4.0:
    +    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
    +
    +  onetime@5.1.2:
    +    resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
    +    engines: {node: '>=6'}
    +
    +  open@11.0.0:
    +    resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
    +    engines: {node: '>=20'}
    +
    +  outdent@0.5.0:
    +    resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
    +
    +  oxc-parser@0.121.0:
    +    resolution: {integrity: sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +
    +  oxc-resolver@11.19.1:
    +    resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==}
    +
    +  oxfmt@0.42.0:
    +    resolution: {integrity: sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    hasBin: true
    +
    +  oxlint@1.57.0:
    +    resolution: {integrity: sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    hasBin: true
    +    peerDependencies:
    +      oxlint-tsgolint: '>=0.15.0'
    +    peerDependenciesMeta:
    +      oxlint-tsgolint:
    +        optional: true
    +
    +  p-filter@2.1.0:
    +    resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==}
    +    engines: {node: '>=8'}
    +
    +  p-limit@2.3.0:
    +    resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
    +    engines: {node: '>=6'}
    +
    +  p-limit@4.0.0:
    +    resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
    +    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
    +
    +  p-locate@3.0.0:
    +    resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
    +    engines: {node: '>=6'}
    +
    +  p-locate@4.1.0:
    +    resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
    +    engines: {node: '>=8'}
    +
    +  p-locate@6.0.0:
    +    resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
    +    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
    +
    +  p-map@2.1.0:
    +    resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
    +    engines: {node: '>=6'}
    +
    +  p-try@2.2.0:
    +    resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
    +    engines: {node: '>=6'}
    +
    +  package-json-from-dist@1.0.1:
    +    resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
    +
    +  package-manager-detector@0.2.11:
    +    resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
    +
    +  parent-module@1.0.1:
    +    resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
    +    engines: {node: '>=6'}
    +
    +  parent-module@2.0.0:
    +    resolution: {integrity: sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==}
    +    engines: {node: '>=8'}
    +
    +  parse-asn1@5.1.9:
    +    resolution: {integrity: sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==}
    +    engines: {node: '>= 0.10'}
    +
    +  parse-json@4.0.0:
    +    resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
    +    engines: {node: '>=4'}
    +
    +  parse5@7.2.1:
    +    resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==}
    +
    +  parse5@8.0.0:
    +    resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
    +
    +  pascalcase@0.1.1:
    +    resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  path-exists@3.0.0:
    +    resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
    +    engines: {node: '>=4'}
    +
    +  path-exists@4.0.0:
    +    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
    +    engines: {node: '>=8'}
    +
    +  path-is-absolute@1.0.1:
    +    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  path-is-inside@1.0.2:
    +    resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==}
    +
    +  path-key@3.1.1:
    +    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
    +    engines: {node: '>=8'}
    +
    +  path-parse@1.0.7:
    +    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
    +
    +  path-scurry@2.0.2:
    +    resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
    +    engines: {node: 18 || 20 || >=22}
    +
    +  path-to-regexp@3.3.0:
    +    resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==}
    +
    +  path-type@4.0.0:
    +    resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
    +    engines: {node: '>=8'}
    +
    +  pathe@2.0.3:
    +    resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
    +
    +  pbkdf2@3.1.5:
    +    resolution: {integrity: sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==}
    +    engines: {node: '>= 0.10'}
    +
    +  picocolors@0.2.1:
    +    resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==}
    +
    +  picocolors@1.1.1:
    +    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
    +
    +  picomatch@2.3.1:
    +    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
    +    engines: {node: '>=8.6'}
    +
    +  picomatch@4.0.4:
    +    resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
    +    engines: {node: '>=12'}
    +
    +  pify@4.0.1:
    +    resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
    +    engines: {node: '>=6'}
    +
    +  pkcs7@1.0.4:
    +    resolution: {integrity: sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==}
    +    hasBin: true
    +
    +  pkg-types@1.3.1:
    +    resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
    +
    +  pkg-types@2.3.0:
    +    resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
    +
    +  playwright-core@1.58.2:
    +    resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
    +    engines: {node: '>=18'}
    +    hasBin: true
    +
    +  playwright@1.58.2:
    +    resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==}
    +    engines: {node: '>=18'}
    +    hasBin: true
    +
    +  pngjs@7.0.0:
    +    resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
    +    engines: {node: '>=14.19.0'}
    +
    +  posix-character-classes@0.1.1:
    +    resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  possible-typed-array-names@1.1.0:
    +    resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
    +    engines: {node: '>= 0.4'}
    +
    +  postcss-load-config@6.0.1:
    +    resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
    +    engines: {node: '>= 18'}
    +    peerDependencies:
    +      jiti: '>=1.21.0'
    +      postcss: '>=8.0.9'
    +      tsx: ^4.8.1
    +      yaml: ^2.4.2
    +    peerDependenciesMeta:
    +      jiti:
    +        optional: true
    +      postcss:
    +        optional: true
    +      tsx:
    +        optional: true
    +      yaml:
    +        optional: true
    +
    +  postcss-safe-parser@7.0.1:
    +    resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==}
    +    engines: {node: '>=18.0'}
    +    peerDependencies:
    +      postcss: ^8.4.31
    +
    +  postcss-selector-parser@7.1.1:
    +    resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
    +    engines: {node: '>=4'}
    +
    +  postcss@7.0.39:
    +    resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==}
    +    engines: {node: '>=6.0.0'}
    +
    +  postcss@8.5.6:
    +    resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
    +    engines: {node: ^10 || ^12 || >=14}
    +
    +  powershell-utils@0.1.0:
    +    resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
    +    engines: {node: '>=20'}
    +
    +  prettier@2.8.8:
    +    resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
    +    engines: {node: '>=10.13.0'}
    +    hasBin: true
    +
    +  prettier@3.8.1:
    +    resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
    +    engines: {node: '>=14'}
    +    hasBin: true
    +
    +  process-es6@0.11.6:
    +    resolution: {integrity: sha512-GYBRQtL4v3wgigq10Pv58jmTbFXlIiTbSfgnNqZLY0ldUPqy1rRxDI5fCjoCpnM6TqmHQI8ydzTBXW86OYc0gA==}
    +
    +  process-nextick-args@2.0.1:
    +    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
    +
    +  process@0.11.10:
    +    resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
    +    engines: {node: '>= 0.6.0'}
    +
    +  prompts@2.4.2:
    +    resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
    +    engines: {node: '>= 6'}
    +
    +  prr@0.0.0:
    +    resolution: {integrity: sha512-LmUECmrW7RVj6mDWKjTXfKug7TFGdiz9P18HMcO4RHL+RW7MCOGNvpj5j47Rnp6ne6r4fZ2VzyUWEpKbg+tsjQ==}
    +
    +  prr@1.0.1:
    +    resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
    +
    +  public-encrypt@4.0.3:
    +    resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
    +
    +  punycode@2.3.1:
    +    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
    +    engines: {node: '>=6'}
    +
    +  quansync@0.2.11:
    +    resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
    +
    +  quansync@1.0.0:
    +    resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
    +
    +  queue-microtask@1.2.3:
    +    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
    +
    +  randombytes@2.1.0:
    +    resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
    +
    +  randomfill@1.0.4:
    +    resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==}
    +
    +  range-parser@1.2.0:
    +    resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==}
    +    engines: {node: '>= 0.6'}
    +
    +  rc@1.2.8:
    +    resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
    +    hasBin: true
    +
    +  react-is@16.13.1:
    +    resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
    +
    +  read-yaml-file@1.1.0:
    +    resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
    +    engines: {node: '>=6'}
    +
    +  readable-stream@1.0.34:
    +    resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==}
    +
    +  readable-stream@1.1.14:
    +    resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
    +
    +  readable-stream@2.3.8:
    +    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
    +
    +  readdirp@4.1.2:
    +    resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
    +    engines: {node: '>= 14.18.0'}
    +
    +  regex-not@1.0.2:
    +    resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==}
    +    engines: {node: '>=0.10.0'}
    +
    +  registry-auth-token@3.3.2:
    +    resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==}
    +
    +  registry-url@3.1.0:
    +    resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  repeat-element@1.1.4:
    +    resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  repeat-string@1.6.1:
    +    resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
    +    engines: {node: '>=0.10'}
    +
    +  require-directory@2.1.1:
    +    resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
    +    engines: {node: '>=0.10.0'}
    +
    +  require-from-string@2.0.2:
    +    resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  require-main-filename@2.0.0:
    +    resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
    +
    +  resolve-from@3.0.0:
    +    resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==}
    +    engines: {node: '>=4'}
    +
    +  resolve-from@4.0.0:
    +    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
    +    engines: {node: '>=4'}
    +
    +  resolve-from@5.0.0:
    +    resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
    +    engines: {node: '>=8'}
    +
    +  resolve-pkg-maps@1.0.0:
    +    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
    +
    +  resolve-url@0.2.1:
    +    resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
    +    deprecated: https://github.com/lydell/resolve-url#deprecated
    +
    +  resolve@1.22.11:
    +    resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
    +    engines: {node: '>= 0.4'}
    +    hasBin: true
    +
    +  ret@0.1.15:
    +    resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==}
    +    engines: {node: '>=0.12'}
    +
    +  reusify@1.1.0:
    +    resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
    +    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
    +
    +  rimraf@6.1.3:
    +    resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==}
    +    engines: {node: 20 || >=22}
    +    hasBin: true
    +
    +  ripemd160@2.0.3:
    +    resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==}
    +    engines: {node: '>= 0.8'}
    +
    +  rolldown-plugin-dts@0.23.2:
    +    resolution: {integrity: sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ==}
    +    engines: {node: '>=20.19.0'}
    +    peerDependencies:
    +      '@ts-macro/tsc': ^0.3.6
    +      '@typescript/native-preview': '>=7.0.0-dev.20260325.1'
    +      rolldown: ^1.0.0-rc.12
    +      typescript: ^5.0.0 || ^6.0.0
    +      vue-tsc: ~3.2.0
    +    peerDependenciesMeta:
    +      '@ts-macro/tsc':
    +        optional: true
    +      '@typescript/native-preview':
    +        optional: true
    +      typescript:
    +        optional: true
    +      vue-tsc:
    +        optional: true
    +
    +  rolldown@1.0.0-rc.12:
    +    resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    hasBin: true
    +
    +  rolldown@1.0.0-rc.15:
    +    resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==}
    +    engines: {node: ^20.19.0 || >=22.12.0}
    +    hasBin: true
    +
    +  rollup-plugin-css-only@2.1.0:
    +    resolution: {integrity: sha512-pfdcqAWEmRMFy+ABXAQPA/DKyPqLuBTOf+lWSOgtrVs1v/q7DSXzYa9QZg4myd8/1F7NHcdvPkWnfWqMxq9vrw==}
    +    engines: {node: '>=10.12.0'}
    +    peerDependencies:
    +      rollup: 1 || 2
    +
    +  rollup-plugin-node-builtins@2.1.2:
    +    resolution: {integrity: sha512-bxdnJw8jIivr2yEyt8IZSGqZkygIJOGAWypXvHXnwKAbUcN4Q/dGTx7K0oAJryC/m6aq6tKutltSeXtuogU6sw==}
    +
    +  rollup-pluginutils@2.8.2:
    +    resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==}
    +
    +  rollup@4.57.1:
    +    resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==}
    +    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
    +    hasBin: true
    +
    +  run-applescript@7.1.0:
    +    resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
    +    engines: {node: '>=18'}
    +
    +  run-parallel@1.2.0:
    +    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
    +
    +  rust-result@1.0.0:
    +    resolution: {integrity: sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==}
    +
    +  rxjs@7.8.2:
    +    resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
    +
    +  safe-buffer@5.1.2:
    +    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
    +
    +  safe-buffer@5.2.1:
    +    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
    +
    +  safe-json-parse@4.0.0:
    +    resolution: {integrity: sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==}
    +
    +  safe-regex@1.1.0:
    +    resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==}
    +
    +  safer-buffer@2.1.2:
    +    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
    +
    +  sass-embedded-all-unknown@1.97.3:
    +    resolution: {integrity: sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg==}
    +    cpu: ['!arm', '!arm64', '!riscv64', '!x64']
    +
    +  sass-embedded-android-arm64@1.97.3:
    +    resolution: {integrity: sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [arm64]
    +    os: [android]
    +
    +  sass-embedded-android-arm@1.97.3:
    +    resolution: {integrity: sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [arm]
    +    os: [android]
    +
    +  sass-embedded-android-riscv64@1.97.3:
    +    resolution: {integrity: sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [riscv64]
    +    os: [android]
    +
    +  sass-embedded-android-x64@1.97.3:
    +    resolution: {integrity: sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [x64]
    +    os: [android]
    +
    +  sass-embedded-darwin-arm64@1.97.3:
    +    resolution: {integrity: sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [arm64]
    +    os: [darwin]
    +
    +  sass-embedded-darwin-x64@1.97.3:
    +    resolution: {integrity: sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [x64]
    +    os: [darwin]
    +
    +  sass-embedded-linux-arm64@1.97.3:
    +    resolution: {integrity: sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: glibc
    +
    +  sass-embedded-linux-arm@1.97.3:
    +    resolution: {integrity: sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [arm]
    +    os: [linux]
    +    libc: glibc
    +
    +  sass-embedded-linux-musl-arm64@1.97.3:
    +    resolution: {integrity: sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [arm64]
    +    os: [linux]
    +    libc: musl
    +
    +  sass-embedded-linux-musl-arm@1.97.3:
    +    resolution: {integrity: sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [arm]
    +    os: [linux]
    +    libc: musl
    +
    +  sass-embedded-linux-musl-riscv64@1.97.3:
    +    resolution: {integrity: sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: musl
    +
    +  sass-embedded-linux-musl-x64@1.97.3:
    +    resolution: {integrity: sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: musl
    +
    +  sass-embedded-linux-riscv64@1.97.3:
    +    resolution: {integrity: sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [riscv64]
    +    os: [linux]
    +    libc: glibc
    +
    +  sass-embedded-linux-x64@1.97.3:
    +    resolution: {integrity: sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [x64]
    +    os: [linux]
    +    libc: glibc
    +
    +  sass-embedded-unknown-all@1.97.3:
    +    resolution: {integrity: sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q==}
    +    os: ['!android', '!darwin', '!linux', '!win32']
    +
    +  sass-embedded-win32-arm64@1.97.3:
    +    resolution: {integrity: sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [arm64]
    +    os: [win32]
    +
    +  sass-embedded-win32-x64@1.97.3:
    +    resolution: {integrity: sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw==}
    +    engines: {node: '>=14.0.0'}
    +    cpu: [x64]
    +    os: [win32]
    +
    +  sass-embedded@1.97.3:
    +    resolution: {integrity: sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA==}
    +    engines: {node: '>=16.0.0'}
    +    hasBin: true
    +
    +  sass@1.97.3:
    +    resolution: {integrity: sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==}
    +    engines: {node: '>=14.0.0'}
    +    hasBin: true
    +
    +  saxes@6.0.0:
    +    resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
    +    engines: {node: '>=v12.22.7'}
    +
    +  semver@2.3.2:
    +    resolution: {integrity: sha512-abLdIKCosKfpnmhS52NCTjO4RiLspDfsn37prjzGrp9im5DPJOgh82Os92vtwGh6XdQryKI/7SREZnV+aqiXrA==}
    +    hasBin: true
    +
    +  semver@6.3.1:
    +    resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
    +    hasBin: true
    +
    +  semver@7.7.4:
    +    resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
    +    engines: {node: '>=10'}
    +    hasBin: true
    +
    +  serve-handler@6.1.6:
    +    resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==}
    +
    +  serve@14.2.5:
    +    resolution: {integrity: sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==}
    +    engines: {node: '>= 14'}
    +    hasBin: true
    +
    +  set-blocking@2.0.0:
    +    resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
    +
    +  set-function-length@1.2.2:
    +    resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
    +    engines: {node: '>= 0.4'}
    +
    +  set-value@2.0.1:
    +    resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  sha.js@2.4.12:
    +    resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==}
    +    engines: {node: '>= 0.10'}
    +    hasBin: true
    +
    +  shebang-command@2.0.0:
    +    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
    +    engines: {node: '>=8'}
    +
    +  shebang-regex@3.0.0:
    +    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
    +    engines: {node: '>=8'}
    +
    +  shell-quote@1.8.3:
    +    resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
    +    engines: {node: '>= 0.4'}
    +
    +  siginfo@2.0.0:
    +    resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
    +
    +  signal-exit@3.0.7:
    +    resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
    +
    +  signal-exit@4.1.0:
    +    resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
    +    engines: {node: '>=14'}
    +
    +  sirv@3.0.2:
    +    resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
    +    engines: {node: '>=18'}
    +
    +  sisteransi@1.0.5:
    +    resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
    +
    +  slash@3.0.0:
    +    resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
    +    engines: {node: '>=8'}
    +
    +  smol-toml@1.6.1:
    +    resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==}
    +    engines: {node: '>= 18'}
    +
    +  snapdragon-node@2.1.1:
    +    resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  snapdragon-util@3.0.1:
    +    resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  snapdragon@0.8.2:
    +    resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  source-map-js@1.2.1:
    +    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
    +    engines: {node: '>=0.10.0'}
    +
    +  source-map-resolve@0.5.3:
    +    resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==}
    +    deprecated: See https://github.com/lydell/source-map-resolve#deprecated
    +
    +  source-map-support@0.5.21:
    +    resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
    +
    +  source-map-url@0.4.1:
    +    resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==}
    +    deprecated: See https://github.com/lydell/source-map-url#deprecated
    +
    +  source-map@0.5.7:
    +    resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  source-map@0.6.1:
    +    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
    +    engines: {node: '>=0.10.0'}
    +
    +  spawndamnit@3.0.1:
    +    resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
    +
    +  split-string@3.1.0:
    +    resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  sprintf-js@1.0.3:
    +    resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
    +
    +  stackback@0.0.2:
    +    resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
    +
    +  static-extend@0.1.2:
    +    resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
    +    engines: {node: '>=0.10.0'}
    +
    +  std-env@4.0.0:
    +    resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==}
    +
    +  string-range@1.2.2:
    +    resolution: {integrity: sha512-tYft6IFi8SjplJpxCUxyqisD3b+R2CSkomrtJYCkvuf1KuCAWgz7YXt4O0jip7efpfCemwHEzTEAO8EuOYgh3w==}
    +
    +  string-width@3.1.0:
    +    resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==}
    +    engines: {node: '>=6'}
    +
    +  string-width@4.2.3:
    +    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
    +    engines: {node: '>=8'}
    +
    +  string-width@5.1.2:
    +    resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
    +    engines: {node: '>=12'}
    +
    +  string_decoder@0.10.31:
    +    resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
    +
    +  string_decoder@1.1.1:
    +    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
    +
    +  strip-ansi@5.2.0:
    +    resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
    +    engines: {node: '>=6'}
    +
    +  strip-ansi@6.0.1:
    +    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
    +    engines: {node: '>=8'}
    +
    +  strip-ansi@7.1.2:
    +    resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
    +    engines: {node: '>=12'}
    +
    +  strip-bom@3.0.0:
    +    resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
    +    engines: {node: '>=4'}
    +
    +  strip-final-newline@2.0.0:
    +    resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
    +    engines: {node: '>=6'}
    +
    +  strip-json-comments@2.0.1:
    +    resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  strip-json-comments@5.0.3:
    +    resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
    +    engines: {node: '>=14.16'}
    +
    +  stylis@3.5.4:
    +    resolution: {integrity: sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==}
    +
    +  supports-color@7.2.0:
    +    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
    +    engines: {node: '>=8'}
    +
    +  supports-color@8.1.1:
    +    resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
    +    engines: {node: '>=10'}
    +
    +  supports-preserve-symlinks-flag@1.0.0:
    +    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
    +    engines: {node: '>= 0.4'}
    +
    +  symbol-tree@3.2.4:
    +    resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
    +
    +  sync-child-process@1.0.2:
    +    resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==}
    +    engines: {node: '>=16.0.0'}
    +
    +  sync-message-port@1.2.0:
    +    resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==}
    +    engines: {node: '>=16.0.0'}
    +
    +  tapable@1.1.3:
    +    resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==}
    +    engines: {node: '>=6'}
    +
    +  term-size@2.2.1:
    +    resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
    +    engines: {node: '>=8'}
    +
    +  terser@5.37.0:
    +    resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==}
    +    engines: {node: '>=10'}
    +    hasBin: true
    +
    +  tinybench@2.9.0:
    +    resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
    +
    +  tinyexec@1.0.4:
    +    resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==}
    +    engines: {node: '>=18'}
    +
    +  tinyglobby@0.2.15:
    +    resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
    +    engines: {node: '>=12.0.0'}
    +
    +  tinypool@2.1.0:
    +    resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
    +    engines: {node: ^20.0.0 || >=22.0.0}
    +
    +  tinyrainbow@3.1.0:
    +    resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
    +    engines: {node: '>=14.0.0'}
    +
    +  tldts-core@7.0.23:
    +    resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==}
    +
    +  tldts@7.0.23:
    +    resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==}
    +    hasBin: true
    +
    +  to-buffer@1.2.2:
    +    resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==}
    +    engines: {node: '>= 0.4'}
    +
    +  to-object-path@0.3.0:
    +    resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  to-regex-range@2.1.1:
    +    resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  to-regex-range@5.0.1:
    +    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
    +    engines: {node: '>=8.0'}
    +
    +  to-regex@3.0.2:
    +    resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==}
    +    engines: {node: '>=0.10.0'}
    +
    +  totalist@3.0.1:
    +    resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
    +    engines: {node: '>=6'}
    +
    +  tough-cookie@6.0.0:
    +    resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
    +    engines: {node: '>=16'}
    +
    +  tr46@6.0.0:
    +    resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
    +    engines: {node: '>=20'}
    +
    +  tree-kill@1.2.2:
    +    resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
    +    hasBin: true
    +
    +  tsdown@0.21.7:
    +    resolution: {integrity: sha512-ukKIxKQzngkWvOYJAyptudclkm4VQqbjq+9HF5K5qDO8GJsYtMh8gIRwicbnZEnvFPr6mquFwYAVZ8JKt3rY2g==}
    +    engines: {node: '>=20.19.0'}
    +    hasBin: true
    +    peerDependencies:
    +      '@arethetypeswrong/core': ^0.18.1
    +      '@tsdown/css': 0.21.7
    +      '@tsdown/exe': 0.21.7
    +      '@vitejs/devtools': '*'
    +      publint: ^0.3.0
    +      typescript: ^5.0.0 || ^6.0.0
    +      unplugin-unused: ^0.5.0
    +    peerDependenciesMeta:
    +      '@arethetypeswrong/core':
    +        optional: true
    +      '@tsdown/css':
    +        optional: true
    +      '@tsdown/exe':
    +        optional: true
    +      '@vitejs/devtools':
    +        optional: true
    +      publint:
    +        optional: true
    +      typescript:
    +        optional: true
    +      unplugin-unused:
    +        optional: true
    +
    +  tslib@2.8.1:
    +    resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
    +
    +  tsx@4.21.0:
    +    resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
    +    engines: {node: '>=18.0.0'}
    +    hasBin: true
    +
    +  type-fest@2.19.0:
    +    resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
    +    engines: {node: '>=12.20'}
    +
    +  typed-array-buffer@1.0.3:
    +    resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
    +    engines: {node: '>= 0.4'}
    +
    +  typedarray-to-buffer@1.0.4:
    +    resolution: {integrity: sha512-vjMKrfSoUDN8/Vnqitw2FmstOfuJ73G6CrSEKnf11A6RmasVxHqfeBcnTb6RsL4pTMuV5Zsv9IiHRphMZyckUw==}
    +
    +  typedarray@0.0.6:
    +    resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
    +
    +  typescript@6.0.2:
    +    resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==}
    +    engines: {node: '>=14.17'}
    +    hasBin: true
    +
    +  ufo@1.6.3:
    +    resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
    +
    +  unbash@2.2.0:
    +    resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==}
    +    engines: {node: '>=14'}
    +
    +  unconfig-core@7.5.0:
    +    resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
    +
    +  undici-types@7.16.0:
    +    resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
    +
    +  undici@7.21.0:
    +    resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==}
    +    engines: {node: '>=20.18.1'}
    +
    +  unicorn-magic@0.3.0:
    +    resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
    +    engines: {node: '>=18'}
    +
    +  union-value@1.0.1:
    +    resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==}
    +    engines: {node: '>=0.10.0'}
    +
    +  universalify@0.1.2:
    +    resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
    +    engines: {node: '>= 4.0.0'}
    +
    +  universalify@2.0.1:
    +    resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
    +    engines: {node: '>= 10.0.0'}
    +
    +  unrun@0.2.34:
    +    resolution: {integrity: sha512-LyaghRBR++r7svhDK6tnDz2XaYHWdneBOA0jbS8wnRsHerI9MFljX4fIiTgbbNbEVzZ0C9P1OjWLLe1OqoaaEw==}
    +    engines: {node: '>=20.19.0'}
    +    hasBin: true
    +    peerDependencies:
    +      synckit: ^0.11.11
    +    peerDependenciesMeta:
    +      synckit:
    +        optional: true
    +
    +  unset-value@1.0.0:
    +    resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  update-browserslist-db@1.2.3:
    +    resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
    +    hasBin: true
    +    peerDependencies:
    +      browserslist: '>= 4.21.0'
    +
    +  update-check@1.5.4:
    +    resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==}
    +
    +  uri-js@4.4.1:
    +    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
    +
    +  urix@0.1.0:
    +    resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==}
    +    deprecated: Please see https://github.com/lydell/urix#deprecated
    +
    +  url-toolkit@2.2.5:
    +    resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==}
    +
    +  urlpattern-polyfill@8.0.2:
    +    resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
    +
    +  use@3.1.1:
    +    resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
    +    engines: {node: '>=0.10.0'}
    +
    +  util-deprecate@1.0.2:
    +    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
    +
    +  varint@6.0.0:
    +    resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
    +
    +  vary@1.1.2:
    +    resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
    +    engines: {node: '>= 0.8'}
    +
    +  video.js@7.21.7:
    +    resolution: {integrity: sha512-T2s3WFAht7Zjr2OSJamND9x9Dn2O+Z5WuHGdh8jI5SYh5mkMdVTQ7vSRmA5PYpjXJ2ycch6jpMjkJEIEU2xxqw==}
    +
    +  videojs-font@3.2.0:
    +    resolution: {integrity: sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==}
    +
    +  videojs-vtt.js@0.15.5:
    +    resolution: {integrity: sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==}
    +
    +  vite@6.4.1:
    +    resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
    +    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
    +    hasBin: true
    +    peerDependencies:
    +      '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
    +      jiti: '>=1.21.0'
    +      less: '*'
    +      lightningcss: ^1.21.0
    +      sass: '*'
    +      sass-embedded: '*'
    +      stylus: '*'
    +      sugarss: '*'
    +      terser: ^5.16.0
    +      tsx: ^4.8.1
    +      yaml: ^2.4.2
    +    peerDependenciesMeta:
    +      '@types/node':
    +        optional: true
    +      jiti:
    +        optional: true
    +      less:
    +        optional: true
    +      lightningcss:
    +        optional: true
    +      sass:
    +        optional: true
    +      sass-embedded:
    +        optional: true
    +      stylus:
    +        optional: true
    +      sugarss:
    +        optional: true
    +      terser:
    +        optional: true
    +      tsx:
    +        optional: true
    +      yaml:
    +        optional: true
    +
    +  vitest-environment-stencil@1.11.6:
    +    resolution: {integrity: sha512-HQzfsZ9WWqvlK3k/5PMUXYmiN2H29dLlM7oKg75/nMoZv0u5DeU5Xs6vTMxqePzXbCRvG/HW0SLUMQNRWnC4EQ==}
    +    peerDependencies:
    +      '@stencil/core': workspace:*
    +
    +  vitest@4.1.2:
    +    resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==}
    +    engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
    +    hasBin: true
    +    peerDependencies:
    +      '@edge-runtime/vm': '*'
    +      '@opentelemetry/api': ^1.9.0
    +      '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
    +      '@vitest/browser-playwright': 4.1.2
    +      '@vitest/browser-preview': 4.1.2
    +      '@vitest/browser-webdriverio': 4.1.2
    +      '@vitest/ui': 4.1.2
    +      happy-dom: '*'
    +      jsdom: '*'
    +      vite: ^6.0.0 || ^7.0.0 || ^8.0.0
    +    peerDependenciesMeta:
    +      '@edge-runtime/vm':
    +        optional: true
    +      '@opentelemetry/api':
    +        optional: true
    +      '@types/node':
    +        optional: true
    +      '@vitest/browser-playwright':
    +        optional: true
    +      '@vitest/browser-preview':
    +        optional: true
    +      '@vitest/browser-webdriverio':
    +        optional: true
    +      '@vitest/ui':
    +        optional: true
    +      happy-dom:
    +        optional: true
    +      jsdom:
    +        optional: true
    +
    +  vscode-languageserver-textdocument@1.0.12:
    +    resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==}
    +
    +  vscode-uri@3.1.0:
    +    resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
    +
    +  w3c-xmlserializer@5.0.0:
    +    resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
    +    engines: {node: '>=18'}
    +
    +  walk-up-path@4.0.0:
    +    resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
    +    engines: {node: 20 || >=22}
    +
    +  webidl-conversions@8.0.1:
    +    resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
    +    engines: {node: '>=20'}
    +
    +  whatwg-mimetype@5.0.0:
    +    resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
    +    engines: {node: '>=20'}
    +
    +  whatwg-url@16.0.0:
    +    resolution: {integrity: sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==}
    +    engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
    +
    +  which-module@2.0.1:
    +    resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
    +
    +  which-typed-array@1.1.20:
    +    resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
    +    engines: {node: '>= 0.4'}
    +
    +  which@2.0.2:
    +    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
    +    engines: {node: '>= 8'}
    +    hasBin: true
    +
    +  why-is-node-running@2.3.0:
    +    resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
    +    engines: {node: '>=8'}
    +    hasBin: true
    +
    +  widest-line@4.0.1:
    +    resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==}
    +    engines: {node: '>=12'}
    +
    +  wrap-ansi@5.1.0:
    +    resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==}
    +    engines: {node: '>=6'}
    +
    +  wrap-ansi@8.1.0:
    +    resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
    +    engines: {node: '>=12'}
    +
    +  wrappy@1.0.2:
    +    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
    +
    +  ws@8.19.0:
    +    resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
    +    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
    +
    +  wsl-utils@0.3.1:
    +    resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
    +    engines: {node: '>=20'}
    +
    +  xdg-basedir@5.1.0:
    +    resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==}
    +    engines: {node: '>=12'}
    +
    +  xml-name-validator@5.0.0:
    +    resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
    +    engines: {node: '>=18'}
    +
    +  xmlchars@2.2.0:
    +    resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
    +
    +  xtend@2.0.6:
    +    resolution: {integrity: sha512-fOZg4ECOlrMl+A6Msr7EIFcON1L26mb4NY5rurSkOex/TWhazOrg6eXD/B0XkuiYcYhQDWLXzQxLMVJ7LXwokg==}
    +    engines: {node: '>=0.4'}
    +
    +  xtend@2.1.2:
    +    resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==}
    +    engines: {node: '>=0.4'}
    +
    +  xtend@2.2.0:
    +    resolution: {integrity: sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==}
    +    engines: {node: '>=0.4'}
    +
    +  xtend@3.0.0:
    +    resolution: {integrity: sha512-sp/sT9OALMjRW1fKDlPeuSZlDQpkqReA0pyJukniWbTGoEKefHxhGJynE3PNhUMlcM8qWIjPwecwCw4LArS5Eg==}
    +    engines: {node: '>=0.4'}
    +
    +  y18n@4.0.3:
    +    resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
    +
    +  yallist@3.1.1:
    +    resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
    +
    +  yaml@2.8.3:
    +    resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
    +    engines: {node: '>= 14.6'}
    +    hasBin: true
    +
    +  yargs-parser@13.1.2:
    +    resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==}
    +
    +  yargs@13.3.2:
    +    resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==}
    +
    +  yocto-queue@1.2.2:
    +    resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
    +    engines: {node: '>=12.20'}
    +
    +  zod@4.3.6:
    +    resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
    +
    +snapshots:
    +
    +  '@acemir/cssom@0.9.31':
    +    optional: true
    +
    +  '@asamuzakjp/css-color@4.1.2':
    +    dependencies:
    +      '@csstools/css-calc': 3.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
    +      '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
    +      '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
    +      '@csstools/css-tokenizer': 4.0.0
    +      lru-cache: 11.2.7
    +    optional: true
    +
    +  '@asamuzakjp/dom-selector@6.7.8':
    +    dependencies:
    +      '@asamuzakjp/nwsapi': 2.3.9
    +      bidi-js: 1.0.3
    +      css-tree: 3.2.1
    +      is-potential-custom-element-name: 1.0.1
    +      lru-cache: 11.2.7
    +    optional: true
    +
    +  '@asamuzakjp/nwsapi@2.3.9':
    +    optional: true
    +
    +  '@babel/code-frame@7.29.0':
    +    dependencies:
    +      '@babel/helper-validator-identifier': 7.28.5
    +      js-tokens: 4.0.0
    +      picocolors: 1.1.1
    +
    +  '@babel/compat-data@7.29.0': {}
    +
    +  '@babel/core@7.29.0':
    +    dependencies:
    +      '@babel/code-frame': 7.29.0
    +      '@babel/generator': 7.29.1
    +      '@babel/helper-compilation-targets': 7.28.6
    +      '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
    +      '@babel/helpers': 7.28.6
    +      '@babel/parser': 7.29.0
    +      '@babel/template': 7.28.6
    +      '@babel/traverse': 7.29.0
    +      '@babel/types': 7.29.0
    +      '@jridgewell/remapping': 2.3.5
    +      convert-source-map: 2.0.0
    +      debug: 4.4.3
    +      gensync: 1.0.0-beta.2
    +      json5: 2.2.3
    +      semver: 6.3.1
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  '@babel/generator@7.29.1':
    +    dependencies:
    +      '@babel/parser': 7.29.0
    +      '@babel/types': 7.29.0
    +      '@jridgewell/gen-mapping': 0.3.13
    +      '@jridgewell/trace-mapping': 0.3.31
    +      jsesc: 3.1.0
    +
    +  '@babel/generator@8.0.0-rc.3':
    +    dependencies:
    +      '@babel/parser': 8.0.0-rc.3
    +      '@babel/types': 8.0.0-rc.3
    +      '@jridgewell/gen-mapping': 0.3.13
    +      '@jridgewell/trace-mapping': 0.3.31
    +      '@types/jsesc': 2.5.1
    +      jsesc: 3.1.0
    +
    +  '@babel/helper-compilation-targets@7.28.6':
    +    dependencies:
    +      '@babel/compat-data': 7.29.0
    +      '@babel/helper-validator-option': 7.27.1
    +      browserslist: 4.28.1
    +      lru-cache: 5.1.1
    +      semver: 6.3.1
    +
    +  '@babel/helper-define-polyfill-provider@0.6.6(@babel/core@7.29.0)':
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-compilation-targets': 7.28.6
    +      '@babel/helper-plugin-utils': 7.28.6
    +      debug: 4.4.3
    +      lodash.debounce: 4.0.8
    +      resolve: 1.22.11
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  '@babel/helper-globals@7.28.0': {}
    +
    +  '@babel/helper-module-imports@7.28.6':
    +    dependencies:
    +      '@babel/traverse': 7.29.0
    +      '@babel/types': 7.29.0
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-module-imports': 7.28.6
    +      '@babel/helper-validator-identifier': 7.28.5
    +      '@babel/traverse': 7.29.0
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  '@babel/helper-plugin-utils@7.28.6': {}
    +
    +  '@babel/helper-string-parser@7.27.1': {}
    +
    +  '@babel/helper-string-parser@8.0.0-rc.3': {}
    +
    +  '@babel/helper-validator-identifier@7.28.5': {}
    +
    +  '@babel/helper-validator-identifier@8.0.0-rc.3': {}
    +
    +  '@babel/helper-validator-option@7.27.1': {}
    +
    +  '@babel/helpers@7.28.6':
    +    dependencies:
    +      '@babel/template': 7.28.6
    +      '@babel/types': 7.29.0
    +
    +  '@babel/parser@7.29.0':
    +    dependencies:
    +      '@babel/types': 7.29.0
    +
    +  '@babel/parser@8.0.0-rc.3':
    +    dependencies:
    +      '@babel/types': 8.0.0-rc.3
    +
    +  '@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.29.0)':
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-plugin-utils': 7.28.6
    +      '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.29.0)
    +
    +  '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)':
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-plugin-utils': 7.28.6
    +
    +  '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.29.0)':
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-plugin-utils': 7.28.6
    +
    +  '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)':
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
    +      '@babel/helper-plugin-utils': 7.28.6
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)':
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-module-imports': 7.28.6
    +      '@babel/helper-plugin-utils': 7.28.6
    +      babel-plugin-polyfill-corejs2: 0.4.15(@babel/core@7.29.0)
    +      babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0)
    +      babel-plugin-polyfill-regenerator: 0.6.6(@babel/core@7.29.0)
    +      semver: 6.3.1
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)':
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-plugin-utils': 7.28.6
    +
    +  '@babel/runtime@7.28.6': {}
    +
    +  '@babel/template@7.28.6':
    +    dependencies:
    +      '@babel/code-frame': 7.29.0
    +      '@babel/parser': 7.29.0
    +      '@babel/types': 7.29.0
    +
    +  '@babel/traverse@7.29.0':
    +    dependencies:
    +      '@babel/code-frame': 7.29.0
    +      '@babel/generator': 7.29.1
    +      '@babel/helper-globals': 7.28.0
    +      '@babel/parser': 7.29.0
    +      '@babel/template': 7.28.6
    +      '@babel/types': 7.29.0
    +      debug: 4.4.3
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  '@babel/types@7.29.0':
    +    dependencies:
    +      '@babel/helper-string-parser': 7.27.1
    +      '@babel/helper-validator-identifier': 7.28.5
    +
    +  '@babel/types@8.0.0-rc.3':
    +    dependencies:
    +      '@babel/helper-string-parser': 8.0.0-rc.3
    +      '@babel/helper-validator-identifier': 8.0.0-rc.3
    +
    +  '@blazediff/core@1.9.1': {}
    +
    +  '@bufbuild/protobuf@2.11.0': {}
    +
    +  '@changesets/apply-release-plan@7.1.0':
    +    dependencies:
    +      '@changesets/config': 3.1.3
    +      '@changesets/get-version-range-type': 0.4.0
    +      '@changesets/git': 3.0.4
    +      '@changesets/should-skip-package': 0.1.2
    +      '@changesets/types': 6.1.0
    +      '@manypkg/get-packages': 1.1.3
    +      detect-indent: 6.1.0
    +      fs-extra: 7.0.1
    +      lodash.startcase: 4.4.0
    +      outdent: 0.5.0
    +      prettier: 2.8.8
    +      resolve-from: 5.0.0
    +      semver: 7.7.4
    +
    +  '@changesets/assemble-release-plan@6.0.9':
    +    dependencies:
    +      '@changesets/errors': 0.2.0
    +      '@changesets/get-dependents-graph': 2.1.3
    +      '@changesets/should-skip-package': 0.1.2
    +      '@changesets/types': 6.1.0
    +      '@manypkg/get-packages': 1.1.3
    +      semver: 7.7.4
    +
    +  '@changesets/changelog-git@0.2.1':
    +    dependencies:
    +      '@changesets/types': 6.1.0
    +
    +  '@changesets/cli@2.30.0(@types/node@24.10.13)':
    +    dependencies:
    +      '@changesets/apply-release-plan': 7.1.0
    +      '@changesets/assemble-release-plan': 6.0.9
    +      '@changesets/changelog-git': 0.2.1
    +      '@changesets/config': 3.1.3
    +      '@changesets/errors': 0.2.0
    +      '@changesets/get-dependents-graph': 2.1.3
    +      '@changesets/get-release-plan': 4.0.15
    +      '@changesets/git': 3.0.4
    +      '@changesets/logger': 0.1.1
    +      '@changesets/pre': 2.0.2
    +      '@changesets/read': 0.6.7
    +      '@changesets/should-skip-package': 0.1.2
    +      '@changesets/types': 6.1.0
    +      '@changesets/write': 0.4.0
    +      '@inquirer/external-editor': 1.0.3(@types/node@24.10.13)
    +      '@manypkg/get-packages': 1.1.3
    +      ansi-colors: 4.1.3
    +      enquirer: 2.4.1
    +      fs-extra: 7.0.1
    +      mri: 1.2.0
    +      package-manager-detector: 0.2.11
    +      picocolors: 1.1.1
    +      resolve-from: 5.0.0
    +      semver: 7.7.4
    +      spawndamnit: 3.0.1
    +      term-size: 2.2.1
    +    transitivePeerDependencies:
    +      - '@types/node'
    +
    +  '@changesets/config@3.1.3':
    +    dependencies:
    +      '@changesets/errors': 0.2.0
    +      '@changesets/get-dependents-graph': 2.1.3
    +      '@changesets/logger': 0.1.1
    +      '@changesets/should-skip-package': 0.1.2
    +      '@changesets/types': 6.1.0
    +      '@manypkg/get-packages': 1.1.3
    +      fs-extra: 7.0.1
    +      micromatch: 4.0.8
    +
    +  '@changesets/errors@0.2.0':
    +    dependencies:
    +      extendable-error: 0.1.7
    +
    +  '@changesets/get-dependents-graph@2.1.3':
    +    dependencies:
    +      '@changesets/types': 6.1.0
    +      '@manypkg/get-packages': 1.1.3
    +      picocolors: 1.1.1
    +      semver: 7.7.4
    +
    +  '@changesets/get-release-plan@4.0.15':
    +    dependencies:
    +      '@changesets/assemble-release-plan': 6.0.9
    +      '@changesets/config': 3.1.3
    +      '@changesets/pre': 2.0.2
    +      '@changesets/read': 0.6.7
    +      '@changesets/types': 6.1.0
    +      '@manypkg/get-packages': 1.1.3
    +
    +  '@changesets/get-version-range-type@0.4.0': {}
    +
    +  '@changesets/git@3.0.4':
    +    dependencies:
    +      '@changesets/errors': 0.2.0
    +      '@manypkg/get-packages': 1.1.3
    +      is-subdir: 1.2.0
    +      micromatch: 4.0.8
    +      spawndamnit: 3.0.1
    +
    +  '@changesets/logger@0.1.1':
    +    dependencies:
    +      picocolors: 1.1.1
    +
    +  '@changesets/parse@0.4.3':
    +    dependencies:
    +      '@changesets/types': 6.1.0
    +      js-yaml: 4.1.1
    +
    +  '@changesets/pre@2.0.2':
    +    dependencies:
    +      '@changesets/errors': 0.2.0
    +      '@changesets/types': 6.1.0
    +      '@manypkg/get-packages': 1.1.3
    +      fs-extra: 7.0.1
    +
    +  '@changesets/read@0.6.7':
    +    dependencies:
    +      '@changesets/git': 3.0.4
    +      '@changesets/logger': 0.1.1
    +      '@changesets/parse': 0.4.3
    +      '@changesets/types': 6.1.0
    +      fs-extra: 7.0.1
    +      p-filter: 2.1.0
    +      picocolors: 1.1.1
    +
    +  '@changesets/should-skip-package@0.1.2':
    +    dependencies:
    +      '@changesets/types': 6.1.0
    +      '@manypkg/get-packages': 1.1.3
    +
    +  '@changesets/types@4.1.0': {}
    +
    +  '@changesets/types@6.1.0': {}
    +
    +  '@changesets/write@0.4.0':
    +    dependencies:
    +      '@changesets/types': 6.1.0
    +      fs-extra: 7.0.1
    +      human-id: 4.1.3
    +      prettier: 2.8.8
    +
    +  '@cspell/cspell-bundled-dicts@9.7.0':
    +    dependencies:
    +      '@cspell/dict-ada': 4.1.1
    +      '@cspell/dict-al': 1.1.1
    +      '@cspell/dict-aws': 4.0.17
    +      '@cspell/dict-bash': 4.2.2
    +      '@cspell/dict-companies': 3.2.11
    +      '@cspell/dict-cpp': 7.0.2
    +      '@cspell/dict-cryptocurrencies': 5.0.5
    +      '@cspell/dict-csharp': 4.0.8
    +      '@cspell/dict-css': 4.1.1
    +      '@cspell/dict-dart': 2.3.2
    +      '@cspell/dict-data-science': 2.0.13
    +      '@cspell/dict-django': 4.1.6
    +      '@cspell/dict-docker': 1.1.17
    +      '@cspell/dict-dotnet': 5.0.13
    +      '@cspell/dict-elixir': 4.0.8
    +      '@cspell/dict-en-common-misspellings': 2.1.12
    +      '@cspell/dict-en-gb-mit': 3.1.22
    +      '@cspell/dict-en_us': 4.4.33
    +      '@cspell/dict-filetypes': 3.0.18
    +      '@cspell/dict-flutter': 1.1.1
    +      '@cspell/dict-fonts': 4.0.6
    +      '@cspell/dict-fsharp': 1.1.1
    +      '@cspell/dict-fullstack': 3.2.9
    +      '@cspell/dict-gaming-terms': 1.1.2
    +      '@cspell/dict-git': 3.1.0
    +      '@cspell/dict-golang': 6.0.26
    +      '@cspell/dict-google': 1.0.9
    +      '@cspell/dict-haskell': 4.0.6
    +      '@cspell/dict-html': 4.0.15
    +      '@cspell/dict-html-symbol-entities': 4.0.5
    +      '@cspell/dict-java': 5.0.12
    +      '@cspell/dict-julia': 1.1.1
    +      '@cspell/dict-k8s': 1.0.12
    +      '@cspell/dict-kotlin': 1.1.1
    +      '@cspell/dict-latex': 5.1.0
    +      '@cspell/dict-lorem-ipsum': 4.0.5
    +      '@cspell/dict-lua': 4.0.8
    +      '@cspell/dict-makefile': 1.0.5
    +      '@cspell/dict-markdown': 2.0.16(@cspell/dict-css@4.1.1)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.15)(@cspell/dict-typescript@3.2.3)
    +      '@cspell/dict-monkeyc': 1.0.12
    +      '@cspell/dict-node': 5.0.9
    +      '@cspell/dict-npm': 5.2.38
    +      '@cspell/dict-php': 4.1.1
    +      '@cspell/dict-powershell': 5.0.15
    +      '@cspell/dict-public-licenses': 2.0.16
    +      '@cspell/dict-python': 4.2.26
    +      '@cspell/dict-r': 2.1.1
    +      '@cspell/dict-ruby': 5.1.1
    +      '@cspell/dict-rust': 4.1.2
    +      '@cspell/dict-scala': 5.0.9
    +      '@cspell/dict-shell': 1.1.2
    +      '@cspell/dict-software-terms': 5.2.2
    +      '@cspell/dict-sql': 2.2.1
    +      '@cspell/dict-svelte': 1.0.7
    +      '@cspell/dict-swift': 2.0.6
    +      '@cspell/dict-terraform': 1.1.3
    +      '@cspell/dict-typescript': 3.2.3
    +      '@cspell/dict-vue': 3.0.5
    +      '@cspell/dict-zig': 1.0.0
    +
    +  '@cspell/cspell-json-reporter@9.7.0':
    +    dependencies:
    +      '@cspell/cspell-types': 9.7.0
    +
    +  '@cspell/cspell-performance-monitor@9.7.0': {}
    +
    +  '@cspell/cspell-pipe@9.7.0': {}
    +
    +  '@cspell/cspell-resolver@9.7.0':
    +    dependencies:
    +      global-directory: 5.0.0
    +
    +  '@cspell/cspell-service-bus@9.7.0': {}
    +
    +  '@cspell/cspell-types@9.7.0': {}
    +
    +  '@cspell/cspell-worker@9.7.0':
    +    dependencies:
    +      cspell-lib: 9.7.0
    +
    +  '@cspell/dict-ada@4.1.1': {}
    +
    +  '@cspell/dict-al@1.1.1': {}
    +
    +  '@cspell/dict-aws@4.0.17': {}
    +
    +  '@cspell/dict-bash@4.2.2':
    +    dependencies:
    +      '@cspell/dict-shell': 1.1.2
    +
    +  '@cspell/dict-companies@3.2.11': {}
    +
    +  '@cspell/dict-cpp@7.0.2': {}
    +
    +  '@cspell/dict-cryptocurrencies@5.0.5': {}
    +
    +  '@cspell/dict-csharp@4.0.8': {}
    +
    +  '@cspell/dict-css@4.1.1': {}
    +
    +  '@cspell/dict-dart@2.3.2': {}
    +
    +  '@cspell/dict-data-science@2.0.13': {}
    +
    +  '@cspell/dict-django@4.1.6': {}
    +
    +  '@cspell/dict-docker@1.1.17': {}
    +
    +  '@cspell/dict-dotnet@5.0.13': {}
    +
    +  '@cspell/dict-elixir@4.0.8': {}
    +
    +  '@cspell/dict-en-common-misspellings@2.1.12': {}
    +
    +  '@cspell/dict-en-gb-mit@3.1.22': {}
    +
    +  '@cspell/dict-en_us@4.4.33': {}
    +
    +  '@cspell/dict-filetypes@3.0.18': {}
    +
    +  '@cspell/dict-flutter@1.1.1': {}
    +
    +  '@cspell/dict-fonts@4.0.6': {}
    +
    +  '@cspell/dict-fsharp@1.1.1': {}
    +
    +  '@cspell/dict-fullstack@3.2.9': {}
    +
    +  '@cspell/dict-gaming-terms@1.1.2': {}
    +
    +  '@cspell/dict-git@3.1.0': {}
    +
    +  '@cspell/dict-golang@6.0.26': {}
    +
    +  '@cspell/dict-google@1.0.9': {}
    +
    +  '@cspell/dict-haskell@4.0.6': {}
    +
    +  '@cspell/dict-html-symbol-entities@4.0.5': {}
    +
    +  '@cspell/dict-html@4.0.15': {}
    +
    +  '@cspell/dict-java@5.0.12': {}
    +
    +  '@cspell/dict-julia@1.1.1': {}
    +
    +  '@cspell/dict-k8s@1.0.12': {}
    +
    +  '@cspell/dict-kotlin@1.1.1': {}
    +
    +  '@cspell/dict-latex@5.1.0': {}
    +
    +  '@cspell/dict-lorem-ipsum@4.0.5': {}
    +
    +  '@cspell/dict-lua@4.0.8': {}
    +
    +  '@cspell/dict-makefile@1.0.5': {}
    +
    +  '@cspell/dict-markdown@2.0.16(@cspell/dict-css@4.1.1)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.15)(@cspell/dict-typescript@3.2.3)':
    +    dependencies:
    +      '@cspell/dict-css': 4.1.1
    +      '@cspell/dict-html': 4.0.15
    +      '@cspell/dict-html-symbol-entities': 4.0.5
    +      '@cspell/dict-typescript': 3.2.3
    +
    +  '@cspell/dict-monkeyc@1.0.12': {}
    +
    +  '@cspell/dict-node@5.0.9': {}
    +
    +  '@cspell/dict-npm@5.2.38': {}
    +
    +  '@cspell/dict-php@4.1.1': {}
    +
    +  '@cspell/dict-powershell@5.0.15': {}
    +
    +  '@cspell/dict-public-licenses@2.0.16': {}
    +
    +  '@cspell/dict-python@4.2.26':
    +    dependencies:
    +      '@cspell/dict-data-science': 2.0.13
    +
    +  '@cspell/dict-r@2.1.1': {}
    +
    +  '@cspell/dict-ruby@5.1.1': {}
    +
    +  '@cspell/dict-rust@4.1.2': {}
    +
    +  '@cspell/dict-scala@5.0.9': {}
    +
    +  '@cspell/dict-shell@1.1.2': {}
    +
    +  '@cspell/dict-software-terms@5.2.2': {}
    +
    +  '@cspell/dict-sql@2.2.1': {}
    +
    +  '@cspell/dict-svelte@1.0.7': {}
    +
    +  '@cspell/dict-swift@2.0.6': {}
    +
    +  '@cspell/dict-terraform@1.1.3': {}
    +
    +  '@cspell/dict-typescript@3.2.3': {}
    +
    +  '@cspell/dict-vue@3.0.5': {}
    +
    +  '@cspell/dict-zig@1.0.0': {}
    +
    +  '@cspell/dynamic-import@9.7.0':
    +    dependencies:
    +      '@cspell/url': 9.7.0
    +      import-meta-resolve: 4.2.0
    +
    +  '@cspell/filetypes@9.7.0': {}
    +
    +  '@cspell/rpc@9.7.0': {}
    +
    +  '@cspell/strong-weak-map@9.7.0': {}
    +
    +  '@cspell/url@9.7.0': {}
    +
    +  '@csstools/color-helpers@6.0.1':
    +    optional: true
    +
    +  '@csstools/css-calc@3.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
    +    dependencies:
    +      '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
    +      '@csstools/css-tokenizer': 4.0.0
    +    optional: true
    +
    +  '@csstools/css-color-parser@4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
    +    dependencies:
    +      '@csstools/color-helpers': 6.0.1
    +      '@csstools/css-calc': 3.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
    +      '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
    +      '@csstools/css-tokenizer': 4.0.0
    +    optional: true
    +
    +  '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
    +    dependencies:
    +      '@csstools/css-tokenizer': 4.0.0
    +    optional: true
    +
    +  '@csstools/css-syntax-patches-for-csstree@1.0.27':
    +    optional: true
    +
    +  '@csstools/css-tokenizer@4.0.0':
    +    optional: true
    +
    +  '@emnapi/core@1.8.1':
    +    dependencies:
    +      '@emnapi/wasi-threads': 1.1.0
    +      tslib: 2.8.1
    +    optional: true
    +
    +  '@emnapi/core@1.9.2':
    +    dependencies:
    +      '@emnapi/wasi-threads': 1.2.1
    +      tslib: 2.8.1
    +    optional: true
    +
    +  '@emnapi/runtime@1.8.1':
    +    dependencies:
    +      tslib: 2.8.1
    +    optional: true
    +
    +  '@emnapi/runtime@1.9.2':
    +    dependencies:
    +      tslib: 2.8.1
    +    optional: true
    +
    +  '@emnapi/wasi-threads@1.1.0':
    +    dependencies:
    +      tslib: 2.8.1
    +    optional: true
    +
    +  '@emnapi/wasi-threads@1.2.1':
    +    dependencies:
    +      tslib: 2.8.1
    +    optional: true
    +
    +  '@emotion/is-prop-valid@0.8.8':
    +    dependencies:
    +      '@emotion/memoize': 0.7.4
    +
    +  '@emotion/memoize@0.7.4': {}
    +
    +  '@esbuild/aix-ppc64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/aix-ppc64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/android-arm64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/android-arm64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/android-arm@0.25.12':
    +    optional: true
    +
    +  '@esbuild/android-arm@0.27.3':
    +    optional: true
    +
    +  '@esbuild/android-x64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/android-x64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/darwin-arm64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/darwin-arm64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/darwin-x64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/darwin-x64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/freebsd-arm64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/freebsd-arm64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/freebsd-x64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/freebsd-x64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/linux-arm64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/linux-arm64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/linux-arm@0.25.12':
    +    optional: true
    +
    +  '@esbuild/linux-arm@0.27.3':
    +    optional: true
    +
    +  '@esbuild/linux-ia32@0.25.12':
    +    optional: true
    +
    +  '@esbuild/linux-ia32@0.27.3':
    +    optional: true
    +
    +  '@esbuild/linux-loong64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/linux-loong64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/linux-mips64el@0.25.12':
    +    optional: true
    +
    +  '@esbuild/linux-mips64el@0.27.3':
    +    optional: true
    +
    +  '@esbuild/linux-ppc64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/linux-ppc64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/linux-riscv64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/linux-riscv64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/linux-s390x@0.25.12':
    +    optional: true
    +
    +  '@esbuild/linux-s390x@0.27.3':
    +    optional: true
    +
    +  '@esbuild/linux-x64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/linux-x64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/netbsd-arm64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/netbsd-arm64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/netbsd-x64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/netbsd-x64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/openbsd-arm64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/openbsd-arm64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/openbsd-x64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/openbsd-x64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/openharmony-arm64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/openharmony-arm64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/sunos-x64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/sunos-x64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/win32-arm64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/win32-arm64@0.27.3':
    +    optional: true
    +
    +  '@esbuild/win32-ia32@0.25.12':
    +    optional: true
    +
    +  '@esbuild/win32-ia32@0.27.3':
    +    optional: true
    +
    +  '@esbuild/win32-x64@0.25.12':
    +    optional: true
    +
    +  '@esbuild/win32-x64@0.27.3':
    +    optional: true
    +
    +  '@exodus/bytes@1.14.0':
    +    optional: true
    +
    +  '@extism/extism@2.0.0-rc13': {}
    +
    +  '@extism/js-pdk@1.1.1':
    +    dependencies:
    +      urlpattern-polyfill: 8.0.2
    +
    +  '@inquirer/external-editor@1.0.3(@types/node@24.10.13)':
    +    dependencies:
    +      chardet: 2.1.1
    +      iconv-lite: 0.7.2
    +    optionalDependencies:
    +      '@types/node': 24.10.13
    +
    +  '@ionic/core@7.8.6':
    +    dependencies:
    +      '@stencil/core': 4.43.1
    +      ionicons: 7.4.0
    +      tslib: 2.8.1
    +
    +  '@ionic/prettier-config@4.0.0(prettier@3.8.1)':
    +    dependencies:
    +      prettier: 3.8.1
    +
    +  '@jridgewell/gen-mapping@0.3.13':
    +    dependencies:
    +      '@jridgewell/sourcemap-codec': 1.5.5
    +      '@jridgewell/trace-mapping': 0.3.31
    +
    +  '@jridgewell/remapping@2.3.5':
    +    dependencies:
    +      '@jridgewell/gen-mapping': 0.3.13
    +      '@jridgewell/trace-mapping': 0.3.31
    +
    +  '@jridgewell/resolve-uri@3.1.2': {}
    +
    +  '@jridgewell/source-map@0.3.11':
    +    dependencies:
    +      '@jridgewell/gen-mapping': 0.3.13
    +      '@jridgewell/trace-mapping': 0.3.31
    +
    +  '@jridgewell/sourcemap-codec@1.5.5': {}
    +
    +  '@jridgewell/trace-mapping@0.3.31':
    +    dependencies:
    +      '@jridgewell/resolve-uri': 3.1.2
    +      '@jridgewell/sourcemap-codec': 1.5.5
    +
    +  '@manypkg/find-root@1.1.0':
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      '@types/node': 12.20.55
    +      find-up: 4.1.0
    +      fs-extra: 8.1.0
    +
    +  '@manypkg/get-packages@1.1.3':
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      '@changesets/types': 4.1.0
    +      '@manypkg/find-root': 1.1.0
    +      fs-extra: 8.1.0
    +      globby: 11.1.0
    +      read-yaml-file: 1.1.0
    +
    +  '@napi-rs/wasm-runtime@1.1.1':
    +    dependencies:
    +      '@emnapi/core': 1.8.1
    +      '@emnapi/runtime': 1.8.1
    +      '@tybys/wasm-util': 0.10.1
    +    optional: true
    +
    +  '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
    +    dependencies:
    +      '@emnapi/core': 1.9.2
    +      '@emnapi/runtime': 1.9.2
    +      '@tybys/wasm-util': 0.10.1
    +    optional: true
    +
    +  '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
    +    dependencies:
    +      '@emnapi/core': 1.9.2
    +      '@emnapi/runtime': 1.9.2
    +      '@tybys/wasm-util': 0.10.1
    +    optional: true
    +
    +  '@nodelib/fs.scandir@2.1.5':
    +    dependencies:
    +      '@nodelib/fs.stat': 2.0.5
    +      run-parallel: 1.2.0
    +
    +  '@nodelib/fs.stat@2.0.5': {}
    +
    +  '@nodelib/fs.walk@1.2.8':
    +    dependencies:
    +      '@nodelib/fs.scandir': 2.1.5
    +      fastq: 1.20.1
    +
    +  '@oxc-parser/binding-android-arm-eabi@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-android-arm64@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-darwin-arm64@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-darwin-x64@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-freebsd-x64@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-arm-musleabihf@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-arm64-gnu@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-arm64-musl@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-ppc64-gnu@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-riscv64-gnu@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-riscv64-musl@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-s390x-gnu@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-x64-gnu@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-linux-x64-musl@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-openharmony-arm64@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-wasm32-wasi@0.121.0':
    +    dependencies:
    +      '@napi-rs/wasm-runtime': 1.1.1
    +    optional: true
    +
    +  '@oxc-parser/binding-win32-arm64-msvc@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-win32-ia32-msvc@0.121.0':
    +    optional: true
    +
    +  '@oxc-parser/binding-win32-x64-msvc@0.121.0':
    +    optional: true
    +
    +  '@oxc-project/types@0.121.0': {}
    +
    +  '@oxc-project/types@0.122.0': {}
    +
    +  '@oxc-project/types@0.124.0': {}
    +
    +  '@oxc-resolver/binding-android-arm-eabi@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-android-arm64@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-darwin-arm64@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-darwin-x64@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-freebsd-x64@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-arm64-gnu@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-arm64-musl@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-riscv64-musl@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-s390x-gnu@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-x64-gnu@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-linux-x64-musl@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-openharmony-arm64@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-wasm32-wasi@11.19.1':
    +    dependencies:
    +      '@napi-rs/wasm-runtime': 1.1.1
    +    optional: true
    +
    +  '@oxc-resolver/binding-win32-arm64-msvc@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-win32-ia32-msvc@11.19.1':
    +    optional: true
    +
    +  '@oxc-resolver/binding-win32-x64-msvc@11.19.1':
    +    optional: true
    +
    +  '@oxfmt/binding-android-arm-eabi@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-android-arm64@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-darwin-arm64@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-darwin-x64@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-freebsd-x64@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-arm-gnueabihf@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-arm-musleabihf@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-arm64-gnu@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-arm64-musl@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-ppc64-gnu@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-riscv64-gnu@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-riscv64-musl@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-s390x-gnu@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-x64-gnu@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-linux-x64-musl@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-openharmony-arm64@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-win32-arm64-msvc@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-win32-ia32-msvc@0.42.0':
    +    optional: true
    +
    +  '@oxfmt/binding-win32-x64-msvc@0.42.0':
    +    optional: true
    +
    +  '@oxlint/binding-android-arm-eabi@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-android-arm64@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-darwin-arm64@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-darwin-x64@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-freebsd-x64@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-arm-gnueabihf@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-arm-musleabihf@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-arm64-gnu@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-arm64-musl@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-ppc64-gnu@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-riscv64-gnu@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-riscv64-musl@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-s390x-gnu@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-x64-gnu@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-linux-x64-musl@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-openharmony-arm64@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-win32-arm64-msvc@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-win32-ia32-msvc@1.57.0':
    +    optional: true
    +
    +  '@oxlint/binding-win32-x64-msvc@1.57.0':
    +    optional: true
    +
    +  '@parcel/watcher-android-arm64@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-darwin-arm64@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-darwin-x64@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-freebsd-x64@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-linux-arm-glibc@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-linux-arm-musl@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-linux-arm64-glibc@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-linux-arm64-musl@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-linux-x64-glibc@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-linux-x64-musl@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-win32-arm64@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-win32-ia32@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher-win32-x64@2.5.6':
    +    optional: true
    +
    +  '@parcel/watcher@2.5.6':
    +    dependencies:
    +      detect-libc: 2.1.2
    +      is-glob: 4.0.3
    +      node-addon-api: 7.1.1
    +      picomatch: 4.0.4
    +    optionalDependencies:
    +      '@parcel/watcher-android-arm64': 2.5.6
    +      '@parcel/watcher-darwin-arm64': 2.5.6
    +      '@parcel/watcher-darwin-x64': 2.5.6
    +      '@parcel/watcher-freebsd-x64': 2.5.6
    +      '@parcel/watcher-linux-arm-glibc': 2.5.6
    +      '@parcel/watcher-linux-arm-musl': 2.5.6
    +      '@parcel/watcher-linux-arm64-glibc': 2.5.6
    +      '@parcel/watcher-linux-arm64-musl': 2.5.6
    +      '@parcel/watcher-linux-x64-glibc': 2.5.6
    +      '@parcel/watcher-linux-x64-musl': 2.5.6
    +      '@parcel/watcher-win32-arm64': 2.5.6
    +      '@parcel/watcher-win32-ia32': 2.5.6
    +      '@parcel/watcher-win32-x64': 2.5.6
    +
    +  '@playwright/test@1.58.2':
    +    dependencies:
    +      playwright: 1.58.2
    +
    +  '@polka/url@1.0.0-next.29': {}
    +
    +  '@popperjs/core@2.11.8': {}
    +
    +  '@quansync/fs@1.0.0':
    +    dependencies:
    +      quansync: 1.0.0
    +
    +  '@rolldown/binding-android-arm64@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-android-arm64@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-darwin-arm64@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-darwin-arm64@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-darwin-x64@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-darwin-x64@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-freebsd-x64@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-freebsd-x64@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
    +    dependencies:
    +      '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
    +    transitivePeerDependencies:
    +      - '@emnapi/core'
    +      - '@emnapi/runtime'
    +    optional: true
    +
    +  '@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
    +    dependencies:
    +      '@emnapi/core': 1.9.2
    +      '@emnapi/runtime': 1.9.2
    +      '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
    +    optional: true
    +
    +  '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
    +    optional: true
    +
    +  '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
    +    optional: true
    +
    +  '@rolldown/pluginutils@1.0.0-rc.12': {}
    +
    +  '@rolldown/pluginutils@1.0.0-rc.15': {}
    +
    +  '@rollup/pluginutils@3.1.0(rollup@4.57.1)':
    +    dependencies:
    +      '@types/estree': 0.0.39
    +      estree-walker: 1.0.1
    +      picomatch: 2.3.1
    +      rollup: 4.57.1
    +
    +  '@rollup/pluginutils@5.3.0(rollup@4.57.1)':
    +    dependencies:
    +      '@types/estree': 1.0.8
    +      estree-walker: 2.0.2
    +      picomatch: 4.0.4
    +    optionalDependencies:
    +      rollup: 4.57.1
    +
    +  '@rollup/rollup-android-arm-eabi@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-android-arm64@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-darwin-arm64@4.44.0':
    +    optional: true
    +
    +  '@rollup/rollup-darwin-arm64@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-darwin-x64@4.44.0':
    +    optional: true
    +
    +  '@rollup/rollup-darwin-x64@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-freebsd-arm64@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-freebsd-x64@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-arm-gnueabihf@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-arm-musleabihf@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-arm64-gnu@4.44.0':
    +    optional: true
    +
    +  '@rollup/rollup-linux-arm64-gnu@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-arm64-musl@4.44.0':
    +    optional: true
    +
    +  '@rollup/rollup-linux-arm64-musl@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-loong64-gnu@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-loong64-musl@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-ppc64-gnu@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-ppc64-musl@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-riscv64-gnu@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-riscv64-musl@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-s390x-gnu@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-x64-gnu@4.44.0':
    +    optional: true
    +
    +  '@rollup/rollup-linux-x64-gnu@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-linux-x64-musl@4.44.0':
    +    optional: true
    +
    +  '@rollup/rollup-linux-x64-musl@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-openbsd-x64@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-openharmony-arm64@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-win32-arm64-msvc@4.44.0':
    +    optional: true
    +
    +  '@rollup/rollup-win32-arm64-msvc@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-win32-ia32-msvc@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-win32-x64-gnu@4.57.1':
    +    optional: true
    +
    +  '@rollup/rollup-win32-x64-msvc@4.44.0':
    +    optional: true
    +
    +  '@rollup/rollup-win32-x64-msvc@4.57.1':
    +    optional: true
    +
    +  '@standard-schema/spec@1.1.0': {}
    +
    +  '@stencil/core@4.43.1':
    +    optionalDependencies:
    +      '@rollup/rollup-darwin-arm64': 4.44.0
    +      '@rollup/rollup-darwin-x64': 4.44.0
    +      '@rollup/rollup-linux-arm64-gnu': 4.44.0
    +      '@rollup/rollup-linux-arm64-musl': 4.44.0
    +      '@rollup/rollup-linux-x64-gnu': 4.44.0
    +      '@rollup/rollup-linux-x64-musl': 4.44.0
    +      '@rollup/rollup-win32-arm64-msvc': 4.44.0
    +      '@rollup/rollup-win32-x64-msvc': 4.44.0
    +
    +  '@stencil/playwright@0.4.3(@playwright/test@1.58.2)(@stencil/core@packages+core)':
    +    dependencies:
    +      '@playwright/test': 1.58.2
    +      '@stencil/core': link:packages/core
    +      deepmerge: 4.3.1
    +      find-up: 8.0.0
    +
    +  '@stencil/react-output-target@0.0.9(@stencil/core@packages+core)':
    +    dependencies:
    +      '@stencil/core': link:packages/core
    +
    +  '@stencil/sass@3.2.3(@stencil/core@packages+core)':
    +    dependencies:
    +      '@stencil/core': link:packages/core
    +      sass-embedded: 1.97.3
    +
    +  '@stencil/vitest@1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)':
    +    dependencies:
    +      '@stencil/core': link:packages/core
    +      jiti: 2.6.1
    +      local-pkg: 1.1.2
    +      vitest: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      vitest-environment-stencil: 1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +    optionalDependencies:
    +      '@playwright/test': 1.58.2
    +      '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      jsdom: 28.0.0
    +      playwright: 1.58.2
    +
    +  '@stencil/vitest@1.11.6(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)':
    +    dependencies:
    +      '@stencil/core': link:packages/core
    +      jiti: 2.6.1
    +      local-pkg: 1.1.2
    +      vitest: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      vitest-environment-stencil: 1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +    optionalDependencies:
    +      '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      jsdom: 28.0.0
    +      playwright: 1.58.2
    +
    +  '@stencil/vitest@1.11.6(@stencil/mock-doc@packages+mock-doc)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)':
    +    dependencies:
    +      jiti: 2.6.1
    +      local-pkg: 1.1.2
    +      vitest: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      vitest-environment-stencil: 1.11.6(@stencil/mock-doc@packages+mock-doc)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +    optionalDependencies:
    +      '@stencil/mock-doc': link:packages/mock-doc
    +      '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      jsdom: 28.0.0
    +      playwright: 1.58.2
    +
    +  '@stencil/vitest@1.11.6(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)':
    +    dependencies:
    +      jiti: 2.6.1
    +      local-pkg: 1.1.2
    +      vitest: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      vitest-environment-stencil: 1.11.6(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +    optionalDependencies:
    +      '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      jsdom: 28.0.0
    +      playwright: 1.58.2
    +
    +  '@tsdown/css@0.21.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(jiti@2.6.1)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(tsdown@0.21.7)(tsx@4.21.0)(yaml@2.8.3)':
    +    dependencies:
    +      lightningcss: 1.32.0
    +      postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3)
    +      rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
    +      tsdown: 0.21.7(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@tsdown/css@0.21.6)(oxc-resolver@11.19.1)(typescript@6.0.2)
    +    optionalDependencies:
    +      postcss: 8.5.6
    +      sass: 1.97.3
    +      sass-embedded: 1.97.3
    +    transitivePeerDependencies:
    +      - '@emnapi/core'
    +      - '@emnapi/runtime'
    +      - jiti
    +      - tsx
    +      - yaml
    +
    +  '@tybys/wasm-util@0.10.1':
    +    dependencies:
    +      tslib: 2.8.1
    +    optional: true
    +
    +  '@types/chai@5.2.3':
    +    dependencies:
    +      '@types/deep-eql': 4.0.2
    +      assertion-error: 2.0.1
    +
    +  '@types/deep-eql@4.0.2': {}
    +
    +  '@types/estree@0.0.39': {}
    +
    +  '@types/estree@1.0.8': {}
    +
    +  '@types/file-saver@2.0.7': {}
    +
    +  '@types/jsesc@2.5.1': {}
    +
    +  '@types/lodash-es@4.17.12':
    +    dependencies:
    +      '@types/lodash': 4.17.23
    +
    +  '@types/lodash@4.17.23': {}
    +
    +  '@types/node@12.20.55': {}
    +
    +  '@types/node@24.10.13':
    +    dependencies:
    +      undici-types: 7.16.0
    +
    +  '@types/prompts@2.4.9':
    +    dependencies:
    +      '@types/node': 24.10.13
    +      kleur: 3.0.3
    +
    +  '@types/video.js@7.3.58': {}
    +
    +  '@types/ws@8.18.1':
    +    dependencies:
    +      '@types/node': 24.10.13
    +
    +  '@videojs/http-streaming@2.16.3(video.js@7.21.7)':
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      '@videojs/vhs-utils': 3.0.5
    +      aes-decrypter: 3.1.3
    +      global: 4.4.0
    +      m3u8-parser: 4.8.0
    +      mpd-parser: 0.22.1
    +      mux.js: 6.0.1
    +      video.js: 7.21.7
    +
    +  '@videojs/vhs-utils@3.0.5':
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      global: 4.4.0
    +      url-toolkit: 2.2.5
    +
    +  '@videojs/xhr@2.6.0':
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      global: 4.4.0
    +      is-function: 1.0.2
    +
    +  '@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)':
    +    dependencies:
    +      '@vitest/browser': 4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      '@vitest/mocker': 4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      playwright: 1.58.2
    +      tinyrainbow: 3.1.0
    +      vitest: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +    transitivePeerDependencies:
    +      - bufferutil
    +      - msw
    +      - utf-8-validate
    +      - vite
    +
    +  '@vitest/browser@4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)':
    +    dependencies:
    +      '@blazediff/core': 1.9.1
    +      '@vitest/mocker': 4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      '@vitest/utils': 4.1.2
    +      magic-string: 0.30.21
    +      pngjs: 7.0.0
    +      sirv: 3.0.2
    +      tinyrainbow: 3.1.0
    +      vitest: 4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      ws: 8.19.0
    +    transitivePeerDependencies:
    +      - bufferutil
    +      - msw
    +      - utf-8-validate
    +      - vite
    +
    +  '@vitest/expect@4.1.2':
    +    dependencies:
    +      '@standard-schema/spec': 1.1.0
    +      '@types/chai': 5.2.3
    +      '@vitest/spy': 4.1.2
    +      '@vitest/utils': 4.1.2
    +      chai: 6.2.2
    +      tinyrainbow: 3.1.0
    +
    +  '@vitest/mocker@4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))':
    +    dependencies:
    +      '@vitest/spy': 4.1.2
    +      estree-walker: 3.0.3
    +      magic-string: 0.30.21
    +    optionalDependencies:
    +      vite: 6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3)
    +
    +  '@vitest/pretty-format@4.1.2':
    +    dependencies:
    +      tinyrainbow: 3.1.0
    +
    +  '@vitest/runner@4.1.2':
    +    dependencies:
    +      '@vitest/utils': 4.1.2
    +      pathe: 2.0.3
    +
    +  '@vitest/snapshot@4.1.2':
    +    dependencies:
    +      '@vitest/pretty-format': 4.1.2
    +      '@vitest/utils': 4.1.2
    +      magic-string: 0.30.21
    +      pathe: 2.0.3
    +
    +  '@vitest/spy@4.1.2': {}
    +
    +  '@vitest/utils@4.1.2':
    +    dependencies:
    +      '@vitest/pretty-format': 4.1.2
    +      convert-source-map: 2.0.0
    +      tinyrainbow: 3.1.0
    +
    +  '@xmldom/xmldom@0.8.11': {}
    +
    +  '@zeit/schemas@2.36.0': {}
    +
    +  abstract-leveldown@0.12.4:
    +    dependencies:
    +      xtend: 3.0.0
    +
    +  acorn@8.15.0: {}
    +
    +  aes-decrypter@3.1.3:
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      '@videojs/vhs-utils': 3.0.5
    +      global: 4.4.0
    +      pkcs7: 1.0.4
    +
    +  agent-base@7.1.4:
    +    optional: true
    +
    +  ajv@8.12.0:
    +    dependencies:
    +      fast-deep-equal: 3.1.3
    +      json-schema-traverse: 1.0.0
    +      require-from-string: 2.0.2
    +      uri-js: 4.4.1
    +
    +  ansi-align@3.0.1:
    +    dependencies:
    +      string-width: 4.2.3
    +
    +  ansi-colors@4.1.3: {}
    +
    +  ansi-regex@4.1.1: {}
    +
    +  ansi-regex@5.0.1: {}
    +
    +  ansi-regex@6.2.2: {}
    +
    +  ansi-styles@3.2.1:
    +    dependencies:
    +      color-convert: 1.9.3
    +
    +  ansi-styles@4.3.0:
    +    dependencies:
    +      color-convert: 2.0.1
    +
    +  ansi-styles@6.2.3: {}
    +
    +  ansis@4.2.0: {}
    +
    +  arch@2.2.0: {}
    +
    +  arg@5.0.2: {}
    +
    +  argparse@1.0.10:
    +    dependencies:
    +      sprintf-js: 1.0.3
    +
    +  argparse@2.0.1: {}
    +
    +  arr-diff@4.0.0: {}
    +
    +  arr-flatten@1.1.0: {}
    +
    +  arr-union@3.1.0: {}
    +
    +  array-timsort@1.0.3: {}
    +
    +  array-union@2.1.0: {}
    +
    +  array-unique@0.3.2: {}
    +
    +  asn1.js@4.10.1:
    +    dependencies:
    +      bn.js: 4.12.3
    +      inherits: 2.0.4
    +      minimalistic-assert: 1.0.1
    +
    +  assertion-error@2.0.1: {}
    +
    +  assign-symbols@1.0.0: {}
    +
    +  ast-kit@3.0.0-beta.1:
    +    dependencies:
    +      '@babel/parser': 8.0.0-rc.3
    +      estree-walker: 3.0.3
    +      pathe: 2.0.3
    +
    +  at-least-node@1.0.0: {}
    +
    +  atob@2.1.2: {}
    +
    +  available-typed-arrays@1.0.7:
    +    dependencies:
    +      possible-typed-array-names: 1.1.0
    +
    +  babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0):
    +    dependencies:
    +      '@babel/compat-data': 7.29.0
    +      '@babel/core': 7.29.0
    +      '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
    +      semver: 6.3.1
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0):
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
    +      core-js-compat: 3.48.0
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  babel-plugin-polyfill-regenerator@0.6.6(@babel/core@7.29.0):
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  babel-plugin-transform-react-remove-prop-types@0.4.24: {}
    +
    +  balanced-match@1.0.2: {}
    +
    +  balanced-match@4.0.3: {}
    +
    +  base@0.11.2:
    +    dependencies:
    +      cache-base: 1.0.1
    +      class-utils: 0.3.6
    +      component-emitter: 1.3.1
    +      define-property: 1.0.0
    +      isobject: 3.0.1
    +      mixin-deep: 1.3.2
    +      pascalcase: 0.1.1
    +
    +  baseline-browser-mapping@2.9.19: {}
    +
    +  better-path-resolve@1.0.0:
    +    dependencies:
    +      is-windows: 1.0.2
    +
    +  bidi-js@1.0.3:
    +    dependencies:
    +      require-from-string: 2.0.2
    +    optional: true
    +
    +  big.js@5.2.2: {}
    +
    +  birpc@4.0.0: {}
    +
    +  bl@0.8.2:
    +    dependencies:
    +      readable-stream: 1.0.34
    +
    +  bn.js@4.12.3: {}
    +
    +  bn.js@5.2.3: {}
    +
    +  bootstrap@5.3.8(@popperjs/core@2.11.8):
    +    dependencies:
    +      '@popperjs/core': 2.11.8
    +
    +  boxen@7.0.0:
    +    dependencies:
    +      ansi-align: 3.0.1
    +      camelcase: 7.0.1
    +      chalk: 5.6.2
    +      cli-boxes: 3.0.0
    +      string-width: 5.1.2
    +      type-fest: 2.19.0
    +      widest-line: 4.0.1
    +      wrap-ansi: 8.1.0
    +
    +  brace-expansion@1.1.12:
    +    dependencies:
    +      balanced-match: 1.0.2
    +      concat-map: 0.0.1
    +
    +  brace-expansion@5.0.2:
    +    dependencies:
    +      balanced-match: 4.0.3
    +
    +  braces@2.3.2:
    +    dependencies:
    +      arr-flatten: 1.1.0
    +      array-unique: 0.3.2
    +      extend-shallow: 2.0.1
    +      fill-range: 4.0.0
    +      isobject: 3.0.1
    +      repeat-element: 1.1.4
    +      snapdragon: 0.8.2
    +      snapdragon-node: 2.1.1
    +      split-string: 3.1.0
    +      to-regex: 3.0.2
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  braces@3.0.3:
    +    dependencies:
    +      fill-range: 7.1.1
    +
    +  brorand@1.1.0: {}
    +
    +  browserify-aes@1.2.0:
    +    dependencies:
    +      buffer-xor: 1.0.3
    +      cipher-base: 1.0.7
    +      create-hash: 1.2.0
    +      evp_bytestokey: 1.0.3
    +      inherits: 2.0.4
    +      safe-buffer: 5.2.1
    +
    +  browserify-cipher@1.0.1:
    +    dependencies:
    +      browserify-aes: 1.2.0
    +      browserify-des: 1.0.2
    +      evp_bytestokey: 1.0.3
    +
    +  browserify-des@1.0.2:
    +    dependencies:
    +      cipher-base: 1.0.7
    +      des.js: 1.1.0
    +      inherits: 2.0.4
    +      safe-buffer: 5.2.1
    +
    +  browserify-fs@1.0.0:
    +    dependencies:
    +      level-filesystem: 1.2.0
    +      level-js: 2.2.4
    +      levelup: 0.18.6
    +
    +  browserify-rsa@4.1.1:
    +    dependencies:
    +      bn.js: 5.2.3
    +      randombytes: 2.1.0
    +      safe-buffer: 5.2.1
    +
    +  browserify-sign@4.2.5:
    +    dependencies:
    +      bn.js: 5.2.3
    +      browserify-rsa: 4.1.1
    +      create-hash: 1.2.0
    +      create-hmac: 1.1.7
    +      elliptic: 6.6.1
    +      inherits: 2.0.4
    +      parse-asn1: 5.1.9
    +      readable-stream: 2.3.8
    +      safe-buffer: 5.2.1
    +
    +  browserslist@4.28.1:
    +    dependencies:
    +      baseline-browser-mapping: 2.9.19
    +      caniuse-lite: 1.0.30001769
    +      electron-to-chromium: 1.5.286
    +      node-releases: 2.0.27
    +      update-browserslist-db: 1.2.3(browserslist@4.28.1)
    +
    +  buffer-es6@4.9.3: {}
    +
    +  buffer-from@1.1.2: {}
    +
    +  buffer-xor@1.0.3: {}
    +
    +  bundle-name@4.1.0:
    +    dependencies:
    +      run-applescript: 7.1.0
    +
    +  bytes@3.0.0: {}
    +
    +  bytes@3.1.2: {}
    +
    +  cac@7.0.0: {}
    +
    +  cache-base@1.0.1:
    +    dependencies:
    +      collection-visit: 1.0.0
    +      component-emitter: 1.3.1
    +      get-value: 2.0.6
    +      has-value: 1.0.0
    +      isobject: 3.0.1
    +      set-value: 2.0.1
    +      to-object-path: 0.3.0
    +      union-value: 1.0.1
    +      unset-value: 1.0.0
    +
    +  call-bind-apply-helpers@1.0.2:
    +    dependencies:
    +      es-errors: 1.3.0
    +      function-bind: 1.1.2
    +
    +  call-bind@1.0.8:
    +    dependencies:
    +      call-bind-apply-helpers: 1.0.2
    +      es-define-property: 1.0.1
    +      get-intrinsic: 1.3.0
    +      set-function-length: 1.2.2
    +
    +  call-bound@1.0.4:
    +    dependencies:
    +      call-bind-apply-helpers: 1.0.2
    +      get-intrinsic: 1.3.0
    +
    +  caller-callsite@2.0.0:
    +    dependencies:
    +      callsites: 2.0.0
    +
    +  caller-path@2.0.0:
    +    dependencies:
    +      caller-callsite: 2.0.0
    +
    +  callsites@2.0.0: {}
    +
    +  callsites@3.1.0: {}
    +
    +  camelcase@5.3.1: {}
    +
    +  camelcase@7.0.1: {}
    +
    +  caniuse-lite@1.0.30001769: {}
    +
    +  chai@6.2.2: {}
    +
    +  chalk-template@0.4.0:
    +    dependencies:
    +      chalk: 4.1.2
    +
    +  chalk-template@1.1.2:
    +    dependencies:
    +      chalk: 5.6.2
    +
    +  chalk@4.1.2:
    +    dependencies:
    +      ansi-styles: 4.3.0
    +      supports-color: 7.2.0
    +
    +  chalk@5.0.1: {}
    +
    +  chalk@5.6.2: {}
    +
    +  chardet@2.1.1: {}
    +
    +  chokidar@4.0.3:
    +    dependencies:
    +      readdirp: 4.1.2
    +    optional: true
    +
    +  cipher-base@1.0.7:
    +    dependencies:
    +      inherits: 2.0.4
    +      safe-buffer: 5.2.1
    +      to-buffer: 1.2.2
    +
    +  class-utils@0.3.6:
    +    dependencies:
    +      arr-union: 3.1.0
    +      define-property: 0.2.5
    +      isobject: 3.0.1
    +      static-extend: 0.1.2
    +
    +  clear-module@4.1.2:
    +    dependencies:
    +      parent-module: 2.0.0
    +      resolve-from: 5.0.0
    +
    +  cli-boxes@3.0.0: {}
    +
    +  clipboardy@3.0.0:
    +    dependencies:
    +      arch: 2.2.0
    +      execa: 5.1.1
    +      is-wsl: 2.2.0
    +
    +  cliui@5.0.0:
    +    dependencies:
    +      string-width: 3.1.0
    +      strip-ansi: 5.2.0
    +      wrap-ansi: 5.1.0
    +
    +  clone@0.1.19: {}
    +
    +  collection-visit@1.0.0:
    +    dependencies:
    +      map-visit: 1.0.0
    +      object-visit: 1.0.1
    +
    +  color-convert@1.9.3:
    +    dependencies:
    +      color-name: 1.1.3
    +
    +  color-convert@2.0.1:
    +    dependencies:
    +      color-name: 1.1.4
    +
    +  color-name@1.1.3: {}
    +
    +  color-name@1.1.4: {}
    +
    +  colorjs.io@0.5.2: {}
    +
    +  commander@14.0.3: {}
    +
    +  commander@2.20.3: {}
    +
    +  comment-json@4.6.2:
    +    dependencies:
    +      array-timsort: 1.0.3
    +      esprima: 4.0.1
    +
    +  component-emitter@1.3.1: {}
    +
    +  compressible@2.0.18:
    +    dependencies:
    +      mime-db: 1.52.0
    +
    +  compression@1.8.1:
    +    dependencies:
    +      bytes: 3.1.2
    +      compressible: 2.0.18
    +      debug: 2.6.9
    +      negotiator: 0.6.4
    +      on-headers: 1.1.0
    +      safe-buffer: 5.2.1
    +      vary: 1.1.2
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  concat-map@0.0.1: {}
    +
    +  concat-stream@1.6.2:
    +    dependencies:
    +      buffer-from: 1.1.2
    +      inherits: 2.0.4
    +      readable-stream: 2.3.8
    +      typedarray: 0.0.6
    +
    +  confbox@0.1.8: {}
    +
    +  confbox@0.2.4: {}
    +
    +  content-disposition@0.5.2: {}
    +
    +  convert-source-map@2.0.0: {}
    +
    +  copy-descriptor@0.1.1: {}
    +
    +  core-js-compat@3.48.0:
    +    dependencies:
    +      browserslist: 4.28.1
    +
    +  core-js@3.48.0: {}
    +
    +  core-util-is@1.0.3: {}
    +
    +  cosmiconfig@5.2.1:
    +    dependencies:
    +      import-fresh: 2.0.0
    +      is-directory: 0.3.1
    +      js-yaml: 3.14.2
    +      parse-json: 4.0.0
    +
    +  create-ecdh@4.0.4:
    +    dependencies:
    +      bn.js: 4.12.3
    +      elliptic: 6.6.1
    +
    +  create-hash@1.2.0:
    +    dependencies:
    +      cipher-base: 1.0.7
    +      inherits: 2.0.4
    +      md5.js: 1.3.5
    +      ripemd160: 2.0.3
    +      sha.js: 2.4.12
    +
    +  create-hmac@1.1.7:
    +    dependencies:
    +      cipher-base: 1.0.7
    +      create-hash: 1.2.0
    +      inherits: 2.0.4
    +      ripemd160: 2.0.3
    +      safe-buffer: 5.2.1
    +      sha.js: 2.4.12
    +
    +  cross-spawn@7.0.6:
    +    dependencies:
    +      path-key: 3.1.1
    +      shebang-command: 2.0.0
    +      which: 2.0.2
    +
    +  crypto-browserify@3.12.1:
    +    dependencies:
    +      browserify-cipher: 1.0.1
    +      browserify-sign: 4.2.5
    +      create-ecdh: 4.0.4
    +      create-hash: 1.2.0
    +      create-hmac: 1.1.7
    +      diffie-hellman: 5.0.3
    +      hash-base: 3.0.5
    +      inherits: 2.0.4
    +      pbkdf2: 3.1.5
    +      public-encrypt: 4.0.3
    +      randombytes: 2.1.0
    +      randomfill: 1.0.4
    +
    +  cspell-config-lib@9.7.0:
    +    dependencies:
    +      '@cspell/cspell-types': 9.7.0
    +      comment-json: 4.6.2
    +      smol-toml: 1.6.1
    +      yaml: 2.8.3
    +
    +  cspell-dictionary@9.7.0:
    +    dependencies:
    +      '@cspell/cspell-performance-monitor': 9.7.0
    +      '@cspell/cspell-pipe': 9.7.0
    +      '@cspell/cspell-types': 9.7.0
    +      cspell-trie-lib: 9.7.0(@cspell/cspell-types@9.7.0)
    +      fast-equals: 6.0.0
    +
    +  cspell-gitignore@9.7.0:
    +    dependencies:
    +      '@cspell/url': 9.7.0
    +      cspell-glob: 9.7.0
    +      cspell-io: 9.7.0
    +
    +  cspell-glob@9.7.0:
    +    dependencies:
    +      '@cspell/url': 9.7.0
    +      picomatch: 4.0.4
    +
    +  cspell-grammar@9.7.0:
    +    dependencies:
    +      '@cspell/cspell-pipe': 9.7.0
    +      '@cspell/cspell-types': 9.7.0
    +
    +  cspell-io@9.7.0:
    +    dependencies:
    +      '@cspell/cspell-service-bus': 9.7.0
    +      '@cspell/url': 9.7.0
    +
    +  cspell-lib@9.7.0:
    +    dependencies:
    +      '@cspell/cspell-bundled-dicts': 9.7.0
    +      '@cspell/cspell-performance-monitor': 9.7.0
    +      '@cspell/cspell-pipe': 9.7.0
    +      '@cspell/cspell-resolver': 9.7.0
    +      '@cspell/cspell-types': 9.7.0
    +      '@cspell/dynamic-import': 9.7.0
    +      '@cspell/filetypes': 9.7.0
    +      '@cspell/rpc': 9.7.0
    +      '@cspell/strong-weak-map': 9.7.0
    +      '@cspell/url': 9.7.0
    +      clear-module: 4.1.2
    +      cspell-config-lib: 9.7.0
    +      cspell-dictionary: 9.7.0
    +      cspell-glob: 9.7.0
    +      cspell-grammar: 9.7.0
    +      cspell-io: 9.7.0
    +      cspell-trie-lib: 9.7.0(@cspell/cspell-types@9.7.0)
    +      env-paths: 4.0.0
    +      gensequence: 8.0.8
    +      import-fresh: 3.3.1
    +      resolve-from: 5.0.0
    +      vscode-languageserver-textdocument: 1.0.12
    +      vscode-uri: 3.1.0
    +      xdg-basedir: 5.1.0
    +
    +  cspell-trie-lib@9.7.0(@cspell/cspell-types@9.7.0):
    +    dependencies:
    +      '@cspell/cspell-types': 9.7.0
    +
    +  cspell@9.7.0:
    +    dependencies:
    +      '@cspell/cspell-json-reporter': 9.7.0
    +      '@cspell/cspell-performance-monitor': 9.7.0
    +      '@cspell/cspell-pipe': 9.7.0
    +      '@cspell/cspell-types': 9.7.0
    +      '@cspell/cspell-worker': 9.7.0
    +      '@cspell/dynamic-import': 9.7.0
    +      '@cspell/url': 9.7.0
    +      ansi-regex: 6.2.2
    +      chalk: 5.6.2
    +      chalk-template: 1.1.2
    +      commander: 14.0.3
    +      cspell-config-lib: 9.7.0
    +      cspell-dictionary: 9.7.0
    +      cspell-gitignore: 9.7.0
    +      cspell-glob: 9.7.0
    +      cspell-io: 9.7.0
    +      cspell-lib: 9.7.0
    +      fast-json-stable-stringify: 2.1.0
    +      flatted: 3.4.2
    +      semver: 7.7.4
    +      tinyglobby: 0.2.15
    +
    +  css-tree@3.2.1:
    +    dependencies:
    +      mdn-data: 2.27.1
    +      source-map-js: 1.2.1
    +    optional: true
    +
    +  css-what@7.0.0: {}
    +
    +  cssesc@3.0.0: {}
    +
    +  cssstyle@5.3.7:
    +    dependencies:
    +      '@asamuzakjp/css-color': 4.1.2
    +      '@csstools/css-syntax-patches-for-csstree': 1.0.27
    +      css-tree: 3.2.1
    +      lru-cache: 11.2.7
    +    optional: true
    +
    +  data-urls@7.0.0:
    +    dependencies:
    +      whatwg-mimetype: 5.0.0
    +      whatwg-url: 16.0.0
    +    transitivePeerDependencies:
    +      - '@noble/hashes'
    +    optional: true
    +
    +  debug@2.6.9:
    +    dependencies:
    +      ms: 2.0.0
    +
    +  debug@4.4.3:
    +    dependencies:
    +      ms: 2.1.3
    +
    +  decamelize@1.2.0: {}
    +
    +  decimal.js@10.6.0:
    +    optional: true
    +
    +  decode-uri-component@0.2.2: {}
    +
    +  deep-extend@0.6.0: {}
    +
    +  deepmerge@4.3.1: {}
    +
    +  default-browser-id@5.0.1: {}
    +
    +  default-browser@5.5.0:
    +    dependencies:
    +      bundle-name: 4.1.0
    +      default-browser-id: 5.0.1
    +
    +  deferred-leveldown@0.2.0:
    +    dependencies:
    +      abstract-leveldown: 0.12.4
    +
    +  define-data-property@1.1.4:
    +    dependencies:
    +      es-define-property: 1.0.1
    +      es-errors: 1.3.0
    +      gopd: 1.2.0
    +
    +  define-lazy-prop@3.0.0: {}
    +
    +  define-property@0.2.5:
    +    dependencies:
    +      is-descriptor: 0.1.7
    +
    +  define-property@1.0.0:
    +    dependencies:
    +      is-descriptor: 1.0.3
    +
    +  define-property@2.0.2:
    +    dependencies:
    +      is-descriptor: 1.0.3
    +      isobject: 3.0.1
    +
    +  defu@6.1.4: {}
    +
    +  des.js@1.1.0:
    +    dependencies:
    +      inherits: 2.0.4
    +      minimalistic-assert: 1.0.1
    +
    +  detect-indent@6.1.0: {}
    +
    +  detect-libc@2.1.2: {}
    +
    +  diffie-hellman@5.0.3:
    +    dependencies:
    +      bn.js: 4.12.3
    +      miller-rabin: 4.0.1
    +      randombytes: 2.1.0
    +
    +  dir-glob@3.0.1:
    +    dependencies:
    +      path-type: 4.0.0
    +
    +  dom-walk@0.1.2: {}
    +
    +  dts-resolver@2.1.3(oxc-resolver@11.19.1):
    +    optionalDependencies:
    +      oxc-resolver: 11.19.1
    +
    +  dunder-proto@1.0.1:
    +    dependencies:
    +      call-bind-apply-helpers: 1.0.2
    +      es-errors: 1.3.0
    +      gopd: 1.2.0
    +
    +  eastasianwidth@0.2.0: {}
    +
    +  electron-to-chromium@1.5.286: {}
    +
    +  elliptic@6.6.1:
    +    dependencies:
    +      bn.js: 4.12.3
    +      brorand: 1.1.0
    +      hash.js: 1.1.7
    +      hmac-drbg: 1.0.1
    +      inherits: 2.0.4
    +      minimalistic-assert: 1.0.1
    +      minimalistic-crypto-utils: 1.0.1
    +
    +  emoji-regex@7.0.3: {}
    +
    +  emoji-regex@8.0.0: {}
    +
    +  emoji-regex@9.2.2: {}
    +
    +  emojis-list@3.0.0: {}
    +
    +  empathic@2.0.0: {}
    +
    +  enhanced-resolve@4.5.0:
    +    dependencies:
    +      graceful-fs: 4.2.11
    +      memory-fs: 0.5.0
    +      tapable: 1.1.3
    +
    +  enquirer@2.4.1:
    +    dependencies:
    +      ansi-colors: 4.1.3
    +      strip-ansi: 6.0.1
    +
    +  entities@4.5.0: {}
    +
    +  entities@6.0.1:
    +    optional: true
    +
    +  env-paths@4.0.0:
    +    dependencies:
    +      is-safe-filename: 0.1.1
    +
    +  errno@0.1.8:
    +    dependencies:
    +      prr: 1.0.1
    +
    +  error-ex@1.3.4:
    +    dependencies:
    +      is-arrayish: 0.2.1
    +
    +  es-define-property@1.0.1: {}
    +
    +  es-errors@1.3.0: {}
    +
    +  es-module-lexer@2.0.0: {}
    +
    +  es-object-atoms@1.1.1:
    +    dependencies:
    +      es-errors: 1.3.0
    +
    +  esbuild@0.25.12:
    +    optionalDependencies:
    +      '@esbuild/aix-ppc64': 0.25.12
    +      '@esbuild/android-arm': 0.25.12
    +      '@esbuild/android-arm64': 0.25.12
    +      '@esbuild/android-x64': 0.25.12
    +      '@esbuild/darwin-arm64': 0.25.12
    +      '@esbuild/darwin-x64': 0.25.12
    +      '@esbuild/freebsd-arm64': 0.25.12
    +      '@esbuild/freebsd-x64': 0.25.12
    +      '@esbuild/linux-arm': 0.25.12
    +      '@esbuild/linux-arm64': 0.25.12
    +      '@esbuild/linux-ia32': 0.25.12
    +      '@esbuild/linux-loong64': 0.25.12
    +      '@esbuild/linux-mips64el': 0.25.12
    +      '@esbuild/linux-ppc64': 0.25.12
    +      '@esbuild/linux-riscv64': 0.25.12
    +      '@esbuild/linux-s390x': 0.25.12
    +      '@esbuild/linux-x64': 0.25.12
    +      '@esbuild/netbsd-arm64': 0.25.12
    +      '@esbuild/netbsd-x64': 0.25.12
    +      '@esbuild/openbsd-arm64': 0.25.12
    +      '@esbuild/openbsd-x64': 0.25.12
    +      '@esbuild/openharmony-arm64': 0.25.12
    +      '@esbuild/sunos-x64': 0.25.12
    +      '@esbuild/win32-arm64': 0.25.12
    +      '@esbuild/win32-ia32': 0.25.12
    +      '@esbuild/win32-x64': 0.25.12
    +
    +  esbuild@0.27.3:
    +    optionalDependencies:
    +      '@esbuild/aix-ppc64': 0.27.3
    +      '@esbuild/android-arm': 0.27.3
    +      '@esbuild/android-arm64': 0.27.3
    +      '@esbuild/android-x64': 0.27.3
    +      '@esbuild/darwin-arm64': 0.27.3
    +      '@esbuild/darwin-x64': 0.27.3
    +      '@esbuild/freebsd-arm64': 0.27.3
    +      '@esbuild/freebsd-x64': 0.27.3
    +      '@esbuild/linux-arm': 0.27.3
    +      '@esbuild/linux-arm64': 0.27.3
    +      '@esbuild/linux-ia32': 0.27.3
    +      '@esbuild/linux-loong64': 0.27.3
    +      '@esbuild/linux-mips64el': 0.27.3
    +      '@esbuild/linux-ppc64': 0.27.3
    +      '@esbuild/linux-riscv64': 0.27.3
    +      '@esbuild/linux-s390x': 0.27.3
    +      '@esbuild/linux-x64': 0.27.3
    +      '@esbuild/netbsd-arm64': 0.27.3
    +      '@esbuild/netbsd-x64': 0.27.3
    +      '@esbuild/openbsd-arm64': 0.27.3
    +      '@esbuild/openbsd-x64': 0.27.3
    +      '@esbuild/openharmony-arm64': 0.27.3
    +      '@esbuild/sunos-x64': 0.27.3
    +      '@esbuild/win32-arm64': 0.27.3
    +      '@esbuild/win32-ia32': 0.27.3
    +      '@esbuild/win32-x64': 0.27.3
    +
    +  escalade@3.2.0: {}
    +
    +  esprima@4.0.1: {}
    +
    +  estree-walker@0.6.1: {}
    +
    +  estree-walker@1.0.1: {}
    +
    +  estree-walker@2.0.2: {}
    +
    +  estree-walker@3.0.3:
    +    dependencies:
    +      '@types/estree': 1.0.8
    +
    +  evp_bytestokey@1.0.3:
    +    dependencies:
    +      md5.js: 1.3.5
    +      safe-buffer: 5.2.1
    +
    +  execa@5.1.1:
    +    dependencies:
    +      cross-spawn: 7.0.6
    +      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
    +
    +  expand-brackets@2.1.4:
    +    dependencies:
    +      debug: 2.6.9
    +      define-property: 0.2.5
    +      extend-shallow: 2.0.1
    +      posix-character-classes: 0.1.1
    +      regex-not: 1.0.2
    +      snapdragon: 0.8.2
    +      to-regex: 3.0.2
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  expect-type@1.3.0: {}
    +
    +  exsolve@1.0.8: {}
    +
    +  extend-shallow@2.0.1:
    +    dependencies:
    +      is-extendable: 0.1.1
    +
    +  extend-shallow@3.0.2:
    +    dependencies:
    +      assign-symbols: 1.0.0
    +      is-extendable: 1.0.1
    +
    +  extendable-error@0.1.7: {}
    +
    +  extglob@2.0.4:
    +    dependencies:
    +      array-unique: 0.3.2
    +      define-property: 1.0.0
    +      expand-brackets: 2.1.4
    +      extend-shallow: 2.0.1
    +      fragment-cache: 0.2.1
    +      regex-not: 1.0.2
    +      snapdragon: 0.8.2
    +      to-regex: 3.0.2
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  fast-deep-equal@3.1.3: {}
    +
    +  fast-equals@6.0.0: {}
    +
    +  fast-glob@3.3.3:
    +    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.8
    +
    +  fast-json-stable-stringify@2.1.0: {}
    +
    +  fastq@1.20.1:
    +    dependencies:
    +      reusify: 1.1.0
    +
    +  fd-package-json@2.0.0:
    +    dependencies:
    +      walk-up-path: 4.0.0
    +
    +  fdir@6.5.0(picomatch@4.0.4):
    +    optionalDependencies:
    +      picomatch: 4.0.4
    +
    +  file-saver@2.0.5: {}
    +
    +  fill-range@4.0.0:
    +    dependencies:
    +      extend-shallow: 2.0.1
    +      is-number: 3.0.0
    +      repeat-string: 1.6.1
    +      to-regex-range: 2.1.1
    +
    +  fill-range@7.1.1:
    +    dependencies:
    +      to-regex-range: 5.0.1
    +
    +  find-up@3.0.0:
    +    dependencies:
    +      locate-path: 3.0.0
    +
    +  find-up@4.1.0:
    +    dependencies:
    +      locate-path: 5.0.0
    +      path-exists: 4.0.0
    +
    +  find-up@8.0.0:
    +    dependencies:
    +      locate-path: 8.0.0
    +      unicorn-magic: 0.3.0
    +
    +  find-yarn-workspace-root@1.2.1:
    +    dependencies:
    +      fs-extra: 4.0.3
    +      micromatch: 3.1.10
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  flatted@3.4.2: {}
    +
    +  for-each@0.3.5:
    +    dependencies:
    +      is-callable: 1.2.7
    +
    +  for-in@1.0.2: {}
    +
    +  foreach@2.0.6: {}
    +
    +  formatly@0.3.0:
    +    dependencies:
    +      fd-package-json: 2.0.0
    +
    +  fragment-cache@0.2.1:
    +    dependencies:
    +      map-cache: 0.2.2
    +
    +  fs-extra@4.0.3:
    +    dependencies:
    +      graceful-fs: 4.2.11
    +      jsonfile: 4.0.0
    +      universalify: 0.1.2
    +
    +  fs-extra@7.0.1:
    +    dependencies:
    +      graceful-fs: 4.2.11
    +      jsonfile: 4.0.0
    +      universalify: 0.1.2
    +
    +  fs-extra@8.1.0:
    +    dependencies:
    +      graceful-fs: 4.2.11
    +      jsonfile: 4.0.0
    +      universalify: 0.1.2
    +
    +  fs-extra@9.1.0:
    +    dependencies:
    +      at-least-node: 1.0.0
    +      graceful-fs: 4.2.11
    +      jsonfile: 6.2.0
    +      universalify: 2.0.1
    +
    +  fs.realpath@1.0.0: {}
    +
    +  fsevents@2.3.2:
    +    optional: true
    +
    +  fsevents@2.3.3:
    +    optional: true
    +
    +  function-bind@1.1.2: {}
    +
    +  fwd-stream@1.0.4:
    +    dependencies:
    +      readable-stream: 1.0.34
    +
    +  gensequence@8.0.8: {}
    +
    +  gensync@1.0.0-beta.2: {}
    +
    +  get-caller-file@2.0.5: {}
    +
    +  get-intrinsic@1.3.0:
    +    dependencies:
    +      call-bind-apply-helpers: 1.0.2
    +      es-define-property: 1.0.1
    +      es-errors: 1.3.0
    +      es-object-atoms: 1.1.1
    +      function-bind: 1.1.2
    +      get-proto: 1.0.1
    +      gopd: 1.2.0
    +      has-symbols: 1.1.0
    +      hasown: 2.0.2
    +      math-intrinsics: 1.1.0
    +
    +  get-proto@1.0.1:
    +    dependencies:
    +      dunder-proto: 1.0.1
    +      es-object-atoms: 1.1.1
    +
    +  get-stream@6.0.1: {}
    +
    +  get-tsconfig@4.13.7:
    +    dependencies:
    +      resolve-pkg-maps: 1.0.0
    +
    +  get-value@2.0.6: {}
    +
    +  glob-parent@5.1.2:
    +    dependencies:
    +      is-glob: 4.0.3
    +
    +  glob@13.0.6:
    +    dependencies:
    +      minimatch: 10.2.2
    +      minipass: 7.1.3
    +      path-scurry: 2.0.2
    +
    +  glob@7.2.3:
    +    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
    +
    +  global-directory@5.0.0:
    +    dependencies:
    +      ini: 6.0.0
    +
    +  global@4.4.0:
    +    dependencies:
    +      min-document: 2.19.2
    +      process: 0.11.10
    +
    +  globby@11.1.0:
    +    dependencies:
    +      array-union: 2.1.0
    +      dir-glob: 3.0.1
    +      fast-glob: 3.3.3
    +      ignore: 5.3.2
    +      merge2: 1.4.1
    +      slash: 3.0.0
    +
    +  gopd@1.2.0: {}
    +
    +  graceful-fs@4.2.11: {}
    +
    +  has-flag@4.0.0: {}
    +
    +  has-property-descriptors@1.0.2:
    +    dependencies:
    +      es-define-property: 1.0.1
    +
    +  has-symbols@1.1.0: {}
    +
    +  has-tostringtag@1.0.2:
    +    dependencies:
    +      has-symbols: 1.1.0
    +
    +  has-value@0.3.1:
    +    dependencies:
    +      get-value: 2.0.6
    +      has-values: 0.1.4
    +      isobject: 2.1.0
    +
    +  has-value@1.0.0:
    +    dependencies:
    +      get-value: 2.0.6
    +      has-values: 1.0.0
    +      isobject: 3.0.1
    +
    +  has-values@0.1.4: {}
    +
    +  has-values@1.0.0:
    +    dependencies:
    +      is-number: 3.0.0
    +      kind-of: 4.0.0
    +
    +  hash-base@3.0.5:
    +    dependencies:
    +      inherits: 2.0.4
    +      safe-buffer: 5.2.1
    +
    +  hash-base@3.1.2:
    +    dependencies:
    +      inherits: 2.0.4
    +      readable-stream: 2.3.8
    +      safe-buffer: 5.2.1
    +      to-buffer: 1.2.2
    +
    +  hash.js@1.1.7:
    +    dependencies:
    +      inherits: 2.0.4
    +      minimalistic-assert: 1.0.1
    +
    +  hasown@2.0.2:
    +    dependencies:
    +      function-bind: 1.1.2
    +
    +  hmac-drbg@1.0.1:
    +    dependencies:
    +      hash.js: 1.1.7
    +      minimalistic-assert: 1.0.1
    +      minimalistic-crypto-utils: 1.0.1
    +
    +  hookable@6.1.0: {}
    +
    +  html-encoding-sniffer@6.0.0:
    +    dependencies:
    +      '@exodus/bytes': 1.14.0
    +    transitivePeerDependencies:
    +      - '@noble/hashes'
    +    optional: true
    +
    +  http-proxy-agent@7.0.2:
    +    dependencies:
    +      agent-base: 7.1.4
    +      debug: 4.4.3
    +    transitivePeerDependencies:
    +      - supports-color
    +    optional: true
    +
    +  https-proxy-agent@7.0.6:
    +    dependencies:
    +      agent-base: 7.1.4
    +      debug: 4.4.3
    +    transitivePeerDependencies:
    +      - supports-color
    +    optional: true
    +
    +  human-id@4.1.3: {}
    +
    +  human-signals@2.1.0: {}
    +
    +  iconv-lite@0.7.2:
    +    dependencies:
    +      safer-buffer: 2.1.2
    +
    +  idb-wrapper@1.7.2: {}
    +
    +  ignore@5.3.2: {}
    +
    +  immutable@5.1.4: {}
    +
    +  import-fresh@2.0.0:
    +    dependencies:
    +      caller-path: 2.0.0
    +      resolve-from: 3.0.0
    +
    +  import-fresh@3.3.1:
    +    dependencies:
    +      parent-module: 1.0.1
    +      resolve-from: 4.0.0
    +
    +  import-meta-resolve@4.2.0: {}
    +
    +  import-without-cache@0.2.5: {}
    +
    +  indexof@0.0.1: {}
    +
    +  individual@2.0.0: {}
    +
    +  inflight@1.0.6:
    +    dependencies:
    +      once: 1.4.0
    +      wrappy: 1.0.2
    +
    +  inherits@2.0.4: {}
    +
    +  ini@1.3.8: {}
    +
    +  ini@6.0.0: {}
    +
    +  ionicons@7.4.0:
    +    dependencies:
    +      '@stencil/core': 4.43.1
    +
    +  is-accessor-descriptor@1.0.1:
    +    dependencies:
    +      hasown: 2.0.2
    +
    +  is-arrayish@0.2.1: {}
    +
    +  is-buffer@1.1.6: {}
    +
    +  is-callable@1.2.7: {}
    +
    +  is-core-module@2.16.1:
    +    dependencies:
    +      hasown: 2.0.2
    +
    +  is-data-descriptor@1.0.1:
    +    dependencies:
    +      hasown: 2.0.2
    +
    +  is-descriptor@0.1.7:
    +    dependencies:
    +      is-accessor-descriptor: 1.0.1
    +      is-data-descriptor: 1.0.1
    +
    +  is-descriptor@1.0.3:
    +    dependencies:
    +      is-accessor-descriptor: 1.0.1
    +      is-data-descriptor: 1.0.1
    +
    +  is-directory@0.3.1: {}
    +
    +  is-docker@2.2.1: {}
    +
    +  is-docker@3.0.0: {}
    +
    +  is-extendable@0.1.1: {}
    +
    +  is-extendable@1.0.1:
    +    dependencies:
    +      is-plain-object: 2.0.4
    +
    +  is-extglob@2.1.1: {}
    +
    +  is-fullwidth-code-point@2.0.0: {}
    +
    +  is-fullwidth-code-point@3.0.0: {}
    +
    +  is-function@1.0.2: {}
    +
    +  is-glob@4.0.3:
    +    dependencies:
    +      is-extglob: 2.1.1
    +
    +  is-in-ssh@1.0.0: {}
    +
    +  is-inside-container@1.0.0:
    +    dependencies:
    +      is-docker: 3.0.0
    +
    +  is-number@3.0.0:
    +    dependencies:
    +      kind-of: 3.2.2
    +
    +  is-number@7.0.0: {}
    +
    +  is-object@0.1.2: {}
    +
    +  is-plain-object@2.0.4:
    +    dependencies:
    +      isobject: 3.0.1
    +
    +  is-port-reachable@4.0.0: {}
    +
    +  is-potential-custom-element-name@1.0.1:
    +    optional: true
    +
    +  is-safe-filename@0.1.1: {}
    +
    +  is-stream@2.0.1: {}
    +
    +  is-subdir@1.2.0:
    +    dependencies:
    +      better-path-resolve: 1.0.0
    +
    +  is-typed-array@1.1.15:
    +    dependencies:
    +      which-typed-array: 1.1.20
    +
    +  is-windows@1.0.2: {}
    +
    +  is-wsl@2.2.0:
    +    dependencies:
    +      is-docker: 2.2.1
    +
    +  is-wsl@3.1.1:
    +    dependencies:
    +      is-inside-container: 1.0.0
    +
    +  is@0.2.7: {}
    +
    +  isarray@0.0.1: {}
    +
    +  isarray@1.0.0: {}
    +
    +  isarray@2.0.5: {}
    +
    +  isbuffer@0.0.0: {}
    +
    +  isexe@2.0.0: {}
    +
    +  isobject@2.1.0:
    +    dependencies:
    +      isarray: 1.0.0
    +
    +  isobject@3.0.1: {}
    +
    +  jiti@2.6.1: {}
    +
    +  js-tokens@4.0.0: {}
    +
    +  js-yaml@3.14.2:
    +    dependencies:
    +      argparse: 1.0.10
    +      esprima: 4.0.1
    +
    +  js-yaml@4.1.1:
    +    dependencies:
    +      argparse: 2.0.1
    +
    +  jsdom@28.0.0:
    +    dependencies:
    +      '@acemir/cssom': 0.9.31
    +      '@asamuzakjp/dom-selector': 6.7.8
    +      '@exodus/bytes': 1.14.0
    +      cssstyle: 5.3.7
    +      data-urls: 7.0.0
    +      decimal.js: 10.6.0
    +      html-encoding-sniffer: 6.0.0
    +      http-proxy-agent: 7.0.2
    +      https-proxy-agent: 7.0.6
    +      is-potential-custom-element-name: 1.0.1
    +      parse5: 8.0.0
    +      saxes: 6.0.0
    +      symbol-tree: 3.2.4
    +      tough-cookie: 6.0.0
    +      undici: 7.21.0
    +      w3c-xmlserializer: 5.0.0
    +      webidl-conversions: 8.0.1
    +      whatwg-mimetype: 5.0.0
    +      whatwg-url: 16.0.0
    +      xml-name-validator: 5.0.0
    +    transitivePeerDependencies:
    +      - '@noble/hashes'
    +      - supports-color
    +    optional: true
    +
    +  jsesc@3.1.0: {}
    +
    +  json-parse-better-errors@1.0.2: {}
    +
    +  json-schema-traverse@1.0.0: {}
    +
    +  json5@1.0.2:
    +    dependencies:
    +      minimist: 1.2.8
    +
    +  json5@2.2.3: {}
    +
    +  jsonfile@4.0.0:
    +    optionalDependencies:
    +      graceful-fs: 4.2.11
    +
    +  jsonfile@6.2.0:
    +    dependencies:
    +      universalify: 2.0.1
    +    optionalDependencies:
    +      graceful-fs: 4.2.11
    +
    +  keycode@2.2.1: {}
    +
    +  kind-of@3.2.2:
    +    dependencies:
    +      is-buffer: 1.1.6
    +
    +  kind-of@4.0.0:
    +    dependencies:
    +      is-buffer: 1.1.6
    +
    +  kind-of@6.0.3: {}
    +
    +  kleur@3.0.3: {}
    +
    +  knip@6.1.0:
    +    dependencies:
    +      '@nodelib/fs.walk': 1.2.8
    +      fast-glob: 3.3.3
    +      formatly: 0.3.0
    +      get-tsconfig: 4.13.7
    +      jiti: 2.6.1
    +      minimist: 1.2.8
    +      oxc-parser: 0.121.0
    +      oxc-resolver: 11.19.1
    +      picocolors: 1.1.1
    +      picomatch: 4.0.4
    +      smol-toml: 1.6.1
    +      strip-json-comments: 5.0.3
    +      unbash: 2.2.0
    +      yaml: 2.8.3
    +      zod: 4.3.6
    +
    +  launch-editor@2.13.0:
    +    dependencies:
    +      picocolors: 1.1.1
    +      shell-quote: 1.8.3
    +
    +  level-blobs@0.1.7:
    +    dependencies:
    +      level-peek: 1.0.6
    +      once: 1.4.0
    +      readable-stream: 1.1.14
    +
    +  level-filesystem@1.2.0:
    +    dependencies:
    +      concat-stream: 1.6.2
    +      errno: 0.1.8
    +      fwd-stream: 1.0.4
    +      level-blobs: 0.1.7
    +      level-peek: 1.0.6
    +      level-sublevel: 5.2.3
    +      octal: 1.0.0
    +      once: 1.4.0
    +      xtend: 2.2.0
    +
    +  level-fix-range@1.0.2: {}
    +
    +  level-fix-range@2.0.0:
    +    dependencies:
    +      clone: 0.1.19
    +
    +  level-hooks@4.5.0:
    +    dependencies:
    +      string-range: 1.2.2
    +
    +  level-js@2.2.4:
    +    dependencies:
    +      abstract-leveldown: 0.12.4
    +      idb-wrapper: 1.7.2
    +      isbuffer: 0.0.0
    +      ltgt: 2.2.1
    +      typedarray-to-buffer: 1.0.4
    +      xtend: 2.1.2
    +
    +  level-peek@1.0.6:
    +    dependencies:
    +      level-fix-range: 1.0.2
    +
    +  level-sublevel@5.2.3:
    +    dependencies:
    +      level-fix-range: 2.0.0
    +      level-hooks: 4.5.0
    +      string-range: 1.2.2
    +      xtend: 2.0.6
    +
    +  levelup@0.18.6:
    +    dependencies:
    +      bl: 0.8.2
    +      deferred-leveldown: 0.2.0
    +      errno: 0.1.8
    +      prr: 0.0.0
    +      readable-stream: 1.0.34
    +      semver: 2.3.2
    +      xtend: 3.0.0
    +
    +  lightningcss-android-arm64@1.32.0:
    +    optional: true
    +
    +  lightningcss-darwin-arm64@1.32.0:
    +    optional: true
    +
    +  lightningcss-darwin-x64@1.32.0:
    +    optional: true
    +
    +  lightningcss-freebsd-x64@1.32.0:
    +    optional: true
    +
    +  lightningcss-linux-arm-gnueabihf@1.32.0:
    +    optional: true
    +
    +  lightningcss-linux-arm64-gnu@1.32.0:
    +    optional: true
    +
    +  lightningcss-linux-arm64-musl@1.32.0:
    +    optional: true
    +
    +  lightningcss-linux-x64-gnu@1.32.0:
    +    optional: true
    +
    +  lightningcss-linux-x64-musl@1.32.0:
    +    optional: true
    +
    +  lightningcss-win32-arm64-msvc@1.32.0:
    +    optional: true
    +
    +  lightningcss-win32-x64-msvc@1.32.0:
    +    optional: true
    +
    +  lightningcss@1.32.0:
    +    dependencies:
    +      detect-libc: 2.1.2
    +    optionalDependencies:
    +      lightningcss-android-arm64: 1.32.0
    +      lightningcss-darwin-arm64: 1.32.0
    +      lightningcss-darwin-x64: 1.32.0
    +      lightningcss-freebsd-x64: 1.32.0
    +      lightningcss-linux-arm-gnueabihf: 1.32.0
    +      lightningcss-linux-arm64-gnu: 1.32.0
    +      lightningcss-linux-arm64-musl: 1.32.0
    +      lightningcss-linux-x64-gnu: 1.32.0
    +      lightningcss-linux-x64-musl: 1.32.0
    +      lightningcss-win32-arm64-msvc: 1.32.0
    +      lightningcss-win32-x64-msvc: 1.32.0
    +
    +  lilconfig@3.1.3: {}
    +
    +  linaria@1.4.1(@babel/core@7.29.0):
    +    dependencies:
    +      '@babel/core': 7.29.0
    +      '@babel/generator': 7.29.1
    +      '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.29.0)
    +      '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0)
    +      '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
    +      '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0)
    +      '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0)
    +      '@babel/template': 7.28.6
    +      '@emotion/is-prop-valid': 0.8.8
    +      babel-plugin-transform-react-remove-prop-types: 0.4.24
    +      core-js: 3.48.0
    +      cosmiconfig: 5.2.1
    +      debug: 4.4.3
    +      enhanced-resolve: 4.5.0
    +      find-yarn-workspace-root: 1.2.1
    +      glob: 7.2.3
    +      loader-utils: 1.4.2
    +      mkdirp: 0.5.6
    +      normalize-path: 3.0.0
    +      postcss: 7.0.39
    +      react-is: 16.13.1
    +      rollup-pluginutils: 2.8.2
    +      source-map: 0.6.1
    +      strip-ansi: 5.2.0
    +      stylis: 3.5.4
    +      yargs: 13.3.2
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  loader-utils@1.4.2:
    +    dependencies:
    +      big.js: 5.2.2
    +      emojis-list: 3.0.0
    +      json5: 1.0.2
    +
    +  local-pkg@1.1.2:
    +    dependencies:
    +      mlly: 1.8.0
    +      pkg-types: 2.3.0
    +      quansync: 0.2.11
    +
    +  locate-path@3.0.0:
    +    dependencies:
    +      p-locate: 3.0.0
    +      path-exists: 3.0.0
    +
    +  locate-path@5.0.0:
    +    dependencies:
    +      p-locate: 4.1.0
    +
    +  locate-path@8.0.0:
    +    dependencies:
    +      p-locate: 6.0.0
    +
    +  lodash-es@4.17.23: {}
    +
    +  lodash.debounce@4.0.8: {}
    +
    +  lodash.startcase@4.4.0: {}
    +
    +  lodash@4.17.23: {}
    +
    +  lru-cache@11.2.7: {}
    +
    +  lru-cache@5.1.1:
    +    dependencies:
    +      yallist: 3.1.1
    +
    +  ltgt@2.2.1: {}
    +
    +  m3u8-parser@4.8.0:
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      '@videojs/vhs-utils': 3.0.5
    +      global: 4.4.0
    +
    +  magic-string@0.30.21:
    +    dependencies:
    +      '@jridgewell/sourcemap-codec': 1.5.5
    +
    +  map-cache@0.2.2: {}
    +
    +  map-visit@1.0.0:
    +    dependencies:
    +      object-visit: 1.0.1
    +
    +  math-intrinsics@1.1.0: {}
    +
    +  md5.js@1.3.5:
    +    dependencies:
    +      hash-base: 3.1.2
    +      inherits: 2.0.4
    +      safe-buffer: 5.2.1
    +
    +  mdn-data@2.27.1:
    +    optional: true
    +
    +  memory-fs@0.5.0:
    +    dependencies:
    +      errno: 0.1.8
    +      readable-stream: 2.3.8
    +
    +  merge-stream@2.0.0: {}
    +
    +  merge2@1.4.1: {}
    +
    +  micromatch@3.1.10:
    +    dependencies:
    +      arr-diff: 4.0.0
    +      array-unique: 0.3.2
    +      braces: 2.3.2
    +      define-property: 2.0.2
    +      extend-shallow: 3.0.2
    +      extglob: 2.0.4
    +      fragment-cache: 0.2.1
    +      kind-of: 6.0.3
    +      nanomatch: 1.2.13
    +      object.pick: 1.3.0
    +      regex-not: 1.0.2
    +      snapdragon: 0.8.2
    +      to-regex: 3.0.2
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  micromatch@4.0.8:
    +    dependencies:
    +      braces: 3.0.3
    +      picomatch: 2.3.1
    +
    +  miller-rabin@4.0.1:
    +    dependencies:
    +      bn.js: 4.12.3
    +      brorand: 1.1.0
    +
    +  mime-db@1.33.0: {}
    +
    +  mime-db@1.52.0: {}
    +
    +  mime-types@2.1.18:
    +    dependencies:
    +      mime-db: 1.33.0
    +
    +  mimic-fn@2.1.0: {}
    +
    +  min-document@2.19.2:
    +    dependencies:
    +      dom-walk: 0.1.2
    +
    +  minimalistic-assert@1.0.1: {}
    +
    +  minimalistic-crypto-utils@1.0.1: {}
    +
    +  minimatch@10.2.2:
    +    dependencies:
    +      brace-expansion: 5.0.2
    +
    +  minimatch@3.1.2:
    +    dependencies:
    +      brace-expansion: 1.1.12
    +
    +  minimist@1.2.8: {}
    +
    +  minipass@7.1.3: {}
    +
    +  mixin-deep@1.3.2:
    +    dependencies:
    +      for-in: 1.0.2
    +      is-extendable: 1.0.1
    +
    +  mkdirp@0.5.6:
    +    dependencies:
    +      minimist: 1.2.8
    +
    +  mlly@1.8.0:
    +    dependencies:
    +      acorn: 8.15.0
    +      pathe: 2.0.3
    +      pkg-types: 1.3.1
    +      ufo: 1.6.3
    +
    +  mpd-parser@0.22.1:
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      '@videojs/vhs-utils': 3.0.5
    +      '@xmldom/xmldom': 0.8.11
    +      global: 4.4.0
    +
    +  mri@1.2.0: {}
    +
    +  mrmime@2.0.1: {}
    +
    +  ms@2.0.0: {}
    +
    +  ms@2.1.3: {}
    +
    +  mux.js@6.0.1:
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      global: 4.4.0
    +
    +  nanoid@3.3.11: {}
    +
    +  nanomatch@1.2.13:
    +    dependencies:
    +      arr-diff: 4.0.0
    +      array-unique: 0.3.2
    +      define-property: 2.0.2
    +      extend-shallow: 3.0.2
    +      fragment-cache: 0.2.1
    +      is-windows: 1.0.2
    +      kind-of: 6.0.3
    +      object.pick: 1.3.0
    +      regex-not: 1.0.2
    +      snapdragon: 0.8.2
    +      to-regex: 3.0.2
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  negotiator@0.6.4: {}
    +
    +  node-addon-api@7.1.1: {}
    +
    +  node-releases@2.0.27: {}
    +
    +  normalize-path@3.0.0: {}
    +
    +  normalize.css@8.0.1: {}
    +
    +  npm-run-path@4.0.1:
    +    dependencies:
    +      path-key: 3.1.1
    +
    +  nwsapi@2.2.23: {}
    +
    +  object-copy@0.1.0:
    +    dependencies:
    +      copy-descriptor: 0.1.1
    +      define-property: 0.2.5
    +      kind-of: 3.2.2
    +
    +  object-keys@0.2.0:
    +    dependencies:
    +      foreach: 2.0.6
    +      indexof: 0.0.1
    +      is: 0.2.7
    +
    +  object-keys@0.4.0: {}
    +
    +  object-visit@1.0.1:
    +    dependencies:
    +      isobject: 3.0.1
    +
    +  object.pick@1.3.0:
    +    dependencies:
    +      isobject: 3.0.1
    +
    +  obug@2.1.1: {}
    +
    +  octal@1.0.0: {}
    +
    +  on-headers@1.1.0: {}
    +
    +  once@1.4.0:
    +    dependencies:
    +      wrappy: 1.0.2
    +
    +  onetime@5.1.2:
    +    dependencies:
    +      mimic-fn: 2.1.0
    +
    +  open@11.0.0:
    +    dependencies:
    +      default-browser: 5.5.0
    +      define-lazy-prop: 3.0.0
    +      is-in-ssh: 1.0.0
    +      is-inside-container: 1.0.0
    +      powershell-utils: 0.1.0
    +      wsl-utils: 0.3.1
    +
    +  outdent@0.5.0: {}
    +
    +  oxc-parser@0.121.0:
    +    dependencies:
    +      '@oxc-project/types': 0.121.0
    +    optionalDependencies:
    +      '@oxc-parser/binding-android-arm-eabi': 0.121.0
    +      '@oxc-parser/binding-android-arm64': 0.121.0
    +      '@oxc-parser/binding-darwin-arm64': 0.121.0
    +      '@oxc-parser/binding-darwin-x64': 0.121.0
    +      '@oxc-parser/binding-freebsd-x64': 0.121.0
    +      '@oxc-parser/binding-linux-arm-gnueabihf': 0.121.0
    +      '@oxc-parser/binding-linux-arm-musleabihf': 0.121.0
    +      '@oxc-parser/binding-linux-arm64-gnu': 0.121.0
    +      '@oxc-parser/binding-linux-arm64-musl': 0.121.0
    +      '@oxc-parser/binding-linux-ppc64-gnu': 0.121.0
    +      '@oxc-parser/binding-linux-riscv64-gnu': 0.121.0
    +      '@oxc-parser/binding-linux-riscv64-musl': 0.121.0
    +      '@oxc-parser/binding-linux-s390x-gnu': 0.121.0
    +      '@oxc-parser/binding-linux-x64-gnu': 0.121.0
    +      '@oxc-parser/binding-linux-x64-musl': 0.121.0
    +      '@oxc-parser/binding-openharmony-arm64': 0.121.0
    +      '@oxc-parser/binding-wasm32-wasi': 0.121.0
    +      '@oxc-parser/binding-win32-arm64-msvc': 0.121.0
    +      '@oxc-parser/binding-win32-ia32-msvc': 0.121.0
    +      '@oxc-parser/binding-win32-x64-msvc': 0.121.0
    +
    +  oxc-resolver@11.19.1:
    +    optionalDependencies:
    +      '@oxc-resolver/binding-android-arm-eabi': 11.19.1
    +      '@oxc-resolver/binding-android-arm64': 11.19.1
    +      '@oxc-resolver/binding-darwin-arm64': 11.19.1
    +      '@oxc-resolver/binding-darwin-x64': 11.19.1
    +      '@oxc-resolver/binding-freebsd-x64': 11.19.1
    +      '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1
    +      '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1
    +      '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1
    +      '@oxc-resolver/binding-linux-arm64-musl': 11.19.1
    +      '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1
    +      '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1
    +      '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1
    +      '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1
    +      '@oxc-resolver/binding-linux-x64-gnu': 11.19.1
    +      '@oxc-resolver/binding-linux-x64-musl': 11.19.1
    +      '@oxc-resolver/binding-openharmony-arm64': 11.19.1
    +      '@oxc-resolver/binding-wasm32-wasi': 11.19.1
    +      '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1
    +      '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1
    +      '@oxc-resolver/binding-win32-x64-msvc': 11.19.1
    +
    +  oxfmt@0.42.0:
    +    dependencies:
    +      tinypool: 2.1.0
    +    optionalDependencies:
    +      '@oxfmt/binding-android-arm-eabi': 0.42.0
    +      '@oxfmt/binding-android-arm64': 0.42.0
    +      '@oxfmt/binding-darwin-arm64': 0.42.0
    +      '@oxfmt/binding-darwin-x64': 0.42.0
    +      '@oxfmt/binding-freebsd-x64': 0.42.0
    +      '@oxfmt/binding-linux-arm-gnueabihf': 0.42.0
    +      '@oxfmt/binding-linux-arm-musleabihf': 0.42.0
    +      '@oxfmt/binding-linux-arm64-gnu': 0.42.0
    +      '@oxfmt/binding-linux-arm64-musl': 0.42.0
    +      '@oxfmt/binding-linux-ppc64-gnu': 0.42.0
    +      '@oxfmt/binding-linux-riscv64-gnu': 0.42.0
    +      '@oxfmt/binding-linux-riscv64-musl': 0.42.0
    +      '@oxfmt/binding-linux-s390x-gnu': 0.42.0
    +      '@oxfmt/binding-linux-x64-gnu': 0.42.0
    +      '@oxfmt/binding-linux-x64-musl': 0.42.0
    +      '@oxfmt/binding-openharmony-arm64': 0.42.0
    +      '@oxfmt/binding-win32-arm64-msvc': 0.42.0
    +      '@oxfmt/binding-win32-ia32-msvc': 0.42.0
    +      '@oxfmt/binding-win32-x64-msvc': 0.42.0
    +
    +  oxlint@1.57.0:
    +    optionalDependencies:
    +      '@oxlint/binding-android-arm-eabi': 1.57.0
    +      '@oxlint/binding-android-arm64': 1.57.0
    +      '@oxlint/binding-darwin-arm64': 1.57.0
    +      '@oxlint/binding-darwin-x64': 1.57.0
    +      '@oxlint/binding-freebsd-x64': 1.57.0
    +      '@oxlint/binding-linux-arm-gnueabihf': 1.57.0
    +      '@oxlint/binding-linux-arm-musleabihf': 1.57.0
    +      '@oxlint/binding-linux-arm64-gnu': 1.57.0
    +      '@oxlint/binding-linux-arm64-musl': 1.57.0
    +      '@oxlint/binding-linux-ppc64-gnu': 1.57.0
    +      '@oxlint/binding-linux-riscv64-gnu': 1.57.0
    +      '@oxlint/binding-linux-riscv64-musl': 1.57.0
    +      '@oxlint/binding-linux-s390x-gnu': 1.57.0
    +      '@oxlint/binding-linux-x64-gnu': 1.57.0
    +      '@oxlint/binding-linux-x64-musl': 1.57.0
    +      '@oxlint/binding-openharmony-arm64': 1.57.0
    +      '@oxlint/binding-win32-arm64-msvc': 1.57.0
    +      '@oxlint/binding-win32-ia32-msvc': 1.57.0
    +      '@oxlint/binding-win32-x64-msvc': 1.57.0
    +
    +  p-filter@2.1.0:
    +    dependencies:
    +      p-map: 2.1.0
    +
    +  p-limit@2.3.0:
    +    dependencies:
    +      p-try: 2.2.0
    +
    +  p-limit@4.0.0:
    +    dependencies:
    +      yocto-queue: 1.2.2
    +
    +  p-locate@3.0.0:
    +    dependencies:
    +      p-limit: 2.3.0
    +
    +  p-locate@4.1.0:
    +    dependencies:
    +      p-limit: 2.3.0
    +
    +  p-locate@6.0.0:
    +    dependencies:
    +      p-limit: 4.0.0
    +
    +  p-map@2.1.0: {}
    +
    +  p-try@2.2.0: {}
    +
    +  package-json-from-dist@1.0.1: {}
    +
    +  package-manager-detector@0.2.11:
    +    dependencies:
    +      quansync: 0.2.11
    +
    +  parent-module@1.0.1:
    +    dependencies:
    +      callsites: 3.1.0
    +
    +  parent-module@2.0.0:
    +    dependencies:
    +      callsites: 3.1.0
    +
    +  parse-asn1@5.1.9:
    +    dependencies:
    +      asn1.js: 4.10.1
    +      browserify-aes: 1.2.0
    +      evp_bytestokey: 1.0.3
    +      pbkdf2: 3.1.5
    +      safe-buffer: 5.2.1
    +
    +  parse-json@4.0.0:
    +    dependencies:
    +      error-ex: 1.3.4
    +      json-parse-better-errors: 1.0.2
    +
    +  parse5@7.2.1:
    +    dependencies:
    +      entities: 4.5.0
    +
    +  parse5@8.0.0:
    +    dependencies:
    +      entities: 6.0.1
    +    optional: true
    +
    +  pascalcase@0.1.1: {}
    +
    +  path-exists@3.0.0: {}
    +
    +  path-exists@4.0.0: {}
    +
    +  path-is-absolute@1.0.1: {}
    +
    +  path-is-inside@1.0.2: {}
    +
    +  path-key@3.1.1: {}
    +
    +  path-parse@1.0.7: {}
    +
    +  path-scurry@2.0.2:
    +    dependencies:
    +      lru-cache: 11.2.7
    +      minipass: 7.1.3
    +
    +  path-to-regexp@3.3.0: {}
    +
    +  path-type@4.0.0: {}
    +
    +  pathe@2.0.3: {}
    +
    +  pbkdf2@3.1.5:
    +    dependencies:
    +      create-hash: 1.2.0
    +      create-hmac: 1.1.7
    +      ripemd160: 2.0.3
    +      safe-buffer: 5.2.1
    +      sha.js: 2.4.12
    +      to-buffer: 1.2.2
    +
    +  picocolors@0.2.1: {}
    +
    +  picocolors@1.1.1: {}
    +
    +  picomatch@2.3.1: {}
    +
    +  picomatch@4.0.4: {}
    +
    +  pify@4.0.1: {}
    +
    +  pkcs7@1.0.4:
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +
    +  pkg-types@1.3.1:
    +    dependencies:
    +      confbox: 0.1.8
    +      mlly: 1.8.0
    +      pathe: 2.0.3
    +
    +  pkg-types@2.3.0:
    +    dependencies:
    +      confbox: 0.2.4
    +      exsolve: 1.0.8
    +      pathe: 2.0.3
    +
    +  playwright-core@1.58.2: {}
    +
    +  playwright@1.58.2:
    +    dependencies:
    +      playwright-core: 1.58.2
    +    optionalDependencies:
    +      fsevents: 2.3.2
    +
    +  pngjs@7.0.0: {}
    +
    +  posix-character-classes@0.1.1: {}
    +
    +  possible-typed-array-names@1.1.0: {}
    +
    +  postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3):
    +    dependencies:
    +      lilconfig: 3.1.3
    +    optionalDependencies:
    +      jiti: 2.6.1
    +      postcss: 8.5.6
    +      tsx: 4.21.0
    +      yaml: 2.8.3
    +
    +  postcss-safe-parser@7.0.1(postcss@8.5.6):
    +    dependencies:
    +      postcss: 8.5.6
    +
    +  postcss-selector-parser@7.1.1:
    +    dependencies:
    +      cssesc: 3.0.0
    +      util-deprecate: 1.0.2
    +
    +  postcss@7.0.39:
    +    dependencies:
    +      picocolors: 0.2.1
    +      source-map: 0.6.1
    +
    +  postcss@8.5.6:
    +    dependencies:
    +      nanoid: 3.3.11
    +      picocolors: 1.1.1
    +      source-map-js: 1.2.1
    +
    +  powershell-utils@0.1.0: {}
    +
    +  prettier@2.8.8: {}
    +
    +  prettier@3.8.1: {}
    +
    +  process-es6@0.11.6: {}
    +
    +  process-nextick-args@2.0.1: {}
    +
    +  process@0.11.10: {}
    +
    +  prompts@2.4.2:
    +    dependencies:
    +      kleur: 3.0.3
    +      sisteransi: 1.0.5
    +
    +  prr@0.0.0: {}
    +
    +  prr@1.0.1: {}
    +
    +  public-encrypt@4.0.3:
    +    dependencies:
    +      bn.js: 4.12.3
    +      browserify-rsa: 4.1.1
    +      create-hash: 1.2.0
    +      parse-asn1: 5.1.9
    +      randombytes: 2.1.0
    +      safe-buffer: 5.2.1
    +
    +  punycode@2.3.1: {}
    +
    +  quansync@0.2.11: {}
    +
    +  quansync@1.0.0: {}
    +
    +  queue-microtask@1.2.3: {}
    +
    +  randombytes@2.1.0:
    +    dependencies:
    +      safe-buffer: 5.2.1
    +
    +  randomfill@1.0.4:
    +    dependencies:
    +      randombytes: 2.1.0
    +      safe-buffer: 5.2.1
    +
    +  range-parser@1.2.0: {}
    +
    +  rc@1.2.8:
    +    dependencies:
    +      deep-extend: 0.6.0
    +      ini: 1.3.8
    +      minimist: 1.2.8
    +      strip-json-comments: 2.0.1
    +
    +  react-is@16.13.1: {}
    +
    +  read-yaml-file@1.1.0:
    +    dependencies:
    +      graceful-fs: 4.2.11
    +      js-yaml: 3.14.2
    +      pify: 4.0.1
    +      strip-bom: 3.0.0
    +
    +  readable-stream@1.0.34:
    +    dependencies:
    +      core-util-is: 1.0.3
    +      inherits: 2.0.4
    +      isarray: 0.0.1
    +      string_decoder: 0.10.31
    +
    +  readable-stream@1.1.14:
    +    dependencies:
    +      core-util-is: 1.0.3
    +      inherits: 2.0.4
    +      isarray: 0.0.1
    +      string_decoder: 0.10.31
    +
    +  readable-stream@2.3.8:
    +    dependencies:
    +      core-util-is: 1.0.3
    +      inherits: 2.0.4
    +      isarray: 1.0.0
    +      process-nextick-args: 2.0.1
    +      safe-buffer: 5.1.2
    +      string_decoder: 1.1.1
    +      util-deprecate: 1.0.2
    +
    +  readdirp@4.1.2:
    +    optional: true
    +
    +  regex-not@1.0.2:
    +    dependencies:
    +      extend-shallow: 3.0.2
    +      safe-regex: 1.1.0
    +
    +  registry-auth-token@3.3.2:
    +    dependencies:
    +      rc: 1.2.8
    +      safe-buffer: 5.2.1
    +
    +  registry-url@3.1.0:
    +    dependencies:
    +      rc: 1.2.8
    +
    +  repeat-element@1.1.4: {}
    +
    +  repeat-string@1.6.1: {}
    +
    +  require-directory@2.1.1: {}
    +
    +  require-from-string@2.0.2: {}
    +
    +  require-main-filename@2.0.0: {}
    +
    +  resolve-from@3.0.0: {}
    +
    +  resolve-from@4.0.0: {}
    +
    +  resolve-from@5.0.0: {}
    +
    +  resolve-pkg-maps@1.0.0: {}
    +
    +  resolve-url@0.2.1: {}
    +
    +  resolve@1.22.11:
    +    dependencies:
    +      is-core-module: 2.16.1
    +      path-parse: 1.0.7
    +      supports-preserve-symlinks-flag: 1.0.0
    +
    +  ret@0.1.15: {}
    +
    +  reusify@1.1.0: {}
    +
    +  rimraf@6.1.3:
    +    dependencies:
    +      glob: 13.0.6
    +      package-json-from-dist: 1.0.1
    +
    +  ripemd160@2.0.3:
    +    dependencies:
    +      hash-base: 3.1.2
    +      inherits: 2.0.4
    +
    +  rolldown-plugin-dts@0.23.2(oxc-resolver@11.19.1)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(typescript@6.0.2):
    +    dependencies:
    +      '@babel/generator': 8.0.0-rc.3
    +      '@babel/helper-validator-identifier': 8.0.0-rc.3
    +      '@babel/parser': 8.0.0-rc.3
    +      '@babel/types': 8.0.0-rc.3
    +      ast-kit: 3.0.0-beta.1
    +      birpc: 4.0.0
    +      dts-resolver: 2.1.3(oxc-resolver@11.19.1)
    +      get-tsconfig: 4.13.7
    +      obug: 2.1.1
    +      picomatch: 4.0.4
    +      rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
    +    optionalDependencies:
    +      typescript: 6.0.2
    +    transitivePeerDependencies:
    +      - oxc-resolver
    +
    +  rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2):
    +    dependencies:
    +      '@oxc-project/types': 0.122.0
    +      '@rolldown/pluginutils': 1.0.0-rc.12
    +    optionalDependencies:
    +      '@rolldown/binding-android-arm64': 1.0.0-rc.12
    +      '@rolldown/binding-darwin-arm64': 1.0.0-rc.12
    +      '@rolldown/binding-darwin-x64': 1.0.0-rc.12
    +      '@rolldown/binding-freebsd-x64': 1.0.0-rc.12
    +      '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12
    +      '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12
    +      '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12
    +      '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12
    +      '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12
    +      '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12
    +      '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12
    +      '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12
    +      '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
    +      '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12
    +      '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12
    +    transitivePeerDependencies:
    +      - '@emnapi/core'
    +      - '@emnapi/runtime'
    +
    +  rolldown@1.0.0-rc.15:
    +    dependencies:
    +      '@oxc-project/types': 0.124.0
    +      '@rolldown/pluginutils': 1.0.0-rc.15
    +    optionalDependencies:
    +      '@rolldown/binding-android-arm64': 1.0.0-rc.15
    +      '@rolldown/binding-darwin-arm64': 1.0.0-rc.15
    +      '@rolldown/binding-darwin-x64': 1.0.0-rc.15
    +      '@rolldown/binding-freebsd-x64': 1.0.0-rc.15
    +      '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15
    +      '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15
    +      '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15
    +      '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15
    +      '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15
    +      '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15
    +      '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15
    +      '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15
    +      '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15
    +      '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15
    +      '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15
    +
    +  rollup-plugin-css-only@2.1.0(rollup@4.57.1):
    +    dependencies:
    +      '@rollup/pluginutils': 3.1.0(rollup@4.57.1)
    +      fs-extra: 9.1.0
    +      rollup: 4.57.1
    +
    +  rollup-plugin-node-builtins@2.1.2:
    +    dependencies:
    +      browserify-fs: 1.0.0
    +      buffer-es6: 4.9.3
    +      crypto-browserify: 3.12.1
    +      process-es6: 0.11.6
    +
    +  rollup-pluginutils@2.8.2:
    +    dependencies:
    +      estree-walker: 0.6.1
    +
    +  rollup@4.57.1:
    +    dependencies:
    +      '@types/estree': 1.0.8
    +    optionalDependencies:
    +      '@rollup/rollup-android-arm-eabi': 4.57.1
    +      '@rollup/rollup-android-arm64': 4.57.1
    +      '@rollup/rollup-darwin-arm64': 4.57.1
    +      '@rollup/rollup-darwin-x64': 4.57.1
    +      '@rollup/rollup-freebsd-arm64': 4.57.1
    +      '@rollup/rollup-freebsd-x64': 4.57.1
    +      '@rollup/rollup-linux-arm-gnueabihf': 4.57.1
    +      '@rollup/rollup-linux-arm-musleabihf': 4.57.1
    +      '@rollup/rollup-linux-arm64-gnu': 4.57.1
    +      '@rollup/rollup-linux-arm64-musl': 4.57.1
    +      '@rollup/rollup-linux-loong64-gnu': 4.57.1
    +      '@rollup/rollup-linux-loong64-musl': 4.57.1
    +      '@rollup/rollup-linux-ppc64-gnu': 4.57.1
    +      '@rollup/rollup-linux-ppc64-musl': 4.57.1
    +      '@rollup/rollup-linux-riscv64-gnu': 4.57.1
    +      '@rollup/rollup-linux-riscv64-musl': 4.57.1
    +      '@rollup/rollup-linux-s390x-gnu': 4.57.1
    +      '@rollup/rollup-linux-x64-gnu': 4.57.1
    +      '@rollup/rollup-linux-x64-musl': 4.57.1
    +      '@rollup/rollup-openbsd-x64': 4.57.1
    +      '@rollup/rollup-openharmony-arm64': 4.57.1
    +      '@rollup/rollup-win32-arm64-msvc': 4.57.1
    +      '@rollup/rollup-win32-ia32-msvc': 4.57.1
    +      '@rollup/rollup-win32-x64-gnu': 4.57.1
    +      '@rollup/rollup-win32-x64-msvc': 4.57.1
    +      fsevents: 2.3.3
    +
    +  run-applescript@7.1.0: {}
    +
    +  run-parallel@1.2.0:
    +    dependencies:
    +      queue-microtask: 1.2.3
    +
    +  rust-result@1.0.0:
    +    dependencies:
    +      individual: 2.0.0
    +
    +  rxjs@7.8.2:
    +    dependencies:
    +      tslib: 2.8.1
    +
    +  safe-buffer@5.1.2: {}
    +
    +  safe-buffer@5.2.1: {}
    +
    +  safe-json-parse@4.0.0:
    +    dependencies:
    +      rust-result: 1.0.0
    +
    +  safe-regex@1.1.0:
    +    dependencies:
    +      ret: 0.1.15
    +
    +  safer-buffer@2.1.2: {}
    +
    +  sass-embedded-all-unknown@1.97.3:
    +    dependencies:
    +      sass: 1.97.3
    +    optional: true
    +
    +  sass-embedded-android-arm64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-android-arm@1.97.3:
    +    optional: true
    +
    +  sass-embedded-android-riscv64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-android-x64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-darwin-arm64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-darwin-x64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-linux-arm64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-linux-arm@1.97.3:
    +    optional: true
    +
    +  sass-embedded-linux-musl-arm64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-linux-musl-arm@1.97.3:
    +    optional: true
    +
    +  sass-embedded-linux-musl-riscv64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-linux-musl-x64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-linux-riscv64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-linux-x64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-unknown-all@1.97.3:
    +    dependencies:
    +      sass: 1.97.3
    +    optional: true
    +
    +  sass-embedded-win32-arm64@1.97.3:
    +    optional: true
    +
    +  sass-embedded-win32-x64@1.97.3:
    +    optional: true
    +
    +  sass-embedded@1.97.3:
    +    dependencies:
    +      '@bufbuild/protobuf': 2.11.0
    +      colorjs.io: 0.5.2
    +      immutable: 5.1.4
    +      rxjs: 7.8.2
    +      supports-color: 8.1.1
    +      sync-child-process: 1.0.2
    +      varint: 6.0.0
    +    optionalDependencies:
    +      sass-embedded-all-unknown: 1.97.3
    +      sass-embedded-android-arm: 1.97.3
    +      sass-embedded-android-arm64: 1.97.3
    +      sass-embedded-android-riscv64: 1.97.3
    +      sass-embedded-android-x64: 1.97.3
    +      sass-embedded-darwin-arm64: 1.97.3
    +      sass-embedded-darwin-x64: 1.97.3
    +      sass-embedded-linux-arm: 1.97.3
    +      sass-embedded-linux-arm64: 1.97.3
    +      sass-embedded-linux-musl-arm: 1.97.3
    +      sass-embedded-linux-musl-arm64: 1.97.3
    +      sass-embedded-linux-musl-riscv64: 1.97.3
    +      sass-embedded-linux-musl-x64: 1.97.3
    +      sass-embedded-linux-riscv64: 1.97.3
    +      sass-embedded-linux-x64: 1.97.3
    +      sass-embedded-unknown-all: 1.97.3
    +      sass-embedded-win32-arm64: 1.97.3
    +      sass-embedded-win32-x64: 1.97.3
    +
    +  sass@1.97.3:
    +    dependencies:
    +      chokidar: 4.0.3
    +      immutable: 5.1.4
    +      source-map-js: 1.2.1
    +    optionalDependencies:
    +      '@parcel/watcher': 2.5.6
    +    optional: true
    +
    +  saxes@6.0.0:
    +    dependencies:
    +      xmlchars: 2.2.0
    +    optional: true
    +
    +  semver@2.3.2: {}
    +
    +  semver@6.3.1: {}
    +
    +  semver@7.7.4: {}
    +
    +  serve-handler@6.1.6:
    +    dependencies:
    +      bytes: 3.0.0
    +      content-disposition: 0.5.2
    +      mime-types: 2.1.18
    +      minimatch: 3.1.2
    +      path-is-inside: 1.0.2
    +      path-to-regexp: 3.3.0
    +      range-parser: 1.2.0
    +
    +  serve@14.2.5:
    +    dependencies:
    +      '@zeit/schemas': 2.36.0
    +      ajv: 8.12.0
    +      arg: 5.0.2
    +      boxen: 7.0.0
    +      chalk: 5.0.1
    +      chalk-template: 0.4.0
    +      clipboardy: 3.0.0
    +      compression: 1.8.1
    +      is-port-reachable: 4.0.0
    +      serve-handler: 6.1.6
    +      update-check: 1.5.4
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  set-blocking@2.0.0: {}
    +
    +  set-function-length@1.2.2:
    +    dependencies:
    +      define-data-property: 1.1.4
    +      es-errors: 1.3.0
    +      function-bind: 1.1.2
    +      get-intrinsic: 1.3.0
    +      gopd: 1.2.0
    +      has-property-descriptors: 1.0.2
    +
    +  set-value@2.0.1:
    +    dependencies:
    +      extend-shallow: 2.0.1
    +      is-extendable: 0.1.1
    +      is-plain-object: 2.0.4
    +      split-string: 3.1.0
    +
    +  sha.js@2.4.12:
    +    dependencies:
    +      inherits: 2.0.4
    +      safe-buffer: 5.2.1
    +      to-buffer: 1.2.2
    +
    +  shebang-command@2.0.0:
    +    dependencies:
    +      shebang-regex: 3.0.0
    +
    +  shebang-regex@3.0.0: {}
    +
    +  shell-quote@1.8.3: {}
    +
    +  siginfo@2.0.0: {}
    +
    +  signal-exit@3.0.7: {}
    +
    +  signal-exit@4.1.0: {}
    +
    +  sirv@3.0.2:
    +    dependencies:
    +      '@polka/url': 1.0.0-next.29
    +      mrmime: 2.0.1
    +      totalist: 3.0.1
    +
    +  sisteransi@1.0.5: {}
    +
    +  slash@3.0.0: {}
    +
    +  smol-toml@1.6.1: {}
    +
    +  snapdragon-node@2.1.1:
    +    dependencies:
    +      define-property: 1.0.0
    +      isobject: 3.0.1
    +      snapdragon-util: 3.0.1
    +
    +  snapdragon-util@3.0.1:
    +    dependencies:
    +      kind-of: 3.2.2
    +
    +  snapdragon@0.8.2:
    +    dependencies:
    +      base: 0.11.2
    +      debug: 2.6.9
    +      define-property: 0.2.5
    +      extend-shallow: 2.0.1
    +      map-cache: 0.2.2
    +      source-map: 0.5.7
    +      source-map-resolve: 0.5.3
    +      use: 3.1.1
    +    transitivePeerDependencies:
    +      - supports-color
    +
    +  source-map-js@1.2.1: {}
    +
    +  source-map-resolve@0.5.3:
    +    dependencies:
    +      atob: 2.1.2
    +      decode-uri-component: 0.2.2
    +      resolve-url: 0.2.1
    +      source-map-url: 0.4.1
    +      urix: 0.1.0
    +
    +  source-map-support@0.5.21:
    +    dependencies:
    +      buffer-from: 1.1.2
    +      source-map: 0.6.1
    +
    +  source-map-url@0.4.1: {}
    +
    +  source-map@0.5.7: {}
    +
    +  source-map@0.6.1: {}
    +
    +  spawndamnit@3.0.1:
    +    dependencies:
    +      cross-spawn: 7.0.6
    +      signal-exit: 4.1.0
    +
    +  split-string@3.1.0:
    +    dependencies:
    +      extend-shallow: 3.0.2
    +
    +  sprintf-js@1.0.3: {}
    +
    +  stackback@0.0.2: {}
    +
    +  static-extend@0.1.2:
    +    dependencies:
    +      define-property: 0.2.5
    +      object-copy: 0.1.0
    +
    +  std-env@4.0.0: {}
    +
    +  string-range@1.2.2: {}
    +
    +  string-width@3.1.0:
    +    dependencies:
    +      emoji-regex: 7.0.3
    +      is-fullwidth-code-point: 2.0.0
    +      strip-ansi: 5.2.0
    +
    +  string-width@4.2.3:
    +    dependencies:
    +      emoji-regex: 8.0.0
    +      is-fullwidth-code-point: 3.0.0
    +      strip-ansi: 6.0.1
    +
    +  string-width@5.1.2:
    +    dependencies:
    +      eastasianwidth: 0.2.0
    +      emoji-regex: 9.2.2
    +      strip-ansi: 7.1.2
    +
    +  string_decoder@0.10.31: {}
    +
    +  string_decoder@1.1.1:
    +    dependencies:
    +      safe-buffer: 5.1.2
    +
    +  strip-ansi@5.2.0:
    +    dependencies:
    +      ansi-regex: 4.1.1
    +
    +  strip-ansi@6.0.1:
    +    dependencies:
    +      ansi-regex: 5.0.1
    +
    +  strip-ansi@7.1.2:
    +    dependencies:
    +      ansi-regex: 6.2.2
    +
    +  strip-bom@3.0.0: {}
    +
    +  strip-final-newline@2.0.0: {}
    +
    +  strip-json-comments@2.0.1: {}
    +
    +  strip-json-comments@5.0.3: {}
    +
    +  stylis@3.5.4: {}
    +
    +  supports-color@7.2.0:
    +    dependencies:
    +      has-flag: 4.0.0
    +
    +  supports-color@8.1.1:
    +    dependencies:
    +      has-flag: 4.0.0
    +
    +  supports-preserve-symlinks-flag@1.0.0: {}
    +
    +  symbol-tree@3.2.4:
    +    optional: true
    +
    +  sync-child-process@1.0.2:
    +    dependencies:
    +      sync-message-port: 1.2.0
    +
    +  sync-message-port@1.2.0: {}
    +
    +  tapable@1.1.3: {}
    +
    +  term-size@2.2.1: {}
    +
    +  terser@5.37.0:
    +    dependencies:
    +      '@jridgewell/source-map': 0.3.11
    +      acorn: 8.15.0
    +      commander: 2.20.3
    +      source-map-support: 0.5.21
    +
    +  tinybench@2.9.0: {}
    +
    +  tinyexec@1.0.4: {}
    +
    +  tinyglobby@0.2.15:
    +    dependencies:
    +      fdir: 6.5.0(picomatch@4.0.4)
    +      picomatch: 4.0.4
    +
    +  tinypool@2.1.0: {}
    +
    +  tinyrainbow@3.1.0: {}
    +
    +  tldts-core@7.0.23:
    +    optional: true
    +
    +  tldts@7.0.23:
    +    dependencies:
    +      tldts-core: 7.0.23
    +    optional: true
    +
    +  to-buffer@1.2.2:
    +    dependencies:
    +      isarray: 2.0.5
    +      safe-buffer: 5.2.1
    +      typed-array-buffer: 1.0.3
    +
    +  to-object-path@0.3.0:
    +    dependencies:
    +      kind-of: 3.2.2
    +
    +  to-regex-range@2.1.1:
    +    dependencies:
    +      is-number: 3.0.0
    +      repeat-string: 1.6.1
    +
    +  to-regex-range@5.0.1:
    +    dependencies:
    +      is-number: 7.0.0
    +
    +  to-regex@3.0.2:
    +    dependencies:
    +      define-property: 2.0.2
    +      extend-shallow: 3.0.2
    +      regex-not: 1.0.2
    +      safe-regex: 1.1.0
    +
    +  totalist@3.0.1: {}
    +
    +  tough-cookie@6.0.0:
    +    dependencies:
    +      tldts: 7.0.23
    +    optional: true
    +
    +  tr46@6.0.0:
    +    dependencies:
    +      punycode: 2.3.1
    +    optional: true
    +
    +  tree-kill@1.2.2: {}
    +
    +  tsdown@0.21.7(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@tsdown/css@0.21.6)(oxc-resolver@11.19.1)(typescript@6.0.2):
    +    dependencies:
    +      ansis: 4.2.0
    +      cac: 7.0.0
    +      defu: 6.1.4
    +      empathic: 2.0.0
    +      hookable: 6.1.0
    +      import-without-cache: 0.2.5
    +      obug: 2.1.1
    +      picomatch: 4.0.4
    +      rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
    +      rolldown-plugin-dts: 0.23.2(oxc-resolver@11.19.1)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(typescript@6.0.2)
    +      semver: 7.7.4
    +      tinyexec: 1.0.4
    +      tinyglobby: 0.2.15
    +      tree-kill: 1.2.2
    +      unconfig-core: 7.5.0
    +      unrun: 0.2.34(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
    +    optionalDependencies:
    +      '@tsdown/css': 0.21.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(jiti@2.6.1)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(tsdown@0.21.7)(tsx@4.21.0)(yaml@2.8.3)
    +      typescript: 6.0.2
    +    transitivePeerDependencies:
    +      - '@emnapi/core'
    +      - '@emnapi/runtime'
    +      - '@ts-macro/tsc'
    +      - '@typescript/native-preview'
    +      - oxc-resolver
    +      - synckit
    +      - vue-tsc
    +
    +  tslib@2.8.1: {}
    +
    +  tsx@4.21.0:
    +    dependencies:
    +      esbuild: 0.27.3
    +      get-tsconfig: 4.13.7
    +    optionalDependencies:
    +      fsevents: 2.3.3
    +
    +  type-fest@2.19.0: {}
    +
    +  typed-array-buffer@1.0.3:
    +    dependencies:
    +      call-bound: 1.0.4
    +      es-errors: 1.3.0
    +      is-typed-array: 1.1.15
    +
    +  typedarray-to-buffer@1.0.4: {}
    +
    +  typedarray@0.0.6: {}
    +
    +  typescript@6.0.2: {}
    +
    +  ufo@1.6.3: {}
    +
    +  unbash@2.2.0: {}
    +
    +  unconfig-core@7.5.0:
    +    dependencies:
    +      '@quansync/fs': 1.0.0
    +      quansync: 1.0.0
    +
    +  undici-types@7.16.0: {}
    +
    +  undici@7.21.0:
    +    optional: true
    +
    +  unicorn-magic@0.3.0: {}
    +
    +  union-value@1.0.1:
    +    dependencies:
    +      arr-union: 3.1.0
    +      get-value: 2.0.6
    +      is-extendable: 0.1.1
    +      set-value: 2.0.1
    +
    +  universalify@0.1.2: {}
    +
    +  universalify@2.0.1: {}
    +
    +  unrun@0.2.34(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2):
    +    dependencies:
    +      rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
    +    transitivePeerDependencies:
    +      - '@emnapi/core'
    +      - '@emnapi/runtime'
    +
    +  unset-value@1.0.0:
    +    dependencies:
    +      has-value: 0.3.1
    +      isobject: 3.0.1
    +
    +  update-browserslist-db@1.2.3(browserslist@4.28.1):
    +    dependencies:
    +      browserslist: 4.28.1
    +      escalade: 3.2.0
    +      picocolors: 1.1.1
    +
    +  update-check@1.5.4:
    +    dependencies:
    +      registry-auth-token: 3.3.2
    +      registry-url: 3.1.0
    +
    +  uri-js@4.4.1:
    +    dependencies:
    +      punycode: 2.3.1
    +
    +  urix@0.1.0: {}
    +
    +  url-toolkit@2.2.5: {}
    +
    +  urlpattern-polyfill@8.0.2: {}
    +
    +  use@3.1.1: {}
    +
    +  util-deprecate@1.0.2: {}
    +
    +  varint@6.0.0: {}
    +
    +  vary@1.1.2: {}
    +
    +  video.js@7.21.7:
    +    dependencies:
    +      '@babel/runtime': 7.28.6
    +      '@videojs/http-streaming': 2.16.3(video.js@7.21.7)
    +      '@videojs/vhs-utils': 3.0.5
    +      '@videojs/xhr': 2.6.0
    +      aes-decrypter: 3.1.3
    +      global: 4.4.0
    +      keycode: 2.2.1
    +      m3u8-parser: 4.8.0
    +      mpd-parser: 0.22.1
    +      mux.js: 6.0.1
    +      safe-json-parse: 4.0.0
    +      videojs-font: 3.2.0
    +      videojs-vtt.js: 0.15.5
    +
    +  videojs-font@3.2.0: {}
    +
    +  videojs-vtt.js@0.15.5:
    +    dependencies:
    +      global: 4.4.0
    +
    +  vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3):
    +    dependencies:
    +      esbuild: 0.25.12
    +      fdir: 6.5.0(picomatch@4.0.4)
    +      picomatch: 4.0.4
    +      postcss: 8.5.6
    +      rollup: 4.57.1
    +      tinyglobby: 0.2.15
    +    optionalDependencies:
    +      '@types/node': 24.10.13
    +      fsevents: 2.3.3
    +      jiti: 2.6.1
    +      lightningcss: 1.32.0
    +      sass: 1.97.3
    +      sass-embedded: 1.97.3
    +      terser: 5.37.0
    +      tsx: 4.21.0
    +      yaml: 2.8.3
    +
    +  vitest-environment-stencil@1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2):
    +    dependencies:
    +      '@stencil/core': link:packages/core
    +      '@stencil/vitest': 1.11.6(@playwright/test@1.58.2)(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +    transitivePeerDependencies:
    +      - '@playwright/test'
    +      - '@stencil/mock-doc'
    +      - '@vitest/browser-playwright'
    +      - '@vitest/browser-preview'
    +      - '@vitest/browser-webdriverio'
    +      - '@wdio/globals'
    +      - happy-dom
    +      - jsdom
    +      - playwright
    +      - vitest
    +
    +  vitest-environment-stencil@1.11.6(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2):
    +    dependencies:
    +      '@stencil/core': link:packages/core
    +      '@stencil/vitest': 1.11.6(@stencil/core@packages+core)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +    transitivePeerDependencies:
    +      - '@playwright/test'
    +      - '@stencil/mock-doc'
    +      - '@vitest/browser-playwright'
    +      - '@vitest/browser-preview'
    +      - '@vitest/browser-webdriverio'
    +      - '@wdio/globals'
    +      - happy-dom
    +      - jsdom
    +      - playwright
    +      - vitest
    +
    +  vitest-environment-stencil@1.11.6(@stencil/mock-doc@packages+mock-doc)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2):
    +    dependencies:
    +      '@stencil/vitest': 1.11.6(@stencil/mock-doc@packages+mock-doc)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +    transitivePeerDependencies:
    +      - '@playwright/test'
    +      - '@stencil/mock-doc'
    +      - '@vitest/browser-playwright'
    +      - '@vitest/browser-preview'
    +      - '@vitest/browser-webdriverio'
    +      - '@wdio/globals'
    +      - happy-dom
    +      - jsdom
    +      - playwright
    +      - vitest
    +
    +  vitest-environment-stencil@1.11.6(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2):
    +    dependencies:
    +      '@stencil/vitest': 1.11.6(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(playwright@1.58.2)(vitest@4.1.2)
    +    transitivePeerDependencies:
    +      - '@playwright/test'
    +      - '@stencil/mock-doc'
    +      - '@vitest/browser-playwright'
    +      - '@vitest/browser-preview'
    +      - '@vitest/browser-webdriverio'
    +      - '@wdio/globals'
    +      - happy-dom
    +      - jsdom
    +      - playwright
    +      - vitest
    +
    +  vitest@4.1.2(@types/node@24.10.13)(@vitest/browser-playwright@4.1.2)(jsdom@28.0.0)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3)):
    +    dependencies:
    +      '@vitest/expect': 4.1.2
    +      '@vitest/mocker': 4.1.2(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))
    +      '@vitest/pretty-format': 4.1.2
    +      '@vitest/runner': 4.1.2
    +      '@vitest/snapshot': 4.1.2
    +      '@vitest/spy': 4.1.2
    +      '@vitest/utils': 4.1.2
    +      es-module-lexer: 2.0.0
    +      expect-type: 1.3.0
    +      magic-string: 0.30.21
    +      obug: 2.1.1
    +      pathe: 2.0.3
    +      picomatch: 4.0.4
    +      std-env: 4.0.0
    +      tinybench: 2.9.0
    +      tinyexec: 1.0.4
    +      tinyglobby: 0.2.15
    +      tinyrainbow: 3.1.0
    +      vite: 6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3)
    +      why-is-node-running: 2.3.0
    +    optionalDependencies:
    +      '@types/node': 24.10.13
    +      '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.37.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)
    +      jsdom: 28.0.0
    +    transitivePeerDependencies:
    +      - msw
    +
    +  vscode-languageserver-textdocument@1.0.12: {}
    +
    +  vscode-uri@3.1.0: {}
    +
    +  w3c-xmlserializer@5.0.0:
    +    dependencies:
    +      xml-name-validator: 5.0.0
    +    optional: true
    +
    +  walk-up-path@4.0.0: {}
    +
    +  webidl-conversions@8.0.1:
    +    optional: true
    +
    +  whatwg-mimetype@5.0.0:
    +    optional: true
    +
    +  whatwg-url@16.0.0:
    +    dependencies:
    +      '@exodus/bytes': 1.14.0
    +      tr46: 6.0.0
    +      webidl-conversions: 8.0.1
    +    transitivePeerDependencies:
    +      - '@noble/hashes'
    +    optional: true
    +
    +  which-module@2.0.1: {}
    +
    +  which-typed-array@1.1.20:
    +    dependencies:
    +      available-typed-arrays: 1.0.7
    +      call-bind: 1.0.8
    +      call-bound: 1.0.4
    +      for-each: 0.3.5
    +      get-proto: 1.0.1
    +      gopd: 1.2.0
    +      has-tostringtag: 1.0.2
    +
    +  which@2.0.2:
    +    dependencies:
    +      isexe: 2.0.0
    +
    +  why-is-node-running@2.3.0:
    +    dependencies:
    +      siginfo: 2.0.0
    +      stackback: 0.0.2
    +
    +  widest-line@4.0.1:
    +    dependencies:
    +      string-width: 5.1.2
    +
    +  wrap-ansi@5.1.0:
    +    dependencies:
    +      ansi-styles: 3.2.1
    +      string-width: 3.1.0
    +      strip-ansi: 5.2.0
    +
    +  wrap-ansi@8.1.0:
    +    dependencies:
    +      ansi-styles: 6.2.3
    +      string-width: 5.1.2
    +      strip-ansi: 7.1.2
    +
    +  wrappy@1.0.2: {}
    +
    +  ws@8.19.0: {}
    +
    +  wsl-utils@0.3.1:
    +    dependencies:
    +      is-wsl: 3.1.1
    +      powershell-utils: 0.1.0
    +
    +  xdg-basedir@5.1.0: {}
    +
    +  xml-name-validator@5.0.0:
    +    optional: true
    +
    +  xmlchars@2.2.0:
    +    optional: true
    +
    +  xtend@2.0.6:
    +    dependencies:
    +      is-object: 0.1.2
    +      object-keys: 0.2.0
    +
    +  xtend@2.1.2:
    +    dependencies:
    +      object-keys: 0.4.0
    +
    +  xtend@2.2.0: {}
    +
    +  xtend@3.0.0: {}
    +
    +  y18n@4.0.3: {}
    +
    +  yallist@3.1.1: {}
    +
    +  yaml@2.8.3: {}
    +
    +  yargs-parser@13.1.2:
    +    dependencies:
    +      camelcase: 5.3.1
    +      decamelize: 1.2.0
    +
    +  yargs@13.3.2:
    +    dependencies:
    +      cliui: 5.0.0
    +      find-up: 3.0.0
    +      get-caller-file: 2.0.5
    +      require-directory: 2.1.1
    +      require-main-filename: 2.0.0
    +      set-blocking: 2.0.0
    +      string-width: 3.1.0
    +      which-module: 2.0.1
    +      y18n: 4.0.3
    +      yargs-parser: 13.1.2
    +
    +  yocto-queue@1.2.2: {}
    +
    +  zod@4.3.6: {}
    diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
    new file mode 100644
    index 00000000000..cbd183482ce
    --- /dev/null
    +++ b/pnpm-workspace.yaml
    @@ -0,0 +1,15 @@
    +packages:
    +  - 'packages/*'
    +  - 'test/**/*'
    +
    +catalog:
    +  '@playwright/test': ^1.52.0
    +  '@stencil/playwright': '~0.4.3'
    +  '@stencil/vitest': '^1.11.6'
    +  '@vitest/browser': ^4.1.1
    +  '@vitest/browser-playwright': ^4.1.2
    +  playwright: ^1.58.0
    +  tsdown: '^0.21.7'
    +  typescript: '>4.0.0'
    +  vitest: ^4.1.1
    +  'vitest-environment-stencil': '^1.11.6'
    diff --git a/screenshot/compare/assets/favicon.ico b/screenshot/compare/assets/favicon.ico
    deleted file mode 100644
    index 02bfbdb4a55..00000000000
    Binary files a/screenshot/compare/assets/favicon.ico and /dev/null differ
    diff --git a/screenshot/compare/assets/logo.png b/screenshot/compare/assets/logo.png
    deleted file mode 100644
    index f8723db54dd..00000000000
    Binary files a/screenshot/compare/assets/logo.png and /dev/null differ
    diff --git a/screenshot/compare/build/app.css b/screenshot/compare/build/app.css
    deleted file mode 100644
    index 11e8f9e22c7..00000000000
    --- a/screenshot/compare/build/app.css
    +++ /dev/null
    @@ -1 +0,0 @@
    -:root{--font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;--font-color:rgb(32, 32, 32);--background-color:#f7f8fb;--breadcrumb-color:#8e9bb2;--header-box-shadow:0px 1px 4px rgba(0, 12, 32, 0.12), 0px 1px 0px rgba(0,12,32,0.02);--screenshot-box-shadow:0px 0px 4px rgba(0, 0, 0, 0.08), 0px 1px 3px rgba(0,0,0,0.12);--screenshot-border-radius:4px;--analysis-data-font-size:12px;--analysis-data-color:rgb(111, 111, 111)}*{box-sizing:border-box}body{padding:0;margin:0;font-family:var(--font-family);background:var(--background-color);color:var(--font-color)}compare-table{display:table;width:100%;margin-top:84px;margin-bottom:12px}compare-tbody{display:table-row-group}compare-tbody compare-cell{padding-top:12px}compare-row{display:table-row}compare-row[hidden]{display:none}compare-cell{display:table-cell;vertical-align:top;padding:3px 10px}body{overflow:hidden;position:absolute;width:100%;height:100vh}screenshot-compare{position:absolute;display:block;top:0px;width:100%;height:100vh}compare-header{display:block;position:absolute;z-index:1;display:block;top:0;left:-100px;padding-left:100px;width:calc(100% + 200px);height:84px}compare-thead{position:relative;top:64px;z-index:1}.scroll-x{position:absolute;top:0;width:100%;height:100vh;overflow-x:scroll;overflow-y:hidden}.scroll-y{position:absolute;top:0;height:100vh;overflow-x:hidden;overflow-y:scroll;-webkit-overflow-scrolling:touch;will-change:scroll-position}
    \ No newline at end of file
    diff --git a/screenshot/compare/build/app.esm.js b/screenshot/compare/build/app.esm.js
    deleted file mode 100644
    index 2634e05083b..00000000000
    --- a/screenshot/compare/build/app.esm.js
    +++ /dev/null
    @@ -1 +0,0 @@
    -import{p as e,b as r}from"./p-fbbae598.js";e().then(e=>r([["p-5479268c",[[0,"screenshot-compare",{appSrcUrl:[1,"app-src-url"],imagesUrl:[1,"images-url"],buildsUrl:[1,"builds-url"],comparesUrl:[1,"compares-url"],jsonpUrl:[1,"jsonp-url"],match:[16],a:[1040],b:[1040],filter:[32],diffs:[32]},[[0,"filterChange","filterChange"],[0,"diffNavChange","diffNavChange"],[0,"compareLoaded","compareLoaded"]]]]],["p-2c298727",[[0,"screenshot-preview",{appSrcUrl:[1,"app-src-url"],imagesUrl:[1,"images-url"],buildsUrl:[1,"builds-url"],match:[16]}]]],["p-f0b99977",[[0,"context-consumer",{context:[16],renderer:[16],subscribe:[16],unsubscribe:[32]}]]],["p-6ba08604",[[1,"screenshot-lookup"]]],["p-ec2f13e0",[[0,"stencil-async-content",{documentLocation:[1,"document-location"],content:[32]}]]],["p-d1bf53f5",[[4,"stencil-route-link",{url:[1],urlMatch:[1,"url-match"],activeClass:[1,"active-class"],exact:[4],strict:[4],custom:[1],anchorClass:[1,"anchor-class"],anchorRole:[1,"anchor-role"],anchorTitle:[1,"anchor-title"],anchorTabIndex:[1,"anchor-tab-index"],anchorId:[1,"anchor-id"],history:[16],location:[16],root:[1],ariaHaspopup:[1,"aria-haspopup"],ariaPosinset:[1,"aria-posinset"],ariaSetsize:[2,"aria-setsize"],ariaLabel:[1,"aria-label"],match:[32]}]]],["p-b4cc611c",[[0,"stencil-route-title",{titleSuffix:[1,"title-suffix"],pageTitle:[1,"page-title"]}]]],["p-6bc63295",[[0,"stencil-router-prompt",{when:[4],message:[1],history:[16],unblock:[32]}]]],["p-e8ca6d97",[[0,"stencil-router-redirect",{history:[16],root:[1],url:[1]}]]],["p-573ec8a4",[[1,"compare-analysis",{aId:[1,"a-id"],bId:[1,"b-id"],diff:[16],mismatchedPixels:[2,"mismatched-pixels"]}]]],["p-f4745c2f",[[0,"compare-row",{aId:[1,"a-id"],bId:[1,"b-id"],imagesUrl:[1,"images-url"],jsonpUrl:[1,"jsonp-url"],diff:[16],show:[4],imageASrc:[32],imageBSrc:[32],imageAClass:[32],imageBClass:[32],canvasClass:[32]}],[1,"compare-thead",{a:[16],b:[16],diffs:[16]}]]],["p-227a1e18",[[0,"app-root"],[0,"stencil-route",{group:[513],componentUpdated:[16],match:[1040],url:[1],component:[1],componentProps:[16],exact:[4],routeRender:[16],scrollTopOffset:[2,"scroll-top-offset"],routeViewsUpdated:[16],location:[16],history:[16],historyType:[1,"history-type"]}],[4,"stencil-route-switch",{group:[513],scrollTopOffset:[2,"scroll-top-offset"],location:[16],routeViewsUpdated:[16]}],[4,"stencil-router",{root:[1],historyType:[1,"history-type"],titleSuffix:[1,"title-suffix"],scrollTopOffset:[2,"scroll-top-offset"],location:[32],history:[32]}]]],["p-7a3759fd",[[1,"compare-header",{appSrcUrl:[1,"app-src-url"],diffs:[16],filter:[16]}],[1,"compare-filter",{diffs:[16],filter:[16]}]]]],e));
    \ No newline at end of file
    diff --git a/screenshot/compare/build/app.js b/screenshot/compare/build/app.js
    deleted file mode 100644
    index 9219ff1cffe..00000000000
    --- a/screenshot/compare/build/app.js
    +++ /dev/null
    @@ -1,33 +0,0 @@
    -
    -(function() {
    -  function checkSupport() {
    -  if (!document.body) {
    -    setTimeout(checkSupport);
    -    return;
    -  }
    -  function supportsDynamicImports() {
    -    try {
    -    new Function('import("")');
    -    return true;
    -    } catch (e) {}
    -    return false;
    -  }
    -  var supportsEsModules = !!('noModule' in document.createElement('script'));
    -
    -  if (!supportsEsModules) {
    -    document.body.innerHTML = '\n  \n\n\n\n  

    This Stencil app is disabled for this browser.

    \n\n

    Developers:

    \n
      \n
    • ES5 builds are disabled during development to take advantage of 2x faster build times.
    • \n
    • Please see the example below or our config docs if you would like to develop on a browser that does not fully support ES2017 and custom elements.
    • \n
    • Note that by default, ES5 builds and polyfills are enabled during production builds.
    • \n
    • When testing browsers it is recommended to always test in production mode, and ES5 builds should always be enabled during production builds.
    • \n
    • This is only an experiment and if it slows down app development then we will revert this and enable ES5 builds during dev.
    • \n
    \n\n\n

    Enabling ES5 builds during development:

    \n
    \n  npm run dev --es5\n  
    \n

    For stencil-component-starter, use:

    \n
    \n  npm start --es5\n  
    \n\n\n

    Enabling full production builds during development:

    \n
    \n  npm run dev --prod\n  
    \n

    For stencil-component-starter, use:

    \n
    \n  npm start --prod\n  
    \n\n

    Current Browser\'s Support:

    \n \n\n

    Current Browser:

    \n
    \n  \n  
    \n'; - - document.getElementById('current-browser-output').textContent = window.navigator.userAgent; - document.getElementById('es-modules-test').textContent = supportsEsModules; - document.getElementById('es-dynamic-modules-test').textContent = supportsDynamicImports(); - document.getElementById('shadow-dom-test').textContent = !!(document.head.attachShadow); - document.getElementById('custom-elements-test').textContent = !!(window.customElements); - document.getElementById('css-variables-test').textContent = !!(window.CSS && window.CSS.supports && window.CSS.supports('color', 'var(--c)')); - document.getElementById('fetch-test').textContent = !!(window.fetch); - } else { - document.body.innerHTML = '\n \n\n\n\n

    Update src/index.html

    \n\n

    Stencil recently changed how scripts are loaded in order to improve performance.

    \n\n

    BEFORE:

    \n

    Previously, a single script was included that handled loading the correct JavaScript based on browser support.

    \n
    \n  <script src="/build/app.js"></script>\n\n  
    \n\n

    AFTER:

    \n

    The index.html should now include two scripts using the modern ES Module script pattern.\n Note that only one file will actually be requested and loaded based on the browser\'s native support for ES Modules.\n For more info, please see Using JavaScript modules on the web.\n

    \n
    \n  <script type="module" src="/build/app.esm.js"></script>\n  <script nomodule src="/build/app.js"></script>\n  
    \n'; - } - } - - setTimeout(checkSupport); -})(); \ No newline at end of file diff --git a/screenshot/compare/build/index.esm.js b/screenshot/compare/build/index.esm.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/screenshot/compare/build/p-081b0641.js b/screenshot/compare/build/p-081b0641.js deleted file mode 100644 index 71bc49ccfb8..00000000000 --- a/screenshot/compare/build/p-081b0641.js +++ /dev/null @@ -1 +0,0 @@ -function t(t,n,r){const s=o(t,n,r),c=localStorage.getItem(s);if("string"==typeof c){const t=parseInt(c,10);if(!isNaN(t))return t}return null}function n(t,n,r,s){const c=o(t,n,r);localStorage.setItem(c,String(s))}function o(t,n,o){return`screenshot_mismatch_${t}_${n}_${o}`}export{t as g,n as s} \ No newline at end of file diff --git a/screenshot/compare/build/p-227a1e18.entry.js b/screenshot/compare/build/p-227a1e18.entry.js deleted file mode 100644 index 0350010494a..00000000000 --- a/screenshot/compare/build/p-227a1e18.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as t,h as s,g as e,c as o}from"./p-fbbae598.js";import{A as i}from"./p-e2efe0df.js";import{m as n,a as r,s as a,b as h,c,d as l,e as u,f as p,g as d,h as f,i as g,j as y,k as m,l as b,n as w,o as P,p as v}from"./p-9b6a9315.js";class O{constructor(s){t(this,s)}render(){return s("stencil-router",{class:"full-screen"},s("stencil-route-switch",null,s("stencil-route",{url:"/:buildIdA/:buildIdB",exact:!0,component:"screenshot-compare",class:"full-screen"}),s("stencil-route",{url:"/:buildId",component:"screenshot-preview",class:"full-screen"}),s("stencil-route",{url:"/",component:"screenshot-preview",exact:!0,class:"full-screen"})))}}class j{constructor(s){t(this,s),this.group=null,this.match=null,this.componentProps={},this.exact=!1,this.scrollOnNextRender=!1,this.previousMatch=null}computeMatch(t){const s=null!=this.group||null!=this.el.parentElement&&"stencil-route-switch"===this.el.parentElement.tagName.toLowerCase();if(t&&!s)return this.previousMatch=this.match,this.match=n(t.pathname,{path:this.url,exact:this.exact,strict:!0})}async loadCompleted(){let t={};this.history&&this.history.location.hash?t={scrollToId:this.history.location.hash.substr(1)}:this.scrollTopOffset&&(t={scrollTopOffset:this.scrollTopOffset}),"function"==typeof this.componentUpdated?this.componentUpdated(t):this.match&&!r(this.match,this.previousMatch)&&this.routeViewsUpdated&&this.routeViewsUpdated(t)}async componentDidUpdate(){await this.loadCompleted()}async componentDidLoad(){await this.loadCompleted()}render(){if(!this.match||!this.history)return null;const t=Object.assign({},this.componentProps,{history:this.history,match:this.match});return this.routeRender?this.routeRender(Object.assign({},t,{component:this.component})):this.component?s(this.component,Object.assign({},t)):void 0}get el(){return e(this)}static get watchers(){return{location:["computeMatch"]}}}i.injectProps(j,["location","history","historyType","routeViewsUpdated"]),j.style="stencil-route.inactive{display:none}";const L=t=>"STENCIL-ROUTE"===t.tagName;class S{constructor(s){t(this,s),this.group=((1e17*Math.random()).toString().match(/.{4}/g)||[]).join("-"),this.subscribers=[],this.queue=o(this,"queue")}componentWillLoad(){null!=this.location&&this.regenerateSubscribers(this.location)}async regenerateSubscribers(t){if(null==t)return;let s=-1;if(this.subscribers=Array.prototype.slice.call(this.el.children).filter(L).map((e,o)=>{const i=n(t.pathname,{path:e.url,exact:e.exact,strict:!0});return i&&-1===s&&(s=o),{el:e,match:i}}),-1===s)return;if(this.activeIndex===s)return void(this.subscribers[s].el.match=this.subscribers[s].match);this.activeIndex=s;const e=this.subscribers[this.activeIndex];this.scrollTopOffset&&(e.el.scrollTopOffset=this.scrollTopOffset),e.el.group=this.group,e.el.match=e.match,e.el.componentUpdated=t=>{this.queue.write(()=>{this.subscribers.forEach((t,s)=>{if(t.el.componentUpdated=void 0,s===this.activeIndex)return t.el.style.display="";this.scrollTopOffset&&(t.el.scrollTopOffset=this.scrollTopOffset),t.el.group=this.group,t.el.match=null,t.el.style.display="none"})}),this.routeViewsUpdated&&this.routeViewsUpdated(Object.assign({scrollTopOffset:this.scrollTopOffset},t))}}render(){return s("slot",null)}get el(){return e(this)}static get watchers(){return{location:["regenerateSubscribers"]}}}i.injectProps(S,["location","routeViewsUpdated"]);const U=(t,...s)=>{t||console.warn(...s)},k=()=>{let t,s=[];return{setPrompt:s=>(U(null==t,"A history supports only one prompt at a time"),t=s,()=>{t===s&&(t=null)}),confirmTransitionTo:(s,e,o,i)=>{if(null!=t){const n="function"==typeof t?t(s,e):t;"string"==typeof n?"function"==typeof o?o(n,i):(U(!1,"A history needs a getUserConfirmation function in order to use a prompt message"),i(!0)):i(!1!==n)}else i(!0)},appendListener:t=>{let e=!0;const o=(...s)=>{e&&t(...s)};return s.push(o),()=>{e=!1,s=s.filter(t=>t!==o)}},notifyListeners:(...t)=>{s.forEach(s=>s(...t))}}},H=(t,s="scrollPositions")=>{let e=new Map;const o=(s,o)=>{if(e.set(s,o),a(t,"sessionStorage")){const s=[];e.forEach((t,e)=>{s.push([e,t])}),t.sessionStorage.setItem("scrollPositions",JSON.stringify(s))}};if(a(t,"sessionStorage")){const o=t.sessionStorage.getItem(s);e=o?new Map(JSON.parse(o)):e}return"scrollRestoration"in t.history&&(history.scrollRestoration="manual"),{set:o,get:t=>e.get(t),has:t=>e.has(t),capture:s=>{o(s,[t.scrollX,t.scrollY])}}},T={hashbang:{encodePath:t=>"!"===t.charAt(0)?t:"!/"+P(t),decodePath:t=>"!"===t.charAt(0)?t.substr(1):t},noslash:{encodePath:P,decodePath:u},slash:{encodePath:u,decodePath:u}},E=(t,s)=>{const e=0==t.pathname.indexOf(s)?"/"+t.pathname.slice(s.length):t.pathname;return Object.assign({},t,{pathname:e})},A={browser:(t,s={})=>{let e=!1;const o=t.history,i=t.location,n=t.navigator,r=h(t),a=!c(n),w=H(t),P=null!=s.forceRefresh&&s.forceRefresh,v=null!=s.getUserConfirmation?s.getUserConfirmation:m,O=null!=s.keyLength?s.keyLength:6,j=s.basename?l(u(s.basename)):"",L=()=>{try{return t.history.state||{}}catch(t){return{}}},S=t=>{t=t||{};const{key:s,state:e}=t,{pathname:o,search:n,hash:r}=i;let a=o+n+r;return U(!j||f(a,j),'You are attempting to use a basename on a page whose URL path does not begin with the basename. Expected path "'+a+'" to begin with "'+j+'".'),j&&(a=g(a,j)),p(a,e,s||d(O))},T=k(),E=t=>{w.capture(q.location.key),Object.assign(q,t),q.location.scrollPosition=w.get(q.location.key),q.length=o.length,T.notifyListeners(q.location,q.action)},A=t=>{b(n,t)||C(S(t.state))},x=()=>{C(S(L()))},C=t=>{if(e)e=!1,E();else{const s="POP";T.confirmTransitionTo(t,s,v,e=>{e?E({action:s,location:t}):R(t)})}},R=t=>{let s=B.indexOf(q.location.key),o=B.indexOf(t.key);-1===s&&(s=0),-1===o&&(o=0);const i=s-o;i&&(e=!0,N(i))},M=S(L());let B=[M.key],I=0,_=!1;const Y=t=>j+y(t),N=t=>{o.go(t)},V=s=>{I+=s,1===I?(t.addEventListener("popstate",A),a&&t.addEventListener("hashchange",x)):0===I&&(t.removeEventListener("popstate",A),a&&t.removeEventListener("hashchange",x))},q={length:o.length,action:"POP",location:M,createHref:Y,push:(t,s)=>{U(!("object"==typeof t&&void 0!==t.state&&void 0!==s),"You should avoid providing a 2nd state argument to push when the 1st argument is a location-like object that already has state; it is ignored");const e=p(t,s,d(O),q.location);T.confirmTransitionTo(e,"PUSH",v,t=>{if(!t)return;const s=Y(e),{key:n,state:a}=e;if(r)if(o.pushState({key:n,state:a},"",s),P)i.href=s;else{const t=B.indexOf(q.location.key),s=B.slice(0,-1===t?0:t+1);s.push(e.key),B=s,E({action:"PUSH",location:e})}else U(void 0===a,"Browser history cannot push state in browsers that do not support HTML5 history"),i.href=s})},replace:(t,s)=>{U(!("object"==typeof t&&void 0!==t.state&&void 0!==s),"You should avoid providing a 2nd state argument to replace when the 1st argument is a location-like object that already has state; it is ignored");const e=p(t,s,d(O),q.location);T.confirmTransitionTo(e,"REPLACE",v,t=>{if(!t)return;const s=Y(e),{key:n,state:a}=e;if(r)if(o.replaceState({key:n,state:a},"",s),P)i.replace(s);else{const t=B.indexOf(q.location.key);-1!==t&&(B[t]=e.key),E({action:"REPLACE",location:e})}else U(void 0===a,"Browser history cannot replace state in browsers that do not support HTML5 history"),i.replace(s)})},go:N,goBack:()=>N(-1),goForward:()=>N(1),block:(t="")=>{const s=T.setPrompt(t);return _||(V(1),_=!0),()=>(_&&(_=!1,V(-1)),s())},listen:t=>{const s=T.appendListener(t);return V(1),()=>{V(-1),s()}},win:t};return q},hash:(t,s={})=>{let e=!1,o=null,i=0,n=!1;const r=t.location,a=t.history,h=w(t.navigator),c=null!=s.keyLength?s.keyLength:6,{getUserConfirmation:b=m,hashType:P="slash"}=s,O=s.basename?l(u(s.basename)):"",{encodePath:j,decodePath:L}=T[P],S=()=>{const t=r.href,s=t.indexOf("#");return-1===s?"":t.substring(s+1)},H=t=>{const s=r.href.indexOf("#");r.replace(r.href.slice(0,s>=0?s:0)+"#"+t)},E=()=>{let t=L(S());return U(!O||f(t,O),'You are attempting to use a basename on a page whose URL path does not begin with the basename. Expected path "'+t+'" to begin with "'+O+'".'),O&&(t=g(t,O)),p(t,void 0,d(c))},A=k(),x=t=>{Object.assign(q,t),q.length=a.length,A.notifyListeners(q.location,q.action)},C=()=>{const t=S(),s=j(t);if(t!==s)H(s);else{const t=E(),s=q.location;if(!e&&v(s,t))return;if(o===y(t))return;o=null,R(t)}},R=t=>{if(e)e=!1,x();else{const s="POP";A.confirmTransitionTo(t,s,b,e=>{e?x({action:s,location:t}):M(t)})}},M=t=>{let s=Y.lastIndexOf(y(q.location)),o=Y.lastIndexOf(y(t));-1===s&&(s=0),-1===o&&(o=0);const i=s-o;i&&(e=!0,N(i))},B=S(),I=j(B);B!==I&&H(I);const _=E();let Y=[y(_)];const N=t=>{U(h,"Hash history go(n) causes a full page reload in this browser"),a.go(t)},V=(t,s)=>{i+=s,1===i?t.addEventListener("hashchange",C):0===i&&t.removeEventListener("hashchange",C)},q={length:a.length,action:"POP",location:_,createHref:t=>"#"+j(O+y(t)),push:(t,s)=>{U(void 0===s,"Hash history cannot push state; it is ignored");const e=p(t,void 0,d(c),q.location);A.confirmTransitionTo(e,"PUSH",b,t=>{if(!t)return;const s=y(e),i=j(O+s);if(S()!==i){o=s,(t=>{r.hash=t})(i);const t=Y.lastIndexOf(y(q.location)),n=Y.slice(0,-1===t?0:t+1);n.push(s),Y=n,x({action:"PUSH",location:e})}else U(!1,"Hash history cannot PUSH the same path; a new entry will not be added to the history stack"),x()})},replace:(t,s)=>{U(void 0===s,"Hash history cannot replace state; it is ignored");const e=p(t,void 0,d(c),q.location);A.confirmTransitionTo(e,"REPLACE",b,t=>{if(!t)return;const s=y(e),i=j(O+s);S()!==i&&(o=s,H(i));const n=Y.indexOf(y(q.location));-1!==n&&(Y[n]=s),x({action:"REPLACE",location:e})})},go:N,goBack:()=>N(-1),goForward:()=>N(1),block:(s="")=>{const e=A.setPrompt(s);return n||(V(t,1),n=!0),()=>(n&&(n=!1,V(t,-1)),e())},listen:s=>{const e=A.appendListener(s);return V(t,1),()=>{V(t,-1),e()}},win:t};return q}};class x{constructor(s){t(this,s),this.root="/",this.historyType="browser",this.titleSuffix="",this.routeViewsUpdated=(t={})=>{if(this.history&&t.scrollToId&&"browser"===this.historyType){const s=this.history.win.document.getElementById(t.scrollToId);if(s)return s.scrollIntoView()}this.scrollTo(t.scrollTopOffset||this.scrollTopOffset)},this.isServer=o(this,"isServer"),this.queue=o(this,"queue")}componentWillLoad(){this.history=A[this.historyType](this.el.ownerDocument.defaultView),this.history.listen(t=>{t=E(t,this.root),this.location=t}),this.location=E(this.history.location,this.root)}scrollTo(t){const s=this.history;if(null!=t&&!this.isServer&&s)return"POP"===s.action&&Array.isArray(s.location.scrollPosition)?this.queue.write(()=>{s&&s.location&&Array.isArray(s.location.scrollPosition)&&s.win.scrollTo(s.location.scrollPosition[0],s.location.scrollPosition[1])}):this.queue.write(()=>{s.win.scrollTo(0,t)})}render(){if(this.location&&this.history)return s(i.Provider,{state:{historyType:this.historyType,location:this.location,titleSuffix:this.titleSuffix,root:this.root,history:this.history,routeViewsUpdated:this.routeViewsUpdated}},s("slot",null))}get el(){return e(this)}}export{O as app_root,j as stencil_route,S as stencil_route_switch,x as stencil_router} \ No newline at end of file diff --git a/screenshot/compare/build/p-2c298727.entry.js b/screenshot/compare/build/p-2c298727.entry.js deleted file mode 100644 index 1dc161a7c04..00000000000 --- a/screenshot/compare/build/p-2c298727.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as e,h as t}from"./p-fbbae598.js";class s{constructor(t){e(this,t),this.appSrcUrl="",this.imagesUrl="/data/images/",this.buildsUrl="/data/builds/"}async componentWillLoad(){let e="master";this.match&&this.match.params.buildId&&(e=this.match.params.buildId.substr(0,7));let t=`${this.buildsUrl}${e}.json`;"master"===e&&(t+="?ts="+Date.now());const s=await fetch(t);s.ok&&(this.build=await s.json(),document.title=`${this.build.id} Preview: ${this.build.message}`)}render(){const e=[];return this.build&&this.build.screenshots.forEach(t=>{const s=t.testPath.split("/");s.pop();const i=`/data/tests/${this.build.id}/${s.join("/")}/`;if(!e.some(e=>e.url===i)){const s={desc:t.desc.split(",")[0],url:i};e.push(s)}}),e.sort((e,t)=>e.desc.toLowerCase()t.desc.toLowerCase()?1:0),[t("compare-header",{appSrcUrl:this.appSrcUrl}),t("section",{class:"scroll-y"},t("section",{class:"content"},this.build?t("h1",null,t("a",{href:this.build.url},this.build.message)):null,e.map(e=>t("div",null,t("a",{href:e.url},e.desc)))))]}}s.style="screenshot-preview{display:block}screenshot-preview .scroll-y{width:100%}screenshot-preview h1{color:var(--analysis-data-color);font-size:16px;margin:0}screenshot-preview .content{padding:80px 20px 140px 20px}screenshot-preview a{display:block;padding:8px;color:var(--analysis-data-color);text-decoration:none}screenshot-preview a:hover{text-decoration:underline}screenshot-preview compare-header{left:0;padding:0;width:100%;width:100%;height:auto;padding-top:env(safe-area-inset-top)}screenshot-preview compare-header compare-filter{display:none}@media (max-width: 480px){screenshot-preview a{padding:12px;font-size:18px}screenshot-preview a:hover{text-decoration:none}}";export{s as screenshot_preview} \ No newline at end of file diff --git a/screenshot/compare/build/p-5479268c.entry.js b/screenshot/compare/build/p-5479268c.entry.js deleted file mode 100644 index b9d1293e981..00000000000 --- a/screenshot/compare/build/p-5479268c.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as t,h as s}from"./p-fbbae598.js";import{g as i}from"./p-081b0641.js";function e(t,s){const i=Object.assign({},t,s),e=Object.keys(i),o=[];return e.map(t=>{const s=i[t];!0===s?o.push(t):null!=s&&""!==s&&o.push(t+"-"+s)}),window.location.hash=o.sort().join(";"),i}class o{constructor(s){t(this,s),this.appSrcUrl="",this.imagesUrl="/data/images/",this.buildsUrl="/data/builds/",this.comparesUrl="/data/compares/",this.jsonpUrl=null,this.diffs=[]}async componentWillLoad(){this.match&&this.match.params.buildIdA&&this.match.params.buildIdB&&await this.loadBuilds(this.match.params.buildIdA,this.match.params.buildIdB),this.diffs=await function(t,s,e){const o=[];return s&&e?(e.screenshots.forEach(s=>{o.push({id:s.id,desc:s.desc,testPath:s.testPath,imageA:null,imageUrlA:null,imageB:s.image,imageUrlB:`${t}${s.image}`,identical:!1,comparable:!1,mismatchedPixels:null,width:s.width,height:s.height,deviceScaleFactor:s.deviceScaleFactor,device:s.device||s.userAgent,show:!1,hasIntersected:!1,threshold:"number"==typeof s.threshold?s.threshold:.05})}),s.screenshots.forEach(s=>{const i=o.find(t=>t.id===s.id);i&&(i.imageA=s.image,i.imageUrlA=`${t}${s.image}`)}),o.forEach(t=>{if(t.comparable=null!=t.imageA&&null!=t.imageB,t.identical=t.comparable&&t.imageA===t.imageB,t.identical)t.mismatchedPixels=0;else{const s=i(t.imageA,t.imageB,t.threshold);"number"==typeof s&&(t.mismatchedPixels=s,0===t.mismatchedPixels&&(t.identical=!0))}}),o):o}(this.imagesUrl,this.a,this.b),this.filter=function(){const t={},s=location.hash.replace("#","");return""!==s&&s.split(";").forEach(s=>{const i=s.split("-");t[i[0]]=!(i.length>1)||i[1]}),t}(),this.updateDiffs()}componentDidLoad(){if("IntersectionObserver"in window){const t={root:document.querySelector(".scroll-y"),rootMargin:"1200px"},s=new IntersectionObserver(t=>{let s=!1;t.forEach(t=>{if(t.isIntersecting){const i=this.diffs.find(s=>t.target.id==="d-"+s.id);i&&(i.hasIntersected=!0,s=!0)}}),s&&(window.requestIdleCallback?window.requestIdleCallback(()=>{this.updateDiffs()}):window.requestAnimationFrame(()=>{this.updateDiffs()}))},t),i=document.querySelectorAll("compare-row");for(let t=0;t{t.hasIntersected=!0}),this.updateDiffs();this.filter&&this.filter.diff&&this.navToDiff(this.filter.diff)}async loadBuilds(t,s){let i=`${this.buildsUrl}${t}.json`;"master"===t&&(i+="?ts="+Date.now());let e=`${this.buildsUrl}${s}.json`;"master"===s&&(e+="?ts="+Date.now());const o=await Promise.all([fetch(i),fetch(e)]),n=await o[0],a=await o[1];n.ok&&a.ok&&(this.a=await n.json(),this.b=await a.json())}filterChange(t){this.filter=e(this.filter,t.detail),this.updateDiffs()}diffNavChange(t){const s=t.detail;this.filter=e(this.filter,{diff:s}),this.updateDiffs(),this.navToDiff(s)}navToDiff(t){const s=document.getElementById("d-"+t),i=document.querySelector(".scroll-y");s&&i&&(i.scrollTop=s.offsetTop-84)}compareLoaded(t){const s=t.detail,i=this.diffs.find(t=>t.id===s.id);i&&(i.mismatchedPixels=s.mismatchedPixels),this.updateDiffs()}updateDiffs(){var t;this.diffs=(t=this.filter,this.diffs.map(s=>(s=Object.assign({},s),function(t,s){const i=!t.device||t.device===s.device,e=!t.search||s.desc.includes(t.search);let o=!0;return t.diff&&t.diff===s.id?o=!0:t.mismatch?null!=s.mismatchedPixels&&"all"!==t.mismatch&&(o=parseInt(t.mismatch,10)0||null==s.mismatchedPixels,s.show=i&&e&&o,s}(t,s))).sort((t,s)=>t.mismatchedPixels>s.mismatchedPixels?-1:t.mismatchedPixelss.desc.toLowerCase()?1:t.device.toLowerCase()s.device.toLowerCase()?1:0))}render(){return[s("compare-header",{diffs:this.diffs,filter:this.filter,appSrcUrl:this.appSrcUrl}),s("section",{class:"scroll-x"},s("compare-thead",{a:this.a,b:this.b,diffs:this.diffs}),s("section",{class:"scroll-y"},s("compare-table",null,s("compare-tbody",null,this.diffs.map(t=>s("compare-row",{key:t.id,aId:this.a.id,bId:this.b.id,id:"d-"+t.id,show:t.show,hidden:!t.show,imagesUrl:this.imagesUrl,jsonpUrl:this.jsonpUrl,diff:t}))))))]}}export{o as screenshot_compare} \ No newline at end of file diff --git a/screenshot/compare/build/p-573ec8a4.entry.js b/screenshot/compare/build/p-573ec8a4.entry.js deleted file mode 100644 index 90df39abe97..00000000000 --- a/screenshot/compare/build/p-573ec8a4.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as l,d as t,h as d}from"./p-fbbae598.js";class a{constructor(d){l(this,d),this.mismatchedPixels=null,this.diffNavChange=t(this,"diffNavChange",7)}navToDiff(l){l.preventDefault(),l.stopPropagation(),this.diffNavChange.emit(this.diff.id)}render(){const l=this.diff,t="number"==typeof this.mismatchedPixels,a=t?this.mismatchedPixels/(l.width*l.deviceScaleFactor*(l.height*l.deviceScaleFactor)):null;let i="";t?this.mismatchedPixels>0&&(i="has-mismatch"):i="not-calculated";const n=l.testPath.split("/");n.pop();const s=n.join("/");return[d("p",{class:"test-path"},l.testPath),d("dl",null,d("div",null,d("dt",null,"Diff"),d("dd",null,d("a",{href:"#diff-"+l.id,onClick:this.navToDiff.bind(this)},l.id))),l.comparable?[d("div",{class:i},d("dt",null,"Mismatched Pixels"),d("dd",null,t?this.mismatchedPixels:"--")),d("div",{class:i},d("dt",null,"Mismatched Ratio"),d("dd",null,t?a.toFixed(4):"--"))]:null,d("div",null,d("dt",null,"Device"),d("dd",null,l.device)),d("div",null,d("dt",null,"Width"),d("dd",null,l.width)),d("div",null,d("dt",null,"Height"),d("dd",null,l.height)),d("div",null,d("dt",null,"Device Scale Factor"),d("dd",null,l.deviceScaleFactor)),l.imageA?d("div",null,d("dt",null,"Left Preview"),d("dd",null,d("a",{href:`/data/tests/${this.aId}/${s}/`,target:"_blank"},"HTML"))):null,l.imageB?d("div",null,d("dt",null,"Right Preview"),d("dd",null,d("a",{href:`/data/tests/${this.bId}/${s}/`,target:"_blank"},"HTML"))):null,d("div",{class:"desc"},d("dt",null,"Description"),d("dd",null,l.desc)))]}}a.style=".test-path{margin-top:0;padding-top:0;font-size:10px;color:var(--analysis-data-color)}dl{padding:0;margin:0;font-size:var(--analysis-data-font-size);line-height:28px}div{display:flex;width:260px}dt{display:inline;flex:2;font-weight:500}dd{display:inline;flex:1;color:var(--analysis-data-color)}.desc,.desc dt{display:block}.desc dd{display:block;margin:0;line-height:22px}.not-calculated dd{color:#cccccc}.has-mismatch dd{color:#ff6200}p{padding-top:14px;font-size:var(--analysis-data-font-size)}a{color:var(--analysis-data-color)}a:hover{text-decoration:none}";export{a as compare_analysis} \ No newline at end of file diff --git a/screenshot/compare/build/p-6ba08604.entry.js b/screenshot/compare/build/p-6ba08604.entry.js deleted file mode 100644 index 2c483ba2815..00000000000 --- a/screenshot/compare/build/p-6ba08604.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as t,h as i}from"./p-fbbae598.js";class o{constructor(i){t(this,i),this.a="",this.b=""}async componentWillLoad(){const t="/data/builds/master.json?ts="+Date.now(),i=await fetch(t);i.ok&&(this.build=await i.json())}onSubmit(t){t.preventDefault(),t.stopPropagation();let i=this.a.trim().toLowerCase(),o=this.b.trim().toLowerCase();i&&o&&(i=i.substring(0,7),o=o.substring(0,7),window.location.href=`/${i}/${o}`)}render(){return[i("header",null,i("div",{class:"logo"},i("a",{href:"/"},i("img",{src:"/assets/logo.png?1"})))),i("section",null,this.build?i("section",{class:"master"},i("p",null,i("a",{href:"/master"},this.build.message))):null,i("form",{onSubmit:this.onSubmit.bind(this)},i("div",null,i("input",{onInput:t=>this.a=t.target.value})),i("div",null,i("input",{onInput:t=>this.b=t.target.value})),i("div",null,i("button",{type:"submit"},"Compare Screenshots"))))]}}o.style="header{padding:8px;background:white;box-shadow:var(--header-box-shadow)}img{width:174px;height:32px}.logo{flex:1;padding:7px}a{padding:8px;color:var(--analysis-data-color);text-decoration:none}.master{text-align:center}a:hover{text-decoration:underline}form{width:160px;margin:40px auto}form div{margin:10px}input{width:100%}";export{o as screenshot_lookup} \ No newline at end of file diff --git a/screenshot/compare/build/p-6bc63295.entry.js b/screenshot/compare/build/p-6bc63295.entry.js deleted file mode 100644 index c2f9760289c..00000000000 --- a/screenshot/compare/build/p-6bc63295.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as s,g as t}from"./p-fbbae598.js";import{A as e}from"./p-e2efe0df.js";class i{constructor(t){s(this,t),this.when=!0,this.message=""}enable(s){this.unblock&&this.unblock(),this.history&&(this.unblock=this.history.block(s))}disable(){this.unblock&&(this.unblock(),this.unblock=void 0)}componentWillLoad(){this.when&&this.enable(this.message)}updateMessage(s,t){this.when?this.when&&t===s||this.enable(this.message):this.disable()}componentDidUnload(){this.disable()}render(){return null}get el(){return t(this)}static get watchers(){return{message:["updateMessage"],when:["updateMessage"]}}}e.injectProps(i,["history"]);export{i as stencil_router_prompt} \ No newline at end of file diff --git a/screenshot/compare/build/p-7a3759fd.entry.js b/screenshot/compare/build/p-7a3759fd.entry.js deleted file mode 100644 index bdc329562c3..00000000000 --- a/screenshot/compare/build/p-7a3759fd.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as e,d as t,h as i}from"./p-fbbae598.js";class s{constructor(i){e(this,i),this.filterChange=t(this,"filterChange",7)}render(){if(!this.diffs||0===this.diffs.length||!this.filter)return;const e=this.diffs.reduce((e,t)=>(e.some(e=>e.value===t.device)||e.push({text:t.device,value:t.device}),e),[{text:"All Devices",value:""}]);return i("section",null,i("div",{class:"showing"},"Showing ",this.diffs.filter(e=>e.show).length),i("div",{class:"search"},i("input",{type:"search",onInput:e=>{this.filterChange.emit({search:e.target.value})},value:this.filter.search||""})),e.length>1?i("div",{class:"device"},i("select",{onInput:e=>{this.filterChange.emit({device:e.target.value})}},e.map(e=>i("option",{key:e.value,selected:e.value===this.filter.device,value:e.value},e.text)))):null,i("div",{class:"mismatch"},i("select",{onInput:e=>{this.filterChange.emit({mismatch:e.target.value})}},i("option",{value:"",selected:""===this.filter.mismatch},"> 0"),i("option",{value:"100",selected:"100"===this.filter.mismatch},"> 100"),i("option",{value:"250",selected:"250"===this.filter.mismatch},"> 250"),i("option",{value:"500",selected:"500"===this.filter.mismatch},"> 500"),i("option",{value:"1000",selected:"1000"===this.filter.mismatch},"> 1,000"),i("option",{value:"2500",selected:"2500"===this.filter.mismatch},"> 2,500"),i("option",{value:"5000",selected:"5000"===this.filter.mismatch},"> 5,000"),i("option",{value:"10000",selected:"10000"===this.filter.mismatch},"> 10,000"),i("option",{value:"25000",selected:"25000"===this.filter.mismatch},"> 25,000"),i("option",{value:"50000",selected:"50000"===this.filter.mismatch},"> 50,000"),i("option",{value:"all",selected:"all"===this.filter.mismatch},"All Screenshots"))))}}s.style="select{font-size:10px}input{font-size:10px}.showing{font-size:12px;white-space:nowrap;margin:17px 8px 0 0;color:var(--analysis-data-color)}section{display:flex;justify-content:flex-end}.search{margin:13px 8px 0 0}.device{margin:13px 8px 0 0}.mismatch{margin:13px 8px 0 0}";class a{constructor(t){e(this,t)}render(){return[i("header",null,i("div",{class:"logo"},i("a",{href:"/"},i("img",{src:this.appSrcUrl+"/assets/logo.png?1"}))),i("compare-filter",{diffs:this.diffs,filter:this.filter}))]}}a.style=":host{background:white;box-shadow:var(--header-box-shadow)}nav{padding:4px 4px}nav a{font-size:14px;text-decoration:none;color:var(--breadcrumb-color);display:inline-block;padding:0 4px 0 4px}nav a:hover{text-decoration:underline}header{display:flex;width:calc(100% - 115px);padding:8px}img{width:174px;height:32px}.logo{flex:1;padding:7px}compare-filter{flex:1}h1{margin:0;padding:0;font-size:18px}";export{s as compare_filter,a as compare_header} \ No newline at end of file diff --git a/screenshot/compare/build/p-7b4e3ba7.js b/screenshot/compare/build/p-7b4e3ba7.js deleted file mode 100644 index 2634e05083b..00000000000 --- a/screenshot/compare/build/p-7b4e3ba7.js +++ /dev/null @@ -1 +0,0 @@ -import{p as e,b as r}from"./p-fbbae598.js";e().then(e=>r([["p-5479268c",[[0,"screenshot-compare",{appSrcUrl:[1,"app-src-url"],imagesUrl:[1,"images-url"],buildsUrl:[1,"builds-url"],comparesUrl:[1,"compares-url"],jsonpUrl:[1,"jsonp-url"],match:[16],a:[1040],b:[1040],filter:[32],diffs:[32]},[[0,"filterChange","filterChange"],[0,"diffNavChange","diffNavChange"],[0,"compareLoaded","compareLoaded"]]]]],["p-2c298727",[[0,"screenshot-preview",{appSrcUrl:[1,"app-src-url"],imagesUrl:[1,"images-url"],buildsUrl:[1,"builds-url"],match:[16]}]]],["p-f0b99977",[[0,"context-consumer",{context:[16],renderer:[16],subscribe:[16],unsubscribe:[32]}]]],["p-6ba08604",[[1,"screenshot-lookup"]]],["p-ec2f13e0",[[0,"stencil-async-content",{documentLocation:[1,"document-location"],content:[32]}]]],["p-d1bf53f5",[[4,"stencil-route-link",{url:[1],urlMatch:[1,"url-match"],activeClass:[1,"active-class"],exact:[4],strict:[4],custom:[1],anchorClass:[1,"anchor-class"],anchorRole:[1,"anchor-role"],anchorTitle:[1,"anchor-title"],anchorTabIndex:[1,"anchor-tab-index"],anchorId:[1,"anchor-id"],history:[16],location:[16],root:[1],ariaHaspopup:[1,"aria-haspopup"],ariaPosinset:[1,"aria-posinset"],ariaSetsize:[2,"aria-setsize"],ariaLabel:[1,"aria-label"],match:[32]}]]],["p-b4cc611c",[[0,"stencil-route-title",{titleSuffix:[1,"title-suffix"],pageTitle:[1,"page-title"]}]]],["p-6bc63295",[[0,"stencil-router-prompt",{when:[4],message:[1],history:[16],unblock:[32]}]]],["p-e8ca6d97",[[0,"stencil-router-redirect",{history:[16],root:[1],url:[1]}]]],["p-573ec8a4",[[1,"compare-analysis",{aId:[1,"a-id"],bId:[1,"b-id"],diff:[16],mismatchedPixels:[2,"mismatched-pixels"]}]]],["p-f4745c2f",[[0,"compare-row",{aId:[1,"a-id"],bId:[1,"b-id"],imagesUrl:[1,"images-url"],jsonpUrl:[1,"jsonp-url"],diff:[16],show:[4],imageASrc:[32],imageBSrc:[32],imageAClass:[32],imageBClass:[32],canvasClass:[32]}],[1,"compare-thead",{a:[16],b:[16],diffs:[16]}]]],["p-227a1e18",[[0,"app-root"],[0,"stencil-route",{group:[513],componentUpdated:[16],match:[1040],url:[1],component:[1],componentProps:[16],exact:[4],routeRender:[16],scrollTopOffset:[2,"scroll-top-offset"],routeViewsUpdated:[16],location:[16],history:[16],historyType:[1,"history-type"]}],[4,"stencil-route-switch",{group:[513],scrollTopOffset:[2,"scroll-top-offset"],location:[16],routeViewsUpdated:[16]}],[4,"stencil-router",{root:[1],historyType:[1,"history-type"],titleSuffix:[1,"title-suffix"],scrollTopOffset:[2,"scroll-top-offset"],location:[32],history:[32]}]]],["p-7a3759fd",[[1,"compare-header",{appSrcUrl:[1,"app-src-url"],diffs:[16],filter:[16]}],[1,"compare-filter",{diffs:[16],filter:[16]}]]]],e)); \ No newline at end of file diff --git a/screenshot/compare/build/p-988eb362.css b/screenshot/compare/build/p-988eb362.css deleted file mode 100644 index 11e8f9e22c7..00000000000 --- a/screenshot/compare/build/p-988eb362.css +++ /dev/null @@ -1 +0,0 @@ -:root{--font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;--font-color:rgb(32, 32, 32);--background-color:#f7f8fb;--breadcrumb-color:#8e9bb2;--header-box-shadow:0px 1px 4px rgba(0, 12, 32, 0.12), 0px 1px 0px rgba(0,12,32,0.02);--screenshot-box-shadow:0px 0px 4px rgba(0, 0, 0, 0.08), 0px 1px 3px rgba(0,0,0,0.12);--screenshot-border-radius:4px;--analysis-data-font-size:12px;--analysis-data-color:rgb(111, 111, 111)}*{box-sizing:border-box}body{padding:0;margin:0;font-family:var(--font-family);background:var(--background-color);color:var(--font-color)}compare-table{display:table;width:100%;margin-top:84px;margin-bottom:12px}compare-tbody{display:table-row-group}compare-tbody compare-cell{padding-top:12px}compare-row{display:table-row}compare-row[hidden]{display:none}compare-cell{display:table-cell;vertical-align:top;padding:3px 10px}body{overflow:hidden;position:absolute;width:100%;height:100vh}screenshot-compare{position:absolute;display:block;top:0px;width:100%;height:100vh}compare-header{display:block;position:absolute;z-index:1;display:block;top:0;left:-100px;padding-left:100px;width:calc(100% + 200px);height:84px}compare-thead{position:relative;top:64px;z-index:1}.scroll-x{position:absolute;top:0;width:100%;height:100vh;overflow-x:scroll;overflow-y:hidden}.scroll-y{position:absolute;top:0;height:100vh;overflow-x:hidden;overflow-y:scroll;-webkit-overflow-scrolling:touch;will-change:scroll-position} \ No newline at end of file diff --git a/screenshot/compare/build/p-9b6a9315.js b/screenshot/compare/build/p-9b6a9315.js deleted file mode 100644 index 8c0457b5ea4..00000000000 --- a/screenshot/compare/build/p-9b6a9315.js +++ /dev/null @@ -1 +0,0 @@ -const r=new RegExp(["(\\\\.)","(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?"].join("|"),"g"),e=r=>r.replace(/([.+*?=^!:${}()[\]|/\\])/g,"\\$1"),t=r=>r.replace(/([=!:$/()])/g,"\\$1"),n=r=>r&&r.sensitive?"":"i",a=(r,t,a)=>{for(var o=(a=a||{}).strict,s=!1!==a.end,i=e(a.delimiter||"/"),l=a.delimiters||"./",c=[].concat(a.endsWith||[]).map(e).concat("$").join("|"),u="",f=!1,p=0;p-1;else{var h=e(d.prefix||""),v=d.repeat?"(?:"+d.pattern+")(?:"+h+"(?:"+d.pattern+"))*":d.pattern;t&&t.push(d),u+=d.optional?d.partial?h+"("+v+")?":"(?:"+h+"("+v+"))?":h+"("+v+")"}}return s?(o||(u+="(?:"+i+")?"),u+="$"===c?"$":"(?="+c+")"):(o||(u+="(?:"+i+"(?="+c+"))?"),f||(u+="(?="+i+"|"+c+")")),new RegExp("^"+u,n(a))},o=(s,i,l)=>s instanceof RegExp?((r,e)=>{if(!e)return r;var t=r.source.match(/\((?!\?)/g);if(t)for(var n=0;n{for(var a=[],s=0;sa(((n,a)=>{for(var o,s=[],i=0,l=0,c="",u=a&&a.delimiter||"/",f=a&&a.delimiters||"./",p=!1;null!==(o=r.exec(n));){var d=o[0],h=o[1],v=o.index;if(c+=n.slice(l,v),l=v+d.length,h)c+=h[1],p=!0;else{var g="",y=n[l],E=o[2],x=o[3],R=o[4],m=o[5];if(!p&&c.length){var $=c.length-1;f.indexOf(c[$])>-1&&(g=c[$],c=c.slice(0,$))}c&&(s.push(c),c="",p=!1);var O=g||u,_=x||R;s.push({name:E||i++,prefix:g,delimiter:O,optional:"?"===m||"*"===m,repeat:"+"===m||"*"===m,partial:""!==g&&void 0!==y&&y!==g,pattern:_?t(_):"[^"+e(O)+"]+?"})}}return(c||lnew RegExp("^"+e+"(\\/|\\?|#|$)","i").test(r),i=(r,e)=>s(r,e)?r.substr(e.length):r,l=r=>"/"===r.charAt(r.length-1)?r.slice(0,-1):r,c=r=>"/"===r.charAt(0)?r:"/"+r,u=r=>"/"===r.charAt(0)?r.substr(1):r,f=r=>{const{pathname:e,search:t,hash:n}=r;let a=e||"/";return t&&"?"!==t&&(a+="?"===t.charAt(0)?t:"?"+t),n&&"#"!==n&&(a+="#"===n.charAt(0)?n:"#"+n),a},p=r=>"/"===r.charAt(0),d=r=>Math.random().toString(36).substr(2,r),h=(r,e)=>{for(let t=e,n=t+1,a=r.length;n{if(r===e)return!0;if(null==r||null==e)return!1;if(Array.isArray(r))return Array.isArray(e)&&r.length===e.length&&r.every((r,t)=>v(r,e[t]));const t=typeof r;if(t!==typeof e)return!1;if("object"===t){const t=r.valueOf(),n=e.valueOf();if(t!==r||n!==e)return v(t,n);const a=Object.keys(r),o=Object.keys(e);return a.length===o.length&&a.every(t=>v(r[t],e[t]))}return!1},g=(r,e)=>r.pathname===e.pathname&&r.search===e.search&&r.hash===e.hash&&r.key===e.key&&v(r.state,e.state),y=(r,e,t,n)=>{let a;"string"==typeof r?(a=(r=>{let e=r||"/",t="",n="";const a=e.indexOf("#");-1!==a&&(n=e.substr(a),e=e.substr(0,a));const o=e.indexOf("?");return-1!==o&&(t=e.substr(o),e=e.substr(0,o)),{pathname:e,search:"?"===t?"":t,hash:"#"===n?"":n,query:{},key:""}})(r),void 0!==e&&(a.state=e)):(a=Object.assign({pathname:""},r),a.search&&"?"!==a.search.charAt(0)&&(a.search="?"+a.search),a.hash&&"#"!==a.hash.charAt(0)&&(a.hash="#"+a.hash),void 0!==e&&void 0===a.state&&(a.state=e));try{a.pathname=decodeURI(a.pathname)}catch(r){throw r instanceof URIError?new URIError('Pathname "'+a.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):r}var o;return a.key=t,n?a.pathname?"/"!==a.pathname.charAt(0)&&(a.pathname=((r,e="")=>{let t,n=e&&e.split("/")||[],a=0;const o=r&&r.split("/")||[],s=r&&p(r),i=e&&p(e),l=s||i;if(r&&p(r)?n=o:o.length&&(n.pop(),n=n.concat(o)),!n.length)return"/";if(n.length){const r=n[n.length-1];t="."===r||".."===r||""===r}else t=!1;for(let r=n.length;r>=0;r--){const e=n[r];"."===e?h(n,r):".."===e?(h(n,r),a++):a&&(h(n,r),a--)}if(!l)for(;a--;a)n.unshift("..");!l||""===n[0]||n[0]&&p(n[0])||n.unshift("");let c=n.join("/");return t&&"/"!==c.substr(-1)&&(c+="/"),c})(a.pathname,n.pathname)):a.pathname=n.pathname:a.pathname||(a.pathname="/"),a.query=(o=a.search||"")?(/^[?#]/.test(o)?o.slice(1):o).split("&").reduce((r,e)=>{let[t,n]=e.split("=");return r[t]=n?decodeURIComponent(n.replace(/\+/g," ")):"",r},{}):{},a};let E=0;const x={},R=(r,e={})=>{"string"==typeof e&&(e={path:e});const{path:t="/",exact:n=!1,strict:a=!1}=e,{re:s,keys:i}=((r,e)=>{const t=`${e.end}${e.strict}`,n=x[t]||(x[t]={}),a=JSON.stringify(r);if(n[a])return n[a];const s=[],i={re:o(r,s,e),keys:s};return E<1e4&&(n[a]=i,E+=1),i})(t,{end:n,strict:a}),l=s.exec(r);if(!l)return null;const[c,...u]=l,f=r===c;return n&&!f?null:{path:t,url:"/"===t&&""===c?"/":c,isExact:f,params:i.reduce((r,e,t)=>(r[e.name]=u[t],r),{})}},m=(r,e)=>null==r&&null==e||null!=e&&r&&e&&r.path===e.path&&r.url===e.url&&v(r.params,e.params),$=(r,e,t)=>t(r.confirm(e)),O=r=>r.metaKey||r.altKey||r.ctrlKey||r.shiftKey,_=r=>{const e=r.navigator.userAgent;return(-1===e.indexOf("Android 2.")&&-1===e.indexOf("Android 4.0")||-1===e.indexOf("Mobile Safari")||-1!==e.indexOf("Chrome")||-1!==e.indexOf("Windows Phone"))&&r.history&&"pushState"in r.history},b=r=>-1===r.userAgent.indexOf("Trident"),w=r=>-1===r.userAgent.indexOf("Firefox"),A=(r,e)=>void 0===e.state&&-1===r.userAgent.indexOf("CriOS"),j=(r,e)=>{const t=r[e],n="__storage_test__";try{return t.setItem(n,n),t.removeItem(n),!0}catch(r){return r instanceof DOMException&&(22===r.code||1014===r.code||"QuotaExceededError"===r.name||"NS_ERROR_DOM_QUOTA_REACHED"===r.name)&&0!==t.length}};export{m as a,_ as b,b as c,l as d,c as e,y as f,d as g,s as h,i,f as j,$ as k,A as l,R as m,w as n,u as o,g as p,O as q,j as s} \ No newline at end of file diff --git a/screenshot/compare/build/p-b4cc611c.entry.js b/screenshot/compare/build/p-b4cc611c.entry.js deleted file mode 100644 index 89f30e16ef5..00000000000 --- a/screenshot/compare/build/p-b4cc611c.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as t,g as e}from"./p-fbbae598.js";import{A as s}from"./p-e2efe0df.js";class i{constructor(e){t(this,e),this.titleSuffix="",this.pageTitle=""}updateDocumentTitle(){const t=this.el;t.ownerDocument&&(t.ownerDocument.title=`${this.pageTitle}${this.titleSuffix||""}`)}componentWillLoad(){this.updateDocumentTitle()}get el(){return e(this)}static get watchers(){return{pageTitle:["updateDocumentTitle"]}}}s.injectProps(i,["titleSuffix"]);export{i as stencil_route_title} \ No newline at end of file diff --git a/screenshot/compare/build/p-d1bf53f5.entry.js b/screenshot/compare/build/p-d1bf53f5.entry.js deleted file mode 100644 index 3401a9d11e2..00000000000 --- a/screenshot/compare/build/p-d1bf53f5.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as t,h as i,g as s}from"./p-fbbae598.js";import{A as h}from"./p-e2efe0df.js";import{m as a,q as e}from"./p-9b6a9315.js";class r{constructor(i){t(this,i),this.unsubscribe=()=>{},this.activeClass="link-active",this.exact=!1,this.strict=!0,this.custom="a",this.match=null}componentWillLoad(){this.computeMatch()}computeMatch(){this.location&&(this.match=a(this.location.pathname,{path:this.urlMatch||this.url,exact:this.exact,strict:this.strict}))}handleClick(t){var i,s;if(!e(t)&&this.history&&this.url&&this.root)return t.preventDefault(),this.history.push((s=this.root,"/"==(i=this.url).charAt(0)&&"/"==s.charAt(s.length-1)?s.slice(0,s.length-1)+i:s+i))}render(){let t={class:{[this.activeClass]:null!==this.match},onClick:this.handleClick.bind(this)};return this.anchorClass&&(t.class[this.anchorClass]=!0),"a"===this.custom&&(t=Object.assign({},t,{href:this.url,title:this.anchorTitle,role:this.anchorRole,tabindex:this.anchorTabIndex,"aria-haspopup":this.ariaHaspopup,id:this.anchorId,"aria-posinset":this.ariaPosinset,"aria-setsize":this.ariaSetsize,"aria-label":this.ariaLabel})),i(this.custom,Object.assign({},t),i("slot",null))}get el(){return s(this)}static get watchers(){return{location:["computeMatch"]}}}h.injectProps(r,["history","location","root"]);export{r as stencil_route_link} \ No newline at end of file diff --git a/screenshot/compare/build/p-e2efe0df.js b/screenshot/compare/build/p-e2efe0df.js deleted file mode 100644 index 34fcb6f7fe4..00000000000 --- a/screenshot/compare/build/p-e2efe0df.js +++ /dev/null @@ -1 +0,0 @@ -import{h as t}from"./p-fbbae598.js";const e=(()=>{let e=new Map,r={historyType:"browser",location:{pathname:"",query:{},key:""},titleSuffix:"",root:"/",routeViewsUpdated:()=>{}};const o=(t,e)=>{Array.isArray(t)?[...t].forEach(t=>{e[t]=r[t]}):e[t]=Object.assign({},r)},s=(t,r)=>(e.has(t)||(e.set(t,r),o(r,t)),()=>{e.has(t)&&e.delete(t)});return{Provider:({state:t},s)=>(r=t,e.forEach(o),s),Consumer:(e,r)=>((e,r)=>t("context-consumer",{subscribe:e,renderer:r}))(s,r[0]),injectProps:(t,r)=>{const o=t.prototype,n=o.connectedCallback,i=o.disconnectedCallback;o.connectedCallback=function(){if(s(this,r),n)return n.call(this)},o.disconnectedCallback=function(){e.delete(this),i&&i.call(this)}}}})();export{e as A} \ No newline at end of file diff --git a/screenshot/compare/build/p-e8ca6d97.entry.js b/screenshot/compare/build/p-e8ca6d97.entry.js deleted file mode 100644 index 12781bcb51e..00000000000 --- a/screenshot/compare/build/p-e8ca6d97.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as t,g as r}from"./p-fbbae598.js";import{A as s}from"./p-e2efe0df.js";class i{constructor(r){t(this,r)}componentWillLoad(){if(this.history&&this.root&&this.url)return this.history.replace((r=this.root,"/"==(t=this.url).charAt(0)&&"/"==r.charAt(r.length-1)?r.slice(0,r.length-1)+t:r+t));var t,r}get el(){return r(this)}}s.injectProps(i,["history","root"]);export{i as stencil_router_redirect} \ No newline at end of file diff --git a/screenshot/compare/build/p-ec2f13e0.entry.js b/screenshot/compare/build/p-ec2f13e0.entry.js deleted file mode 100644 index 3c4e88a9979..00000000000 --- a/screenshot/compare/build/p-ec2f13e0.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as t,h as e}from"./p-fbbae598.js";class n{constructor(e){t(this,e),this.content=""}componentWillLoad(){if(null!=this.documentLocation)return this.fetchNewContent(this.documentLocation)}fetchNewContent(t){return fetch(t).then(t=>t.text()).then(t=>{this.content=t})}render(){return e("div",{innerHTML:this.content})}static get watchers(){return{documentLocation:["fetchNewContent"]}}}export{n as stencil_async_content} \ No newline at end of file diff --git a/screenshot/compare/build/p-f0b99977.entry.js b/screenshot/compare/build/p-f0b99977.entry.js deleted file mode 100644 index 358907e26a6..00000000000 --- a/screenshot/compare/build/p-f0b99977.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as t,g as s}from"./p-fbbae598.js";class e{constructor(s){t(this,s),this.context={},this.renderer=()=>null}connectedCallback(){null!=this.subscribe&&(this.unsubscribe=this.subscribe(this.el,"context"))}disconnectedCallback(){null!=this.unsubscribe&&this.unsubscribe()}render(){return this.renderer(Object.assign({},this.context))}get el(){return s(this)}}export{e as context_consumer} \ No newline at end of file diff --git a/screenshot/compare/build/p-f4745c2f.entry.js b/screenshot/compare/build/p-f4745c2f.entry.js deleted file mode 100644 index a8104539185..00000000000 --- a/screenshot/compare/build/p-f4745c2f.entry.js +++ /dev/null @@ -1 +0,0 @@ -import{r as t,d as s,h as i,g as e}from"./p-fbbae598.js";import{s as n}from"./p-081b0641.js";const h={threshold:.1,includeAA:!1,alpha:.1,aaColor:[255,255,0],diffColor:[255,0,0],diffColorAlt:null,diffMask:!1};function o(t){return ArrayBuffer.isView(t)&&1===t.constructor.BYTES_PER_ELEMENT}function r(t,s,i,e,n,h){const o=Math.max(s-1,0),r=Math.max(i-1,0),c=Math.min(s+1,e-1),d=Math.min(i+1,n-1),f=4*(i*e+s);let u,p,m,g,w=s===o||s===c||i===r||i===d?1:0,y=0,v=0;for(let n=o;n<=c;n++)for(let h=r;h<=d;h++){if(n===s&&h===i)continue;const o=l(t,t,f,4*(h*e+n),!0);if(0===o){if(w++,w>2)return!1}else ov&&(v=o,m=n,g=h)}return 0!==y&&0!==v&&(a(t,u,p,e,n)&&a(h,u,p,e,n)||a(t,m,g,e,n)&&a(h,m,g,e,n))}function a(t,s,i,e,n){const h=Math.max(s-1,0),o=Math.max(i-1,0),r=Math.min(s+1,e-1),a=Math.min(i+1,n-1),l=4*(i*e+s);let c=s===h||s===r||i===o||i===a?1:0;for(let n=h;n<=r;n++)for(let h=o;h<=a;h++){if(n===s&&h===i)continue;const o=4*(h*e+n);if(t[l]===t[o]&&t[l+1]===t[o+1]&&t[l+2]===t[o+2]&&t[l+3]===t[o+3]&&c++,c>2)return!0}return!1}function l(t,s,i,e,n){let h=t[i+0],o=t[i+1],r=t[i+2],a=t[i+3],l=s[e+0],p=s[e+1],m=s[e+2],g=s[e+3];if(a===g&&h===l&&o===p&&r===m)return 0;a<255&&(a/=255,h=u(h,a),o=u(o,a),r=u(r,a)),g<255&&(g/=255,l=u(l,g),p=u(p,g),m=u(m,g));const w=c(h,o,r),y=c(l,p,m),v=w-y;if(n)return v;const b=d(h,o,r)-d(l,p,m),x=f(h,o,r)-f(l,p,m),M=.5053*v*v+.299*b*b+.1957*x*x;return w>y?-M:M}function c(t,s,i){return.29889531*t+.58662247*s+.11448223*i}function d(t,s,i){return.59597799*t-.2741761*s-.32180189*i}function f(t,s,i){return.21147017*t-.52261711*s+.31114694*i}function u(t,s){return 255+(t-255)*s}function p(t,s,i,e,n){t[s+0]=i,t[s+1]=e,t[s+2]=n,t[s+3]=255}function m(t,s,i,e){const n=u(c(t[s+0],t[s+1],t[s+2]),i*t[s+3]/255);p(e,s,n,n,n)}function g(t,s,i){if(y.has(s))return void i(y.get(s));if(w.has(s))return void w.get(s).push(i);w.set(s,[i]);const e=document.createElement("script");e.src=`${t}screenshot_${s}.js`,document.head.appendChild(e)}window.loadScreenshot=(t,s)=>{const i=w.get(t);i&&(i.forEach(t=>t(s)),w.delete(t)),y.set(t,s)};const w=new Map,y=new Map;class v{constructor(i){t(this,i),this.imageASrc=null,this.imageBSrc=null,this.imageAClass="is-loading",this.imageBClass="is-loading",this.canvasClass="is-loading",this.imagesLoaded=new Set,this.isImageALoaded=!1,this.isImageBLoaded=!1,this.isMismatchInitialized=!1,this.hasCalculatedMismatch=!1,this.compareLoaded=s(this,"compareLoaded",7)}componentWillLoad(){this.loadScreenshots()}componentWillUpdate(){this.loadScreenshots()}loadScreenshots(){if(this.show&&this.diff.hasIntersected)return this.diff.identical?(this.imageASrc=this.imagesUrl+this.diff.imageA,this.isImageALoaded=!0,this.imageAClass="has-loaded",this.imageBSrc=this.imagesUrl+this.diff.imageB,this.isImageBLoaded=!0,void(this.imageBClass="has-loaded")):void(this.isMismatchInitialized||(this.isMismatchInitialized=!0,null!=this.jsonpUrl?(null!=this.diff.imageA&&g(this.jsonpUrl,this.diff.imageA,t=>{this.imageASrc=t}),null!=this.diff.imageB&&g(this.jsonpUrl,this.diff.imageB,t=>{this.imageBSrc=t})):(this.imageASrc=this.imagesUrl+this.diff.imageA,this.imageBSrc=this.imagesUrl+this.diff.imageB)))}async compareImages(){const t=this.diff;this.isImageALoaded&&this.isImageBLoaded&&!this.hasCalculatedMismatch&&t.comparable&&(this.hasCalculatedMismatch=!0,t.mismatchedPixels=await function(t,s,i,e,n,a){let c=-1;try{const d=document.createElement("canvas");d.width=e,d.height=n;const f=document.createElement("canvas");f.width=e,f.height=n;const u=d.getContext("2d");u.drawImage(t,0,0);const g=f.getContext("2d");g.drawImage(s,0,0);const w=document.createElement("canvas").getContext("2d");w.drawImage(t,0,0),w.getImageData(0,0,e,n);const y=u.getImageData(0,0,e,n).data,v=g.getImageData(0,0,e,n).data,b=i.getContext("2d"),x=b.createImageData(e,d.height);c=function(t,s,i,e,n,a){if(!o(t)||!o(s)||i&&!o(i))throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");if(t.length!==s.length||i&&i.length!==t.length)throw new Error("Image sizes do not match.");if(t.length!==e*n*4)throw new Error("Image data size does not match width/height.");a=Object.assign({},h,a);const c=e*n,d=new Uint32Array(t.buffer,t.byteOffset,c),f=new Uint32Array(s.buffer,s.byteOffset,c);let u=!0;for(let t=0;tg?a.includeAA||!r(t,o,h,e,n,s)&&!r(s,o,h,e,n,t)?(i&&p(i,c,...d<0&&a.diffColorAlt||a.diffColor),w++):i&&!a.diffMask&&p(i,c,...a.aaColor):i&&(a.diffMask||m(t,c,a.alpha,i))}return w}(y,v,x.data,e,n,{threshold:a}),b.putImageData(x,0,0)}catch(t){console.error(t)}return c}(this.imageA,this.imageB,this.canvas,Math.round(t.width*t.deviceScaleFactor),Math.round(t.height*t.deviceScaleFactor),t.threshold),this.canvasClass="has-loaded",n(t.imageA,t.imageB,t.mismatchedPixels,t.threshold),this.compareLoaded.emit(t))}render(){const t=this.diff,s={width:t.width+"px",height:t.height+"px"};return[i("compare-cell",null,null!=t.imageA?i("a",{href:this.imagesUrl+t.imageA,target:"_blank"},i("img",{src:this.imageASrc,class:this.imageAClass,style:s,onLoad:this.diff.identical?null:()=>{this.isImageALoaded=!0,this.imageAClass="has-loaded",this.compareImages()},ref:t=>this.imageA=t})):i("img",{style:s,class:"is-loading"})),i("compare-cell",null,null!=t.imageB?i("a",{href:this.imagesUrl+t.imageB,target:"_blank"},i("img",{src:this.imageBSrc,class:this.imageBClass,style:s,onLoad:this.diff.identical?null:()=>{this.isImageBLoaded=!0,this.imageBClass="has-loaded",this.compareImages()},ref:t=>this.imageB=t})):i("img",{style:s,class:"is-loading"})),i("compare-cell",null,this.diff.identical?i("img",{style:s,src:this.imageASrc}):i("canvas",{width:Math.round(t.width*t.deviceScaleFactor),height:Math.round(t.height*t.deviceScaleFactor),class:this.canvasClass,style:s,hidden:!t.comparable,ref:t=>this.canvas=t})),i("compare-cell",null,i("compare-analysis",{aId:this.aId,bId:this.bId,mismatchedPixels:this.diff.mismatchedPixels,diff:this.diff}))]}get elm(){return e(this)}}v.style="compare-row img,compare-row canvas{display:block;box-shadow:var(--screenshot-box-shadow);border-radius:var(--screenshot-border-radius)}compare-row a{display:block}.is-loading{visibility:hidden}";class b{constructor(s){t(this,s)}render(){if(!this.a||!this.b||!this.diffs)return;let t=0;this.diffs.forEach(s=>{s.width>t&&(t=s.width)}),t-=6;const s={width:t+"px"};return[i("th-cell",null,i("div",{style:s},i("a",{href:this.a.url,target:"_blank"},this.a.message))),i("th-cell",null,i("div",{style:s},i("a",{href:this.b.url,target:"_blank"},this.b.message))),i("th-cell",null,i("div",{style:s},i("a",{href:`https://github.com/ionic-team/ionic/compare/${this.a.id}...${this.b.id}`,target:"_blank"},"Compare: ",this.a.id," - ",this.b.id))),i("th-cell",{class:"analysis"},i("div",null,"Analysis"))]}}b.style=":host{display:flex}th-cell{display:block;flex:1;font-weight:500;font-size:12px}th-cell div{padding-left:12px;padding-right:12px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}th-cell a{color:var(--font-color);text-decoration:none}th-cell a:hover{color:var(--analysis-data-color);text-decoration:underline}.analysis div{width:262px}";export{v as compare_row,b as compare_thead} \ No newline at end of file diff --git a/screenshot/compare/build/p-fbbae598.js b/screenshot/compare/build/p-fbbae598.js deleted file mode 100644 index 432178d510b..00000000000 --- a/screenshot/compare/build/p-fbbae598.js +++ /dev/null @@ -1 +0,0 @@ -let e,t,n,l=!1,o=!1,s=!1,r=0,i=!1;const c="undefined"!=typeof window?window:{},a=c.document||{head:{}},f={t:0,l:"",jmp:e=>e(),raf:e=>requestAnimationFrame(e),ael:(e,t,n,l)=>e.addEventListener(t,n,l),rel:(e,t,n,l)=>e.removeEventListener(t,n,l)},u=e=>Promise.resolve(e),d=(()=>{try{return new CSSStyleSheet,!0}catch(e){}return!1})(),p={},$=(e,t,n)=>{n&&n.map(([n,l,o])=>{const s=e,r=m(t,o),i=h(n);f.ael(s,l,r,i),(t.o=t.o||[]).push(()=>f.rel(s,l,r,i))})},m=(e,t)=>n=>{256&e.t?e.s[t](n):(e.i=e.i||[]).push([t,n])},h=e=>0!=(2&e),y="http://www.w3.org/1999/xlink",b=new WeakMap,w=e=>"sc-"+e.u,k={},v=e=>"object"==(e=typeof e)||"function"===e,g=(e,t,...n)=>{let l=null,o=null,s=null,r=!1,i=!1,c=[];const a=t=>{for(let n=0;ne[t]).join(" "))}}if("function"==typeof e)return e(null===t?{}:t,c,M);const f=S(e,null);return f.$=t,c.length>0&&(f.m=c),f.h=o,f.k=s,f},S=(e,t)=>({t:0,v:e,p:t,g:null,m:null,$:null,h:null,k:null}),j={},M={forEach:(e,t)=>e.map(U).forEach(t),map:(e,t)=>e.map(U).map(t).map(C)},U=e=>({vattrs:e.$,vchildren:e.m,vkey:e.h,vname:e.k,vtag:e.v,vtext:e.p}),C=e=>{const t=S(e.vtag,e.vtext);return t.$=e.vattrs,t.m=e.vchildren,t.h=e.vkey,t.k=e.vname,t},R=(e,t,n,l,o,s)=>{if(n!==l){let r=de(e,t),i=t.toLowerCase();if("class"===t){const t=e.classList,o=L(n),s=L(l);t.remove(...o.filter(e=>e&&!s.includes(e))),t.add(...s.filter(e=>e&&!o.includes(e)))}else if("style"===t){for(const t in n)l&&null!=l[t]||(t.includes("-")?e.style.removeProperty(t):e.style[t]="");for(const t in l)n&&l[t]===n[t]||(t.includes("-")?e.style.setProperty(t,l[t]):e.style[t]=l[t])}else if("key"===t);else if("ref"===t)l&&l(e);else if(r||"o"!==t[0]||"n"!==t[1]){const c=v(l);if((r||c&&null!==l)&&!o)try{if(e.tagName.includes("-"))e[t]=l;else{let o=null==l?"":l;"list"===t?r=!1:null!=n&&e[t]==o||(e[t]=o)}}catch(e){}let a=!1;i!==(i=i.replace(/^xlink\:?/,""))&&(t=i,a=!0),null==l||!1===l?a?e.removeAttributeNS(y,t):e.removeAttribute(t):(!r||4&s||o)&&!c&&(l=!0===l?"":l,a?e.setAttributeNS(y,t,l):e.setAttribute(t,l))}else t="-"===t[2]?t.slice(3):de(c,i)?i.slice(2):i[2]+t.slice(3),n&&f.rel(e,t,n,!1),l&&f.ael(e,t,l,!1)}},x=/\s/,L=e=>e?e.split(x):[],O=(e,t,n,l)=>{const o=11===t.g.nodeType&&t.g.host?t.g.host:t.g,s=e&&e.$||k,r=t.$||k;for(l in s)l in r||R(o,l,s[l],void 0,n,t.t);for(l in r)R(o,l,s[l],r[l],n,t.t)},P=(o,r,i,c)=>{let f,u,d,p=r.m[i],$=0;if(l||(s=!0,"slot"===p.v&&(e&&c.classList.add(e+"-s"),p.t|=p.m?2:1)),null!==p.p)f=p.g=a.createTextNode(p.p);else if(1&p.t)f=p.g=a.createTextNode("");else if(f=p.g=a.createElement(2&p.t?"slot-fb":p.v),O(null,p,!1),null!=e&&f["s-si"]!==e&&f.classList.add(f["s-si"]=e),p.m)for($=0;${f.t|=1;const l=e.childNodes;for(let e=l.length-1;e>=0;e--){const o=l[e];o["s-hn"]!==n&&o["s-ol"]&&(A(o).insertBefore(o,q(o)),o["s-ol"].remove(),o["s-ol"]=void 0,s=!0),t&&T(o,t)}f.t&=-2},E=(e,t,l,o,s,r)=>{let i,c=e["s-cr"]&&e["s-cr"].parentNode||e;for(c.shadowRoot&&c.tagName===n&&(c=c.shadowRoot);s<=r;++s)o[s]&&(i=P(null,l,s,e),i&&(o[s].g=i,c.insertBefore(i,q(t))))},W=(e,t,n,l,s)=>{for(;t<=n;++t)(l=e[t])&&(s=l.g,z(l),o=!0,s["s-ol"]?s["s-ol"].remove():T(s,!0),s.remove())},D=(e,t)=>e.v===t.v&&("slot"===e.v?e.k===t.k:e.h===t.h),q=e=>e&&e["s-ol"]||e,A=e=>(e["s-ol"]?e["s-ol"]:e).parentNode,F=(e,t)=>{const n=t.g=e.g,l=e.m,o=t.m,s=t.p;let r;null===s?("slot"===t.v||O(e,t,!1),null!==l&&null!==o?((e,t,n,l)=>{let o,s,r=0,i=0,c=0,a=0,f=t.length-1,u=t[0],d=t[f],p=l.length-1,$=l[0],m=l[p];for(;r<=f&&i<=p;)if(null==u)u=t[++r];else if(null==d)d=t[--f];else if(null==$)$=l[++i];else if(null==m)m=l[--p];else if(D(u,$))F(u,$),u=t[++r],$=l[++i];else if(D(d,m))F(d,m),d=t[--f],m=l[--p];else if(D(u,m))"slot"!==u.v&&"slot"!==m.v||T(u.g.parentNode,!1),F(u,m),e.insertBefore(u.g,d.g.nextSibling),u=t[++r],m=l[--p];else if(D(d,$))"slot"!==u.v&&"slot"!==m.v||T(d.g.parentNode,!1),F(d,$),e.insertBefore(d.g,u.g),d=t[--f],$=l[++i];else{for(c=-1,a=r;a<=f;++a)if(t[a]&&null!==t[a].h&&t[a].h===$.h){c=a;break}c>=0?(s=t[c],s.v!==$.v?o=P(t&&t[i],n,c,e):(F(s,$),t[c]=void 0,o=s.g),$=l[++i]):(o=P(t&&t[i],n,i,e),$=l[++i]),o&&A(u.g).insertBefore(o,q(u.g))}r>f?E(e,null==l[p+1]?null:l[p+1].g,n,l,i,p):i>p&&W(t,r,f)})(n,l,t,o):null!==o?(null!==e.p&&(n.textContent=""),E(n,null,t,o,0,o.length-1)):null!==l&&W(l,0,l.length-1)):(r=n["s-cr"])?r.parentNode.textContent=s:e.p!==s&&(n.data=s)},N=e=>{let t,n,l,o,s,r,i=e.childNodes;for(n=0,l=i.length;n{let t,n,l,s,r,i,c=0,a=e.childNodes,f=a.length;for(;c=0;i--)n=l[i],n["s-cn"]||n["s-nr"]||n["s-hn"]===t["s-hn"]||(_(n,s)?(r=H.find(e=>e.S===n),o=!0,n["s-sn"]=n["s-sn"]||s,r?r.j=t:H.push({j:t,S:n}),n["s-sr"]&&H.map(e=>{_(e.S,n["s-sn"])&&(r=H.find(e=>e.S===n),r&&!e.j&&(e.j=r.j))})):H.some(e=>e.S===n)||H.push({S:n}));1===t.nodeType&&V(t)}},_=(e,t)=>1===e.nodeType?null===e.getAttribute("slot")&&""===t||e.getAttribute("slot")===t:e["s-sn"]===t||""===t,z=e=>{e.$&&e.$.ref&&e.$.ref(null),e.m&&e.m.map(z)},B=e=>ae(e).M,G=(e,t,n)=>{const l=B(e);return{emit:e=>I(l,t,{bubbles:!!(4&n),composed:!!(2&n),cancelable:!!(1&n),detail:e})}},I=(e,t,n)=>{const l=new CustomEvent(t,n);return e.dispatchEvent(l),l},J=(e,t)=>{t&&!e.U&&t["s-p"]&&t["s-p"].push(new Promise(t=>e.U=t))},K=(e,t)=>{if(e.t|=16,!(4&e.t))return J(e,e.C),Me(()=>Q(e,t));e.t|=512},Q=(e,t)=>{const n=e.s;let l;return t?(e.t|=256,e.i&&(e.i.map(([e,t])=>te(n,e,t)),e.i=null),l=te(n,"componentWillLoad")):l=te(n,"componentWillUpdate"),ne(l,()=>X(e,n,t))},X=(r,i,c)=>{const u=r.M,d=u["s-rc"];c&&(e=>{const t=e.R,n=e.M,l=t.t,o=((e,t)=>{let n=w(t),l=he.get(n);if(e=11===e.nodeType?e:a,l)if("string"==typeof l){let t,o=b.get(e=e.head||e);o||b.set(e,o=new Set),o.has(n)||(t=a.createElement("style"),t.innerHTML=l,e.insertBefore(t,e.querySelector("link")),o&&o.add(n))}else e.adoptedStyleSheets.includes(l)||(e.adoptedStyleSheets=[...e.adoptedStyleSheets,l]);return n})(n.shadowRoot?n.shadowRoot:n.getRootNode(),t);10&l&&(n["s-sc"]=o,n.classList.add(o+"-h"))})(r),((r,i)=>{const c=r.M,u=r.R,d=r.L||S(null,null),p=(e=>e&&e.v===j)(i)?i:g(null,null,i);if(n=c.tagName,u.O&&(p.$=p.$||{},u.O.map(([e,t])=>p.$[t]=c[e])),p.v=null,p.t|=4,r.L=p,p.g=d.g=c.shadowRoot||c,e=c["s-sc"],t=c["s-cr"],l=0!=(1&u.t),o=!1,F(d,p),f.t|=1,s){let e,t,n,l,o,s;V(p.g);let r=0;for(;re()),u["s-rc"]=void 0);{const e=u["s-p"],t=()=>Z(r);0===e.length?t():(Promise.all(e).then(t),r.t|=4,e.length=0)}},Y=(e,t)=>{try{t=t.render&&t.render(),e.t&=-17,e.t|=2}catch(e){pe(e)}return t},Z=e=>{const t=e.M,n=e.s,l=e.C;64&e.t?te(n,"componentDidUpdate"):(e.t|=64,le(t),te(n,"componentDidLoad"),e.P(t),l||ee()),e.U&&(e.U(),e.U=void 0),512&e.t&&Se(()=>K(e,!1)),e.t&=-517},ee=()=>{le(a.documentElement),f.t|=2,Se(()=>I(c,"appload",{detail:{namespace:"app"}}))},te=(e,t,n)=>{if(e&&e[t])try{return e[t](n)}catch(e){pe(e)}},ne=(e,t)=>e&&e.then?e.then(t):t(),le=e=>e.classList.add("hydrated"),oe=(e,t,n)=>{if(t.T){e.watchers&&(t.W=e.watchers);const l=Object.entries(t.T),o=e.prototype;if(l.map(([e,[l]])=>{(31&l||2&n&&32&l)&&Object.defineProperty(o,e,{get(){return((e,t)=>ae(this).D.get(t))(0,e)},set(n){((e,t,n,l)=>{const o=ae(this),s=o.D.get(t),r=o.t,i=o.s;if(n=((e,t)=>null==e||v(e)?e:4&t?"false"!==e&&(""===e||!!e):2&t?parseFloat(e):1&t?e+"":e)(n,l.T[t][0]),!(8&r&&void 0!==s||n===s)&&(o.D.set(t,n),i)){if(l.W&&128&r){const e=l.W[t];e&&e.map(e=>{try{i[e](n,s,t)}catch(e){pe(e)}})}2==(18&r)&&K(o,!1)}})(0,e,n,t)},configurable:!0,enumerable:!0})}),1&n){const n=new Map;o.attributeChangedCallback=function(e,t,l){f.jmp(()=>{const t=n.get(e);this[t]=(null!==l||"boolean"!=typeof this[t])&&l})},e.observedAttributes=l.filter(([e,t])=>15&t[0]).map(([e,l])=>{const o=l[1]||e;return n.set(o,e),512&l[0]&&t.O.push([e,o]),o})}}return e},se=e=>{te(e,"connectedCallback")},re=(e,t={})=>{const n=[],l=t.exclude||[],o=c.customElements,s=a.head,r=s.querySelector("meta[charset]"),i=a.createElement("style"),u=[];let p,m=!0;Object.assign(f,t),f.l=new URL(t.resourcesUrl||"./",a.baseURI).href,t.syncQueue&&(f.t|=4),e.map(e=>e[1].map(t=>{const s={t:t[0],u:t[1],T:t[2],q:t[3]};s.T=t[2],s.q=t[3],s.O=[],s.W={};const r=s.u,i=class extends HTMLElement{constructor(e){super(e),ue(e=this,s),1&s.t&&e.attachShadow({mode:"open"})}connectedCallback(){p&&(clearTimeout(p),p=null),m?u.push(this):f.jmp(()=>(e=>{if(0==(1&f.t)){const t=ae(e),n=t.R,l=()=>{};if(1&t.t)$(e,t,n.q),se(t.s);else{t.t|=1,12&n.t&&(e=>{const t=e["s-cr"]=a.createComment("");t["s-cn"]=!0,e.insertBefore(t,e.firstChild)})(e);{let n=e;for(;n=n.parentNode||n.host;)if(n["s-p"]){J(t,t.C=n);break}}n.T&&Object.entries(n.T).map(([t,[n]])=>{if(31&n&&e.hasOwnProperty(t)){const n=e[t];delete e[t],e[t]=n}}),Se(()=>(async(e,t,n,l,o)=>{if(0==(32&t.t)){t.t|=32;{if((o=me(n)).then){const e=()=>{};o=await o,e()}o.isProxied||(n.W=o.watchers,oe(o,n,2),o.isProxied=!0);const e=()=>{};t.t|=8;try{new o(t)}catch(e){pe(e)}t.t&=-9,t.t|=128,e(),se(t.s)}const e=w(n);if(!he.has(e)&&o.style){const t=()=>{};((e,t,n)=>{let l=he.get(e);d&&n?(l=l||new CSSStyleSheet,l.replace(t)):l=t,he.set(e,l)})(e,o.style,!!(1&n.t)),t()}}const s=t.C,r=()=>K(t,!0);s&&s["s-rc"]?s["s-rc"].push(r):r()})(0,t,n))}l()}})(this))}disconnectedCallback(){f.jmp(()=>(()=>{if(0==(1&f.t)){const e=ae(this),t=e.s;e.o&&(e.o.map(e=>e()),e.o=void 0),te(t,"disconnectedCallback"),te(t,"componentDidUnload")}})())}forceUpdate(){(()=>{{const e=ae(this);e.M.isConnected&&2==(18&e.t)&&K(e,!1)}})()}componentOnReady(){return ae(this).A}};s.F=e[0],l.includes(r)||o.get(r)||(n.push(r),o.define(r,oe(i,s,1)))})),i.innerHTML=n+"{visibility:hidden}.hydrated{visibility:inherit}",i.setAttribute("data-styles",""),s.insertBefore(i,r?r.nextSibling:s.firstChild),m=!1,u.length?u.map(e=>e.connectedCallback()):f.jmp(()=>p=setTimeout(ee,30))},ie=(e,t)=>t in p?p[t]:"window"===t?c:"document"===t?a:"isServer"!==t&&"isPrerender"!==t&&("isClient"===t||("resourcesUrl"===t||"publicPath"===t?(()=>{const e=new URL(".",f.l);return e.origin!==c.location.origin?e.href:e.pathname})():"queue"===t?{write:Me,read:je,tick:{then:e=>Se(e)}}:void 0)),ce=new WeakMap,ae=e=>ce.get(e),fe=(e,t)=>ce.set(t.s=e,t),ue=(e,t)=>{const n={t:0,M:e,R:t,D:new Map};return n.A=new Promise(e=>n.P=e),e["s-p"]=[],e["s-rc"]=[],$(e,n,t.q),ce.set(e,n)},de=(e,t)=>t in e,pe=e=>console.error(e),$e=new Map,me=e=>{const t=e.u.replace(/-/g,"_"),n=e.F,l=$e.get(n);return l?l[t]:import(`./${n}.entry.js`).then(e=>($e.set(n,e),e[t]),pe)},he=new Map,ye=[],be=[],we=[],ke=(e,t)=>n=>{e.push(n),i||(i=!0,t&&4&f.t?Se(ge):f.raf(ge))},ve=(e,t)=>{let n=0,l=0;for(;n{r++,(e=>{for(let t=0;t0&&(we.push(...be),be.length=0),(i=ye.length+be.length+we.length>0)?f.raf(ge):r=0}},Se=e=>u().then(e),je=ke(ye,!1),Me=ke(be,!0),Ue=()=>u(),Ce=()=>{const e=import.meta.url,t={};return""!==e&&(t.resourcesUrl=new URL(".",e).href),u(t)};export{Ue as a,re as b,ie as c,G as d,B as g,g as h,Ce as p,fe as r} \ No newline at end of file diff --git a/screenshot/compare/host.config.json b/screenshot/compare/host.config.json deleted file mode 100644 index f4314c75f32..00000000000 --- a/screenshot/compare/host.config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hosting": { - "headers": [ - { - "source": "/build/p-*", - "headers": [ - { - "key": "Cache-Control", - "value": "max-age=31556952, s-maxage=31556952, immutable" - } - ] - } - ] - } -} \ No newline at end of file diff --git a/screenshot/compare/index.html b/screenshot/compare/index.html deleted file mode 100644 index 4b874c3fddb..00000000000 --- a/screenshot/compare/index.html +++ /dev/null @@ -1 +0,0 @@ - Stencil Screenshot Visual Diff \ No newline at end of file diff --git a/screenshot/compare/manifest.json b/screenshot/compare/manifest.json deleted file mode 100644 index 48b7f9802e2..00000000000 --- a/screenshot/compare/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "Screenshot", - "short_name": "Screenshot", - "start_url": "/", - "display": "standalone", - "icons": [{ - "src": "/assets/favicon.ico", - "sizes": "192x192", - "type": "image/x-icon" - }], - "background_color": "#488aff", - "theme_color": "#488aff" -} \ No newline at end of file diff --git a/screenshot/connector.js b/screenshot/connector.js deleted file mode 100644 index 84fceb4f18a..00000000000 --- a/screenshot/connector.js +++ /dev/null @@ -1,2 +0,0 @@ -const { ScreenshotConnector } = require('./index.js'); -module.exports = ScreenshotConnector; diff --git a/screenshot/local-connector.js b/screenshot/local-connector.js deleted file mode 100644 index deea8a73c06..00000000000 --- a/screenshot/local-connector.js +++ /dev/null @@ -1,2 +0,0 @@ -const { ScreenshotLocalConnector } = require('./index.js'); -module.exports = ScreenshotLocalConnector; diff --git a/scripts/build.ts b/scripts/build.ts deleted file mode 100644 index e86aed63d68..00000000000 --- a/scripts/build.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { buildCli } from './esbuild/cli'; -import { buildCompiler } from './esbuild/compiler'; -import { buildDevServer } from './esbuild/dev-server'; -import { buildInternal } from './esbuild/internal'; -import { buildMockDoc } from './esbuild/mock-doc'; -import { buildScreenshot } from './esbuild/screenshot'; -import { buildSysNode } from './esbuild/sys-node'; -import { buildTesting } from './esbuild/testing'; -import { release } from './release'; -import { validateBuild } from './test/validate-build'; -import { BuildOptions, getOptions } from './utils/options'; - -// the main entry point for the build -export async function run(rootDir: string, args: ReadonlyArray) { - const opts = getOptions(process.cwd(), { - isProd: args.includes('--prod'), - isCI: args.includes('--ci'), - isWatch: args.includes('--watch'), - }); - - try { - if (args.includes('--release')) { - await release(rootDir, args); - return; - } - - if (args.includes('--validate-build')) { - await validateBuild(rootDir); - return; - } - await buildAll(opts); - } catch (e) { - console.error(e); - process.exit(1); - } -} - -export async function buildAll(opts: BuildOptions) { - await Promise.all([ - buildCli(opts), - buildCompiler(opts), - buildDevServer(opts), - buildMockDoc(opts), - buildScreenshot(opts), - buildSysNode(opts), - buildTesting(opts), - buildInternal(opts), - ]); -} diff --git a/scripts/esbuild/cli.ts b/scripts/esbuild/cli.ts deleted file mode 100644 index f5691f25e32..00000000000 --- a/scripts/esbuild/cli.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { getBanner } from '../utils/banner'; -import { BuildOptions } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { externalNodeModules, getBaseEsbuildOptions, getEsbuildAliases, runBuilds } from './utils'; - -/** - * Runs esbuild to bundle the `cli` submodule - * - * @param opts build options - * @returns a promise for this bundle's build output - */ -export async function buildCli(opts: BuildOptions) { - // clear out rollup stuff - await fs.emptyDir(opts.output.cliDir); - - const inputDir = join(opts.srcDir, 'cli'); - const buildDir = join(opts.buildDir, 'cli'); - - const outputDir = opts.output.cliDir; - const esmFilename = 'index.js'; - const cjsFilename = 'index.cjs'; - - const dtsFilename = 'index.d.ts'; - - const cliAliases = getEsbuildAliases(); - // this isn't strictly necessary to alias - however, this minimizes cuts down the bundle size by ~70kb. - cliAliases['prompts'] = 'prompts/lib/index.js'; - - const external = [...externalNodeModules, '../testing/*']; - - const cliEsbuildOptions = { - ...getBaseEsbuildOptions(), - alias: cliAliases, - entryPoints: [join(inputDir, 'index.ts')], - external, - platform: 'node', - } satisfies ESBuildOptions; - - // ESM build options - const esmOptions: ESBuildOptions = { - ...cliEsbuildOptions, - outfile: join(outputDir, esmFilename), - format: 'esm', - banner: { - js: getBanner(opts, `Stencil CLI`, true), - }, - }; - - // CommonJS build options - const cjsOptions: ESBuildOptions = { - ...cliEsbuildOptions, - outfile: join(outputDir, cjsFilename), - platform: 'node', - format: 'cjs', - banner: { - js: getBanner(opts, `Stencil CLI (CommonJS)`, true), - }, - }; - - // create public d.ts - let dts = await fs.readFile(join(buildDir, 'public.d.ts'), 'utf8'); - dts = dts.replace('@stencil/core/internal', '../internal/index'); - await fs.writeFile(join(opts.output.cliDir, dtsFilename), dts); - - // copy config-flags.d.ts - let configDts = await fs.readFile(join(buildDir, 'config-flags.d.ts'), 'utf8'); - configDts = configDts.replace('@stencil/core/declarations', '../internal/index'); - await fs.writeFile(join(opts.output.cliDir, 'config-flags.d.ts'), configDts); - - // write cli/package.json - writePkgJson(opts, opts.output.cliDir, { - name: '@stencil/core/cli', - description: 'Stencil CLI.', - main: cjsFilename, - module: esmFilename, - types: dtsFilename, - }); - - return runBuilds([esmOptions, cjsOptions], opts); -} diff --git a/scripts/esbuild/compiler.ts b/scripts/esbuild/compiler.ts deleted file mode 100644 index 9fc45e4d936..00000000000 --- a/scripts/esbuild/compiler.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import { replace } from 'esbuild-plugin-replace'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { getBanner } from '../utils/banner'; -import { BuildOptions, createReplaceData } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { externalNodeModules, getBaseEsbuildOptions, getEsbuildAliases, runBuilds } from './utils'; -import { bundleParse5 } from './utils/parse5'; -import { bundleTerser } from './utils/terser'; -import { bundleTypeScriptSource, tsCacheFilePath } from './utils/typescript-source'; - -export async function buildCompiler(opts: BuildOptions) { - const inputDir = join(opts.buildDir, 'compiler'); - const srcDir = join(opts.srcDir, 'compiler'); - const compilerFileName = 'stencil.js'; - const compilerDtsName = compilerFileName.replace('.js', '.d.ts'); - - // clear out rollup stuff and ensure directory exists - await fs.emptyDir(opts.output.compilerDir); - - // create public d.ts - let dts = await fs.readFile(join(inputDir, 'public.d.ts'), 'utf8'); - dts = dts.replace('@stencil/core/internal', '../internal/index'); - await fs.writeFile(join(opts.output.compilerDir, compilerDtsName), dts); - - // write @stencil/core/compiler/package.json - writePkgJson(opts, opts.output.compilerDir, { - name: '@stencil/core/compiler', - description: 'Stencil Compiler.', - main: compilerFileName, - types: compilerDtsName, - }); - - // copy and edit compiler/sys/in-memory-fs.d.ts - let inMemoryFsDts = await fs.readFile(join(inputDir, 'sys', 'in-memory-fs.d.ts'), 'utf8'); - inMemoryFsDts = inMemoryFsDts.replace('@stencil/core/internal', '../../internal/index'); - await fs.ensureDir(join(opts.output.compilerDir, 'sys')); - await fs.writeFile(join(opts.output.compilerDir, 'sys', 'in-memory-fs.d.ts'), inMemoryFsDts); - - // copy and edit compiler/transpile.d.ts - let transpileDts = await fs.readFile(join(inputDir, 'transpile.d.ts'), 'utf8'); - transpileDts = transpileDts.replace('@stencil/core/internal', '../internal/index'); - await fs.writeFile(join(opts.output.compilerDir, 'transpile.d.ts'), transpileDts); - - const alias: Record = { - ...getEsbuildAliases(), - glob: './sys/node/glob.js', - '@sys-api-node': '../sys/node/index.js', - }; - - const external = [ - ...externalNodeModules, - '../mock-doc/index.cjs', - '../sys/node/autoprefixer.js', - '../sys/node/index.js', - ]; - - // get replace data, which replaces certain strings within the output with - // build-time constants. - // - // this setup was originally designed for use with the Rollup `replace` - // plugin, but there is an esbuild plugin which provides equivalent - // functionality - // - // note that the `bundleTypeScriptSource` function implicitly depends on - // `createReplaceData` being called before it - const replaceData = createReplaceData(opts); - - // stuff to patch typescript before bundling - const tsPath = require.resolve('typescript'); - await bundleTypeScriptSource(tsPath, opts); - const tsFilePath = tsCacheFilePath(opts); - alias['typescript'] = tsFilePath; - - // same for terser - const [, terserPath] = await bundleTerser(opts); - alias['terser'] = terserPath; - - // and parse5 - const [, parse5path] = await bundleParse5(opts); - alias['parse5'] = parse5path; - - const compilerEsbuildOptions: ESBuildOptions = { - ...getBaseEsbuildOptions(), - banner: { js: getBanner(opts, 'Stencil Compiler', true) }, - entryPoints: [join(srcDir, 'index.ts')], - platform: 'node', - external, - format: 'cjs', - alias, - plugins: [replace(replaceData)], - outfile: join(opts.output.compilerDir, compilerFileName), - // workaround for fdir https://github.com/thecodrr/fdir/issues/163 - inject: [join(opts.bundleHelpersDir, 'import-meta-url.js')], - define: { - 'import.meta.url': 'import_meta_url', - }, - }; - - // copy typescript default lib dts files - const tsLibNames = await getTypeScriptDefaultLibNames(opts); - await Promise.all(tsLibNames.map((f) => fs.copy(join(opts.typescriptLibDir, f), join(opts.output.compilerDir, f)))); - - return runBuilds([compilerEsbuildOptions], opts); -} - -/** - * Helper function that reads in the `lib.*.d.ts` files in the TypeScript lib/ directory on disk. - * @param opts the Stencil build options, which includes the location of the TypeScript lib/ - * @returns all file names that match the `lib.*.d.ts` format - */ -async function getTypeScriptDefaultLibNames(opts: BuildOptions): Promise { - return (await fs.readdir(opts.typescriptLibDir)).filter((f) => f.startsWith('lib.') && f.endsWith('.d.ts')); -} diff --git a/scripts/esbuild/dev-server.ts b/scripts/esbuild/dev-server.ts deleted file mode 100644 index da5e78e068d..00000000000 --- a/scripts/esbuild/dev-server.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { builtinModules } from 'node:module'; -import { join } from 'node:path'; - -import type { BuildOptions as ESBuildOptions, Plugin } from 'esbuild'; -import { replace } from 'esbuild-plugin-replace'; -import fs from 'fs-extra'; -import ts from 'typescript'; - -import { getBanner } from '../utils/banner'; -import { type BuildOptions, createReplaceData } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { externalAlias, getBaseEsbuildOptions, getEsbuildAliases, getFirstOutputFile, runBuilds } from './utils'; -import { createContentTypeData } from './utils/content-types'; - -const CONNECTOR_NAME = 'connector.html'; - -/** - * Runs esbuild to bundle the `dev-server` submodule - * - * @param opts build options - * @returns a promise for this bundle's build output - */ -export async function buildDevServer(opts: BuildOptions) { - // create dir of not existing already - await fs.ensureDir(opts.output.devServerDir); - // clear out rollup stuff - await fs.emptyDir(opts.output.devServerDir); - - const inputDir = join(opts.buildDir, 'dev-server'); - - // create public d.ts - let dts = await fs.readFile(join(opts.buildDir, 'dev-server', 'index.d.ts'), 'utf8'); - dts = dts.replace('../declarations', '../internal/index'); - await fs.writeFile(join(opts.output.devServerDir, 'index.d.ts'), dts); - - // write package.json - writePkgJson(opts, opts.output.devServerDir, { - name: '@stencil/core/dev-server', - description: 'Stencil Development Server which communicates with the Stencil Compiler.', - main: 'index.js', - types: 'index.d.ts', - }); - - // copy static files - await fs.copy(join(opts.srcDir, 'dev-server', 'static'), join(opts.output.devServerDir, 'static')); - - // copy server-worker-thread.js - await fs.copy( - join(opts.srcDir, 'dev-server', 'server-worker-thread.js'), - join(opts.output.devServerDir, 'server-worker-thread.js'), - ); - - // copy template files - await fs.copy(join(opts.srcDir, 'dev-server', 'templates'), join(opts.output.devServerDir, 'templates')); - - // open-in-editor's visualstudio.vbs file - const visualstudioVbsSrc = join(opts.nodeModulesDir, 'open-in-editor', 'lib', 'editors', 'visualstudio.vbs'); - const visualstudioVbsDesc = join(opts.output.devServerDir, 'visualstudio.vbs'); - await fs.copy(visualstudioVbsSrc, visualstudioVbsDesc); - - // copy open's xdg-open file - const xdgOpenSrcPath = join(opts.nodeModulesDir, 'open', 'xdg-open'); - const xdgOpenDestPath = join(opts.output.devServerDir, 'xdg-open'); - await fs.copy(xdgOpenSrcPath, xdgOpenDestPath); - - const external = [...builtinModules]; - const devServerAliases = { - ...getEsbuildAliases(), - glob: '../../sys/node/glob.js', - '@stencil/core/mock-doc': '../../mock-doc/index.cjs', - }; - const devServerIndexEsbuildOptions = { - ...getBaseEsbuildOptions(), - alias: devServerAliases, - entryPoints: [join(inputDir, 'index.js')], - outfile: join(opts.output.devServerDir, 'index.js'), - external: ['@dev-server-process', ...external], - format: 'cjs', - platform: 'node', - write: false, - plugins: [serverProcessAliasPlugin(), replace(createReplaceData(opts))], - banner: { - js: getBanner(opts, `Stencil Dev Server`, true), - }, - } satisfies ESBuildOptions; - - const devServerProcessEsbuildOptions = { - ...getBaseEsbuildOptions(), - alias: { - ...devServerAliases, - glob: '../../sys/node/glob.js', - '@stencil/core/mock-doc': '../../mock-doc/index.cjs', - '@sys-api-node': '../sys/node/index.js', - }, - entryPoints: [join(inputDir, 'server-process.js')], - outfile: join(opts.output.devServerDir, 'server-process.js'), - external: [...external, '../sys/node/index.js'], - format: 'cjs', - platform: 'node', - write: false, - plugins: [ - esm2CJSPlugin(), - contentTypesPlugin(opts), - replace(createReplaceData(opts)), - externalAlias('graceful-fs', '../sys/node/graceful-fs.js'), - ], - banner: { - js: getBanner(opts, `Stencil Dev Server Process`, true), - }, - } satisfies ESBuildOptions; - - const connectorAlias = { - glob: '../../sys/node/glob.js', - '@stencil/core/dev-server/client': join(inputDir, 'client', 'index.js'), - '@stencil/core/mock-doc': '../../mock-doc/index.cjs', - }; - const connectorEsbuildOptions = { - ...getBaseEsbuildOptions(), - alias: connectorAlias, - entryPoints: [join(inputDir, 'dev-server-client', 'index.js')], - outfile: join(opts.output.devServerDir, CONNECTOR_NAME), - format: 'cjs', - platform: 'node', - write: false, - plugins: [appErrorCssPlugin(opts), clientConnectorPlugin(opts), replace(createReplaceData(opts))], - } satisfies ESBuildOptions; - - await fs.ensureDir(join(opts.output.devServerDir, 'client')); - // copy dev server client dts files - await fs.copy(join(opts.buildDir, 'dev-server', 'client'), join(opts.output.devServerDir, 'client'), { - filter: (src) => { - if (src.endsWith('.d.ts')) { - return true; - } - const stats = fs.statSync(src); - if (stats.isDirectory()) { - return true; - } - return false; - }, - }); - - // write package.json - writePkgJson(opts, join(opts.output.devServerDir, 'client'), { - name: '@stencil/core/dev-server/client', - description: 'Stencil Dev Server Client.', - main: 'index.js', - types: 'index.d.ts', - }); - - const devServerClientEsbuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [join(opts.buildDir, 'dev-server', 'client', 'index.js')], - outfile: join(opts.output.devServerDir, 'client', 'index.js'), - format: 'esm', - platform: 'node', - plugins: [appErrorCssPlugin(opts), replace(createReplaceData(opts))], - banner: { - js: getBanner(opts, `Stencil Dev Server Client`, true), - }, - } satisfies ESBuildOptions; - - return runBuilds( - [ - devServerIndexEsbuildOptions, - devServerProcessEsbuildOptions, - connectorEsbuildOptions, - devServerClientEsbuildOptions, - ], - opts, - ); -} - -/** - * Load CSS files and export them as a string - * @param opts build options - * @returns an esbuild plugin - */ -function appErrorCssPlugin(opts: BuildOptions): Plugin { - return { - name: 'appErrorCss', - setup(build) { - build.onResolve({ filter: /app-error\.css$/ }, () => ({ - path: join(opts.srcDir, 'dev-server', 'client', 'app-error.css'), - })); - build.onLoad({ filter: /app-error\.css$/ }, async (args) => { - const code = await fs.readFile(args.path, 'utf8'); - const css = code.replace(/\n/g, ' ').trim(); - const minified = css.replace(/ /g, ' '); - return { contents: `export default ${JSON.stringify(minified)};` }; - }); - }, - }; -} - -/** - * Transform connector client script into a HTML file - * @param opts build options - * @returns an esbuild plugin - */ -function clientConnectorPlugin(opts: BuildOptions): Plugin { - return { - name: 'clientConnectorPlugin', - setup(build) { - build.onEnd(async (buildResult) => { - const bundle = buildResult.outputFiles?.find((b) => b.path.endsWith(CONNECTOR_NAME)); - if (!bundle) { - throw "Couldn't find build result!"; - } - let code = Buffer.from(bundle.contents).toString(); - - const tsResults = ts.transpileModule(code, { - compilerOptions: { - target: ts.ScriptTarget.ES5, - }, - }); - - if (tsResults.diagnostics?.length) { - throw new Error(tsResults.diagnostics as any); - } - - code = tsResults.outputText; - code = intro + code + outro; - - if (opts.isProd) { - const { minify } = await import('terser'); - const minifyResults = await minify(code, { - compress: { hoist_vars: true, hoist_funs: true, ecma: 5 }, - format: { ecma: 5 }, - }); - if (minifyResults.code) { - code = minifyResults.code; - } - } - - code = banner + code + footer; - code = code.replace(/__VERSION:STENCIL__/g, opts.version); - return fs.writeFile(bundle.path, code); - }); - }, - }; -} - -/** - * esbuild plugin to support alias of dynamic import. Transforming a path within a dynamic import - * does not seem to be supported yet. - * @see https://github.com/evanw/esbuild/issues/700 - * @returns an esbuild plugin - */ -function serverProcessAliasPlugin(): Plugin { - return { - name: 'serverProcessAlias', - setup(build) { - build.onEnd(async (buildResult) => { - const bundle = getFirstOutputFile(buildResult); - let code = Buffer.from(bundle.contents).toString(); - code = code.replace('await import("@dev-server-process")', '(await import("./server-process.js")).default'); - return fs.writeFile(bundle.path, code); - }); - }, - }; -} - -/** - * The `open` NPM package is build as ESM module and uses ESM runtime features like `import.meta.url`. - * This plugin transforms this into CJS compliant code. - * @returns an esbuild plugin - */ -function esm2CJSPlugin(): Plugin { - return { - name: 'esm2CJS', - setup(build) { - build.onEnd(async (buildResult) => { - const bundle = getFirstOutputFile(buildResult); - let code = Buffer.from(bundle.contents).toString(); - code = code.replace('import_meta.url', 'new (require("url").URL)("file:" + __filename).href'); - return fs.writeFile(bundle.path, code); - }); - }, - }; -} - -/** - * Populates the `content-types-db.json` file with the content types of the `mime-db` package. - * @param opts build options - * @returns an esbuild plugin - */ -function contentTypesPlugin(opts: BuildOptions): Plugin { - return { - name: 'contentTypesPlugin', - setup(build) { - build.onLoad({ filter: /content-types-db\.json$/ }, async () => { - const contents = await createContentTypeData(opts); - return { contents }; - }); - }, - }; -} - -const banner = `Stencil Dev Server Connector __VERSION:STENCIL__ ⚡ - -Stencil Dev Server Connector __VERSION:STENCIL__ ⚡ - -`; diff --git a/scripts/esbuild/helpers/empty.js b/scripts/esbuild/helpers/empty.js deleted file mode 100644 index f053ebf7976..00000000000 --- a/scripts/esbuild/helpers/empty.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/scripts/esbuild/helpers/import-meta-url.js b/scripts/esbuild/helpers/import-meta-url.js deleted file mode 100644 index 7d389f908ea..00000000000 --- a/scripts/esbuild/helpers/import-meta-url.js +++ /dev/null @@ -1 +0,0 @@ -export var import_meta_url = require('url').pathToFileURL(__filename); diff --git a/scripts/esbuild/helpers/jest/jest-environment.js b/scripts/esbuild/helpers/jest/jest-environment.js deleted file mode 100644 index 08832b92c82..00000000000 --- a/scripts/esbuild/helpers/jest/jest-environment.js +++ /dev/null @@ -1,3 +0,0 @@ -const { getCreateJestPuppeteerEnvironment } = require('./index.js'); -const createJestPuppeteerEnvironment = getCreateJestPuppeteerEnvironment(); -module.exports = createJestPuppeteerEnvironment(); diff --git a/scripts/esbuild/helpers/jest/jest-preprocessor.js b/scripts/esbuild/helpers/jest/jest-preprocessor.js deleted file mode 100644 index 1d617f95ca5..00000000000 --- a/scripts/esbuild/helpers/jest/jest-preprocessor.js +++ /dev/null @@ -1,3 +0,0 @@ -const { getJestPreprocessor } = require('./index.js'); -const jestPreprocessor = getJestPreprocessor(); -module.exports = jestPreprocessor; diff --git a/scripts/esbuild/helpers/jest/jest-preset.js b/scripts/esbuild/helpers/jest/jest-preset.js deleted file mode 100644 index 201807a64c2..00000000000 --- a/scripts/esbuild/helpers/jest/jest-preset.js +++ /dev/null @@ -1,2 +0,0 @@ -const { getJestPreset } = require('./index.js'); -module.exports = getJestPreset(); diff --git a/scripts/esbuild/helpers/jest/jest-runner.js b/scripts/esbuild/helpers/jest/jest-runner.js deleted file mode 100644 index be6f9353e10..00000000000 --- a/scripts/esbuild/helpers/jest/jest-runner.js +++ /dev/null @@ -1,3 +0,0 @@ -const { getCreateJestTestRunner } = require('./index.js'); -const createTestRunner = getCreateJestTestRunner(); -module.exports = createTestRunner(); diff --git a/scripts/esbuild/helpers/jest/jest-setuptestframework.js b/scripts/esbuild/helpers/jest/jest-setuptestframework.js deleted file mode 100644 index e798153965b..00000000000 --- a/scripts/esbuild/helpers/jest/jest-setuptestframework.js +++ /dev/null @@ -1,3 +0,0 @@ -const { getJestSetupTestFramework } = require('./index.js'); -const jestSetupTestFramework = getJestSetupTestFramework(); -jestSetupTestFramework(); diff --git a/scripts/esbuild/helpers/lazy-require.js b/scripts/esbuild/helpers/lazy-require.js deleted file mode 100644 index 7b87d85270c..00000000000 --- a/scripts/esbuild/helpers/lazy-require.js +++ /dev/null @@ -1,16 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function _lazyRequire(moduleId) { - return new Proxy( - {}, - { - get(_target, propertyKey) { - const importedModule = require(moduleId); - return Reflect.get(importedModule, propertyKey); - }, - set(_target, propertyKey, value) { - const importedModule = require(moduleId); - return Reflect.set(importedModule, propertyKey, value); - }, - }, - ); -} diff --git a/scripts/esbuild/helpers/path-is-absolute.js b/scripts/esbuild/helpers/path-is-absolute.js deleted file mode 100644 index ee45d9ec57b..00000000000 --- a/scripts/esbuild/helpers/path-is-absolute.js +++ /dev/null @@ -1,3 +0,0 @@ -import { isAbsolute } from 'path'; - -export default isAbsolute; diff --git a/scripts/esbuild/internal-app-data.ts b/scripts/esbuild/internal-app-data.ts deleted file mode 100644 index 59bbf7bcdb9..00000000000 --- a/scripts/esbuild/internal-app-data.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { BuildOptions } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { getBaseEsbuildOptions } from './utils'; - -/** - * Get an object containing ESbuild options to build the internal app data - * file. This function also performs relevant side-effects, like writing a - * `package.json` file to disk. - * - * @param opts build options - * @returns a Promise wrapping an array of ESbuild option objects - */ -export async function getInternalAppDataBundles(opts: BuildOptions): Promise { - const appDataBuildDir = join(opts.buildDir, 'app-data'); - const appDataSrcDir = join(opts.srcDir, 'app-data'); - const outputInternalAppDataDir = join(opts.output.internalDir, 'app-data'); - - await fs.emptyDir(outputInternalAppDataDir); - - // copy @stencil/core/internal/app-data/index.d.ts - await fs.copyFile(join(appDataBuildDir, 'index.d.ts'), join(outputInternalAppDataDir, 'index.d.ts')); - - // write @stencil/core/internal/app-data/package.json - writePkgJson(opts, outputInternalAppDataDir, { - name: '@stencil/core/internal/app-data', - description: 'Used for default app data and build conditionals within builds.', - main: 'index.cjs', - module: 'index.js', - types: 'index.d.ts', - sideEffects: false, - }); - - const appDataBaseOptions: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [join(appDataSrcDir, 'index.ts')], - platform: 'node', - }; - - const appDataESM: ESBuildOptions = { - ...appDataBaseOptions, - format: 'esm', - outfile: join(outputInternalAppDataDir, 'index.js'), - }; - - const appDataCJS: ESBuildOptions = { - ...appDataBaseOptions, - format: 'cjs', - outfile: join(outputInternalAppDataDir, 'index.cjs'), - }; - - return [appDataESM, appDataCJS]; -} diff --git a/scripts/esbuild/internal-app-globals.ts b/scripts/esbuild/internal-app-globals.ts deleted file mode 100644 index 2b2194600b1..00000000000 --- a/scripts/esbuild/internal-app-globals.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { BuildOptions } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { getBaseEsbuildOptions } from './utils'; - -/** - * Get an object containing ESbuild options to build the internal app globals - * file. This function also performs relevant side-effects, like writing a - * `package.json` file to disk. - * - * @param opts build options - * @returns a Promise wrapping an array of ESbuild option objects - */ -export async function getInternalAppGlobalsBundles(opts: BuildOptions): Promise { - const appGlobalsBuildDir = join(opts.buildDir, 'app-globals'); - const appGlobalsSrcDir = join(opts.srcDir, 'app-globals'); - const outputInternalAppDataDir = join(opts.output.internalDir, 'app-globals'); - - await fs.emptyDir(outputInternalAppDataDir); - - // copy @stencil/core/internal/app-globals/index.d.ts - await fs.copyFile(join(appGlobalsBuildDir, 'index.d.ts'), join(outputInternalAppDataDir, 'index.d.ts')); - - // write @stencil/core/internal/app-globals/package.json - writePkgJson(opts, outputInternalAppDataDir, { - name: '@stencil/core/internal/app-globals', - description: 'Used for default app globals.', - main: 'index.js', - module: 'index.js', - sideEffects: false, - }); - - const appGlobalsBaseOptions: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [join(appGlobalsSrcDir, 'index.ts')], - platform: 'node', - }; - - const appGlobalsESM: ESBuildOptions = { - ...appGlobalsBaseOptions, - format: 'esm', - outfile: join(outputInternalAppDataDir, 'index.js'), - }; - - return [appGlobalsESM]; -} diff --git a/scripts/esbuild/internal-platform-client.ts b/scripts/esbuild/internal-platform-client.ts deleted file mode 100644 index 76905adba4c..00000000000 --- a/scripts/esbuild/internal-platform-client.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { BuildOptions as ESBuildOptions, Plugin } from 'esbuild'; -import { replace } from 'esbuild-plugin-replace'; -import fs from 'fs-extra'; -import { glob } from 'glob'; -import { join } from 'path'; - -import { getBanner } from '../utils/banner'; -import { BuildOptions, createReplaceData } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { externalAlias, externalNodeModules, getBaseEsbuildOptions, getEsbuildAliases } from './utils'; - -/** - * Create objects containing ESbuild options for the two bundles which need to - * be written to `internal/client`. This also performs relevant side-effects, - * like clearing out the directory and writing a `package.json` script to disk. - * - * @param opts build options - * @returns an array of ESBuild option objects - */ -export async function getInternalClientBundles(opts: BuildOptions): Promise { - const inputClientDir = join(opts.srcDir, 'client'); - const outputInternalClientDir = join(opts.output.internalDir, 'client'); - const outputInternalClientPolyfillsDir = join(outputInternalClientDir, 'polyfills'); - - await fs.emptyDir(outputInternalClientDir); - await fs.emptyDir(outputInternalClientPolyfillsDir); - - await copyPolyfills(opts, outputInternalClientPolyfillsDir); - - // write @stencil/core/internal/client/package.json - writePkgJson(opts, outputInternalClientDir, { - name: '@stencil/core/internal/client', - description: - 'Stencil internal client platform to be imported by the Stencil Compiler and internal runtime. Breaking changes can and will happen at any time.', - exports: './index.js', - main: './index.js', - type: 'module', - sideEffects: false, - }); - - const internalClientAliases = getEsbuildAliases(); - internalClientAliases['@platform'] = join(inputClientDir, 'index.ts'); - - const clientExternal = externalNodeModules; - - const internalClientBundle: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [join(inputClientDir, 'index.ts')], - format: 'esm', - // we do 'write: false' here because we write the build to disk in our - // `findAndReplaceLoadModule` plugin below - write: false, - outfile: join(outputInternalClientDir, 'index.js'), - platform: 'node', - external: clientExternal, - alias: internalClientAliases, - banner: { - js: getBanner(opts, 'Stencil Client Platform'), - }, - plugins: [ - replace(createReplaceData(opts)), - externalAlias('@app-data', '@stencil/core/internal/app-data'), - externalAlias('@app-globals', '@stencil/core/internal/app-globals'), - externalAlias('@utils/shadow-css', './shadow-css.js'), - findAndReplaceLoadModule(), - ], - }; - - const patchBrowserAliases = getEsbuildAliases(); - - const polyfills = await fs.readdir(join(opts.srcDir, 'client', 'polyfills')); - for (const polyFillFile of polyfills) { - patchBrowserAliases[`polyfills/${polyFillFile}`] = join(opts.srcDir, 'client', 'polyfills'); - } - - const patchBrowserExternal = [...externalNodeModules, '@stencil/core', '@stencil/core/mock-doc']; - - const internalClientPatchBrowserBundle: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [join(inputClientDir, 'client-patch-browser.ts')], - format: 'esm', - outfile: join(outputInternalClientDir, 'patch-browser.js'), - platform: 'node', - external: patchBrowserExternal, - alias: patchBrowserAliases, - banner: { - js: getBanner(opts, 'Stencil Client Patch Browser'), - }, - plugins: [ - replace(createReplaceData(opts)), - externalAlias('@platform', '@stencil/core'), - externalAlias('@app-data', '@stencil/core/internal/app-data'), - externalAlias('@app-globals', '@stencil/core/internal/app-globals'), - ], - }; - - return [internalClientBundle, internalClientPatchBrowserBundle]; -} - -/** - * We need to manually find-and-replace a bit of code in - * `client-load-module.ts` in order to prevent Esbuild from analyzing / - * transforming the input by ensuring it does not start with `"./"`. However - * some _other_ bundlers will _not_ work with such an import if it _lacks_ a - * leading `"./"`, so we thus we have to do a little dance where we manually - * replace it here after it's been run through Esbuild. - * - * @returns an Esbuild plugin - */ -export function findAndReplaceLoadModule(): Plugin { - return { - name: 'findAndReplaceLoadModule', - setup(build) { - build.onEnd(async (result) => { - for (const file of result.outputFiles!) { - const { path, text } = file; - - await fs.writeFile(path, text.replace(/\${MODULE_IMPORT_PREFIX}/, './')); - } - }); - }, - }; -} - -async function copyPolyfills(opts: BuildOptions, outputInternalClientPolyfillsDir: string) { - const srcPolyfillsDir = join(opts.srcDir, 'client', 'polyfills'); - - const srcPolyfillFiles = glob.sync('*.js', { cwd: srcPolyfillsDir }); - - await Promise.all( - srcPolyfillFiles.map(async (fileName) => { - const src = join(srcPolyfillsDir, fileName); - const dest = join(outputInternalClientPolyfillsDir, fileName); - await fs.copyFile(src, dest); - }), - ); -} diff --git a/scripts/esbuild/internal-platform-hydrate.ts b/scripts/esbuild/internal-platform-hydrate.ts deleted file mode 100644 index c92cdf5eb94..00000000000 --- a/scripts/esbuild/internal-platform-hydrate.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { getBanner } from '../utils/banner'; -import { bundleDts } from '../utils/bundle-dts'; -import { BuildOptions } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { externalAlias, externalNodeModules, getBaseEsbuildOptions, getEsbuildAliases } from './utils'; - -/** - * Create objects containing ESbuild options for the two bundles comprising - * the hydrate platform. This also performs relevant side-effects, like - * clearing out a directory and writing a `package.json` script to disk. - * - * @param opts build options - * @returns an array of ESBuild option objects - */ -export async function getInternalPlatformHydrateBundles(opts: BuildOptions): Promise { - const inputHydrateDir = join(opts.buildDir, 'hydrate'); - const hydrateSrcDir = join(opts.srcDir, 'hydrate'); - const outputInternalHydrateDir = join(opts.output.internalDir, 'hydrate'); - - await fs.emptyDir(outputInternalHydrateDir); - - // write @stencil/core/internal/hydrate/package.json - writePkgJson(opts, outputInternalHydrateDir, { - name: '@stencil/core/internal/hydrate', - description: - 'Stencil internal hydrate platform to be imported by the Stencil Compiler. Breaking changes can and will happen at any time.', - main: 'index.js', - }); - - await createHydrateRunnerDtsBundle(opts, inputHydrateDir, outputInternalHydrateDir); - - const hydratePlatformInput = join(hydrateSrcDir, 'platform', 'index.js'); - - const external = externalNodeModules; - - const internalHydrateAliases = getEsbuildAliases(); - internalHydrateAliases['@platform'] = hydratePlatformInput; - - const internalHydratePlatformBundle: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [hydratePlatformInput], - format: 'esm', - platform: 'node', - outfile: join(outputInternalHydrateDir, 'index.js'), - external, - alias: internalHydrateAliases, - banner: { - js: getBanner(opts, 'Stencil Hydrate Platform'), - }, - plugins: [ - externalAlias('@utils/shadow-css', '../client/shadow-css.js'), - externalAlias('@app-data', '@stencil/core/internal/app-data'), - externalAlias('@app-globals', '@stencil/core/internal/app-globals'), - ], - }; - - const internalHydrateRunnerBundle: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [join(hydrateSrcDir, 'runner', 'index.js')], - external, - format: 'esm', - platform: 'node', - outfile: join(outputInternalHydrateDir, 'runner.js'), - banner: { - js: getBanner(opts, 'Stencil Hydrate Runner'), - }, - plugins: [ - externalAlias('@utils/shadow-css', '../client/shadow-css.js'), - externalAlias('@app-data', '@stencil/core/internal/app-data'), - externalAlias('@hydrate-factory', '@stencil/core/hydrate-factory'), - ], - }; - - return [internalHydratePlatformBundle, internalHydrateRunnerBundle]; -} - -async function createHydrateRunnerDtsBundle(opts: BuildOptions, inputHydrateDir: string, outputDir: string) { - // bundle @stencil/core/internal/hydrate/runner.d.ts - const dtsEntry = join(inputHydrateDir, 'runner', 'index.d.ts'); - const dtsContent = await bundleDts(opts, dtsEntry); - - const outputPath = join(outputDir, 'runner.d.ts'); - await fs.writeFile(outputPath, dtsContent); -} diff --git a/scripts/esbuild/internal-platform-testing.ts b/scripts/esbuild/internal-platform-testing.ts deleted file mode 100644 index 0d7152eef2f..00000000000 --- a/scripts/esbuild/internal-platform-testing.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { BuildOptions } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { externalAlias, externalNodeModules, getBaseEsbuildOptions, getEsbuildAliases } from './utils'; - -/** - * Get an ESBuild configuration object for the internal testing bundle. This - * function also has side-effects which set things up for the bundle to be built - * correctly, like writing a `package.json` file to disk. - * - * @param opts build options - * @returns a promise wrapping an object holding options for ESBuild - */ -export async function getInternalTestingBundle(opts: BuildOptions): Promise { - const inputTestingPlatform = join(opts.srcDir, 'testing', 'platform', 'index.ts'); - const outputTestingPlatformDir = join(opts.output.internalDir, 'testing'); - - await fs.emptyDir(outputTestingPlatformDir); - - // write @stencil/core/internal/testing/package.json - writePkgJson(opts, outputTestingPlatformDir, { - name: '@stencil/core/internal/testing', - description: - 'Stencil internal testing platform to be imported by the Stencil Compiler. Breaking changes can and will happen at any time.', - main: 'index.js', - }); - - // Copy JSX runtime files for automatic JSX transform support - const srcJsxDir = join(opts.srcDir, 'internal', 'testing'); - const jsxFiles = ['jsx-runtime.js', 'jsx-runtime.d.ts', 'jsx-dev-runtime.js', 'jsx-dev-runtime.d.ts']; - await Promise.all(jsxFiles.map((file) => fs.copyFile(join(srcJsxDir, file), join(outputTestingPlatformDir, file)))); - - const internalTestingAliases = { - ...getEsbuildAliases(), - '@platform': inputTestingPlatform, - '@stencil/core/mock-doc': '../../mock-doc/index.cjs', - }; - - const external: string[] = [...externalNodeModules, '../../mock-doc/index.cjs']; - - const internalTestingBuildOptions: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [inputTestingPlatform], - bundle: true, - format: 'cjs', - outfile: join(outputTestingPlatformDir, 'index.js'), - platform: 'node', - logLevel: 'info', - external, - alias: internalTestingAliases, - plugins: [ - externalAlias('@app-data', '@stencil/core/internal/app-data'), - externalAlias('@utils/shadow-css', '../client/shadow-css.js'), - ], - }; - return internalTestingBuildOptions; -} diff --git a/scripts/esbuild/internal.ts b/scripts/esbuild/internal.ts deleted file mode 100644 index 40b305e117b..00000000000 --- a/scripts/esbuild/internal.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { generateDtsBundle } from 'dts-bundle-generator'; -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { bundleDts, cleanDts } from '../utils/bundle-dts'; -import type { BuildOptions } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { getInternalAppDataBundles } from './internal-app-data'; -import { getInternalAppGlobalsBundles } from './internal-app-globals'; -import { getInternalClientBundles } from './internal-platform-client'; -import { getInternalPlatformHydrateBundles } from './internal-platform-hydrate'; -import { getInternalTestingBundle } from './internal-platform-testing'; -import { getBaseEsbuildOptions, runBuilds } from './utils'; - -/** - * Run the build for the `internal/` directory, copying and modifying files - * as-needed while also creating and then building the various bundles that need - * to be written to `internal/`. - * - * @param opts Build options for the current build - * @returns a Promise wrapping the state of the build - */ -export async function buildInternal(opts: BuildOptions) { - const inputInternalDir = join(opts.buildDir, 'internal'); - - await fs.emptyDir(opts.output.internalDir); - - await copyStencilInternalDts(opts, opts.output.internalDir); - - await copyUtilsDtsFiles(opts); - - await copyStencilCoreEntry(opts); - - // copy @stencil/core/internal default entry, which defaults to client - // but we're not exposing all of Stencil's internal code (only the types) - await fs.copyFile(join(inputInternalDir, 'default.js'), join(opts.output.internalDir, 'index.js')); - - // write @stencil/core/internal/package.json - writePkgJson(opts, opts.output.internalDir, { - name: '@stencil/core/internal', - description: - 'Stencil internals only to be imported by the Stencil Compiler. Breaking changes can and will happen at any time.', - main: 'index.js', - types: 'index.d.ts', - sideEffects: false, - }); - - // this is used in several of our bundles, so we bundle it here in one spot - const shadowCSSBundle: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [join(opts.srcDir, 'utils', 'shadow-css.ts')], - format: 'esm', - outfile: join(opts.output.internalDir, 'client', 'shadow-css.js'), - platform: 'node', - }; - - const clientPlatformBundles = await getInternalClientBundles(opts); - const hydratePlatformBundles = await getInternalPlatformHydrateBundles(opts); - const appDataBundles = await getInternalAppDataBundles(opts); - const appGlobalsBundles = await getInternalAppGlobalsBundles(opts); - const internalTestingBundle = await getInternalTestingBundle(opts); - - return runBuilds( - [ - shadowCSSBundle, - ...clientPlatformBundles, - ...hydratePlatformBundles, - internalTestingBundle, - ...appDataBundles, - ...appGlobalsBundles, - ], - opts, - ); -} - -async function copyStencilCoreEntry(opts: BuildOptions) { - // write @stencil/core entry - const stencilCoreSrcDir = join(opts.srcDir, 'internal', 'stencil-core'); - const stencilCoreDstDir = join(opts.output.internalDir, 'stencil-core'); - await fs.ensureDir(stencilCoreDstDir); - await fs.copy(stencilCoreSrcDir, stencilCoreDstDir); -} - -/** - * Copy `.d.ts` files built from `src/utils` to `internal/utils` so that types - * exported from utility modules can be referenced by other typedefs (in - * particular by our declarations). - * - * Some modules within `@utils` incorporate external types which aren't bundled - * so we selectively copy only `.d.ts` files which are 1) standalone and 2) export - * a type that other modules in the codebase (in, for instance, `src/compiler/` - * or `src/cli/`) depend on. - * - * @param opts options for the rollup build - */ -const copyUtilsDtsFiles = async (opts: BuildOptions) => { - const outputDirPath = join(opts.output.internalDir, 'utils'); - await fs.ensureDir(outputDirPath); - - // copy the `.d.ts` file corresponding to `src/utils/result.ts` - const resultDtsFilePath = join(opts.buildDir, 'utils', 'result.d.ts'); - const resultDtsOutputFilePath = join(opts.output.internalDir, 'utils', 'result.d.ts'); - await fs.copyFile(resultDtsFilePath, resultDtsOutputFilePath); - - const utilsIndexDtsPath = join(opts.output.internalDir, 'utils', 'index.d.ts'); - // here we write a simple module that re-exports `./result` so that imports - // elsewhere like `import { result } from '@utils'` will resolve correctly - await fs.writeFile(utilsIndexDtsPath, `export * as result from "./result"`); -}; - -async function copyStencilInternalDts(opts: BuildOptions, outputInternalDir: string) { - const declarationsInputDir = join(opts.buildDir, 'declarations'); - - // copy to @stencil/core/internal - - // @stencil/core/internal/index.d.ts - const indexDtsSrcPath = join(declarationsInputDir, 'index.d.ts'); - const indexDtsDestPath = join(outputInternalDir, 'index.d.ts'); - let indexDts = cleanDts(await fs.readFile(indexDtsSrcPath, 'utf8')); - indexDts = prependExtModules(indexDts); - await fs.writeFile(indexDtsDestPath, indexDts); - - // @stencil/core/internal/stencil-private.d.ts - const privateDtsSrcPath = join(declarationsInputDir, 'stencil-private.d.ts'); - const privateDtsDestPath = join(outputInternalDir, 'stencil-private.d.ts'); - let privateDts = cleanDts(await fs.readFile(privateDtsSrcPath, 'utf8')); - - // @stencil/core/internal/child_process.d.ts - const childProcessSrcPath = join(declarationsInputDir, 'child_process.d.ts'); - const childProcessDestPath = join(outputInternalDir, 'child_process.d.ts'); - - // we generate a tiny tiny bundle here of just - // `src/declarations/child_process.ts` so that `internal/stencil-private.d.ts` - // can import from `'./child_process'` without worrying about resolving the - // types from `node_modules`. - const childProcessDts = generateDtsBundle([ - { - filePath: childProcessSrcPath, - libraries: { - // we need to mark this library so that types imported from it are inlined - inlinedLibraries: ['child_process'], - }, - output: { - noBanner: true, - exportReferencedTypes: false, - }, - }, - ]).join('\n'); - await fs.writeFile(childProcessDestPath, childProcessDts); - - // the private `.d.ts` imports the `Result` type from the `@utils` module, so - // we need to rewrite the path so it imports from the right relative path - privateDts = privateDts.replace('@utils', './utils'); - await fs.writeFile(privateDtsDestPath, privateDts); - - // @stencil/core/internal/stencil-public.compiler.d.ts - const compilerDtsSrcPath = join(declarationsInputDir, 'stencil-public-compiler.d.ts'); - const compilerDtsDestPath = join(outputInternalDir, 'stencil-public-compiler.d.ts'); - const compilerDts = cleanDts(await fs.readFile(compilerDtsSrcPath, 'utf8')); - await fs.writeFile(compilerDtsDestPath, compilerDts); - - // @stencil/core/internal/stencil-public-docs.d.ts - const docsDtsSrcPath = join(declarationsInputDir, 'stencil-public-docs.d.ts'); - const docsDtsDestPath = join(outputInternalDir, 'stencil-public-docs.d.ts'); - // We bundle with `dts-bundle-generator` here to ensure that when the `docs-json` - // OT writes a `docs.d.ts` file based on this file it is fully portable. - const docsDts = await bundleDts(opts, docsDtsSrcPath, { - // we want to suppress the `dts-bundle-generator` banner here because we do - // our own later on - noBanner: true, - // we also don't want the types which are inlined into our bundled file to - // be re-exported, which will change the 'surface' of the module - exportReferencedTypes: false, - }); - await fs.writeFile(docsDtsDestPath, docsDts); - - // @stencil/core/internal/stencil-public-runtime.d.ts - const runtimeDtsSrcPath = join(declarationsInputDir, 'stencil-public-runtime.d.ts'); - const runtimeDtsDestPath = join(outputInternalDir, 'stencil-public-runtime.d.ts'); - const runtimeDts = cleanDts(await fs.readFile(runtimeDtsSrcPath, 'utf8')); - await fs.writeFile(runtimeDtsDestPath, runtimeDts); - - // @stencil/core/internal/stencil-ext-modules.d.ts (.svg/.css) - const srcExtModuleOutput = join(opts.srcDir, 'declarations', 'stencil-ext-modules.d.ts'); - const dstExtModuleOutput = join(outputInternalDir, 'stencil-ext-modules.d.ts'); - await fs.copyFile(srcExtModuleOutput, dstExtModuleOutput); -} - -function prependExtModules(content: string) { - return `/// \n` + content; -} diff --git a/scripts/esbuild/mock-doc.ts b/scripts/esbuild/mock-doc.ts deleted file mode 100644 index 8ce0718e945..00000000000 --- a/scripts/esbuild/mock-doc.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { getBanner } from '../utils/banner'; -import { BuildOptions, createReplaceData } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { getBaseEsbuildOptions, getEsbuildAliases, runBuilds } from './utils'; -import { bundleParse5 } from './utils/parse5'; - -/** - * Use esbuild to bundle the `mock-doc` submodule - * - * @param opts build options - * @returns a promise for this bundle's build output - */ -export async function buildMockDoc(opts: BuildOptions) { - const inputDir = join(opts.buildDir, 'mock-doc'); - const srcDir = join(opts.srcDir, 'mock-doc'); - const outputDir = opts.output.mockDocDir; - - // clear out rollup stuff and ensure directory exists - await fs.emptyDir(outputDir); - - await bundleMockDocDts(inputDir, outputDir); - - writePkgJson(opts, outputDir, { - name: '@stencil/core/mock-doc', - description: 'Mock window, document and DOM outside of a browser environment.', - main: 'index.cjs', - module: 'index.js', - types: 'index.d.ts', - sideEffects: false, - }); - - // we need to call `createReplaceData` here not because we plan to use the - // replace data in this bundle but because the function has some side-effects - // that we need here. in particular, it sets the version of `parse5` on - // `opts` and the `bundleParse5` function has an implicit dependency on this - // value being already set. - createReplaceData(opts); - - const mockDocAliases = getEsbuildAliases(); - - const [, parse5Path] = await bundleParse5(opts); - mockDocAliases['parse5'] = parse5Path; - - const mockDocBuildOptions: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [join(srcDir, 'index.ts')], - bundle: true, - alias: mockDocAliases, - logLevel: 'info', - }; - - const esmOptions: ESBuildOptions = { - ...mockDocBuildOptions, - format: 'esm', - outfile: join(outputDir, 'index.js'), - banner: { js: getBanner(opts, `Stencil Mock Doc`, true) }, - }; - - const cjsOptions: ESBuildOptions = { - ...mockDocBuildOptions, - format: 'cjs', - outfile: join(outputDir, 'index.cjs'), - banner: { js: getBanner(opts, `Stencil Mock Doc (CommonJS)`, true) }, - }; - - return runBuilds([esmOptions, cjsOptions], opts); -} - -async function bundleMockDocDts(inputDir: string, outputDir: string) { - // only reason we can do this is because we already know the shape - // of mock-doc's dts files and how we want them to come together - const srcDtsFiles = (await fs.readdir(inputDir)).filter((f) => { - return f.endsWith('.d.ts') && !f.endsWith('index.d.ts') && !f.endsWith('index.d.ts-bundled.d.ts'); - }); - - const output = await Promise.all( - srcDtsFiles.map((inputDtsFile) => { - return getDtsContent(inputDir, inputDtsFile); - }), - ); - - const srcIndexDts = await fs.readFile(join(inputDir, 'index.d.ts'), 'utf8'); - output.push(getMockDocExports(srcIndexDts)); - - await fs.writeFile(join(outputDir, 'index.d.ts'), output.join('\n') + '\n'); -} - -async function getDtsContent(inputDir: string, inputDtsFile: string) { - const srcDtsText = await fs.readFile(join(inputDir, inputDtsFile), 'utf8'); - const allLines = srcDtsText.split('\n'); - - const filteredLines = allLines.filter((ln) => { - if (ln.trim().startsWith('///')) { - return false; - } - if (ln.trim().startsWith('import ')) { - return false; - } - if (ln.trim().startsWith('__')) { - return false; - } - if (ln.trim().startsWith('private')) { - return false; - } - if (ln.replace(/ /g, '').startsWith('export{}')) { - return false; - } - return true; - }); - - let dtsContent = filteredLines - .map((ln) => { - if (ln.trim().startsWith('export ')) { - ln = ln.replace('export ', ''); - } - return ln; - }) - .join('\n') - .trim(); - - dtsContent = dtsContent.replace(/ /g, ' '); - - return dtsContent; -} - -function getMockDocExports(srcIndexDts: string) { - const exportLines = srcIndexDts.split('\n').filter((ln) => ln.trim().startsWith('export {')); - const dtsExports: string[] = []; - - exportLines.forEach((ln) => { - const splt = ln.split('{')[1].split('}')[0].trim(); - const exportNames = splt - .split(',') - .map((n) => n.trim()) - .filter((n) => n.length > 0); - dtsExports.push(...exportNames); - }); - - return `export { ${dtsExports.sort().join(', ')} }`; -} diff --git a/scripts/esbuild/screenshot.ts b/scripts/esbuild/screenshot.ts deleted file mode 100644 index 749b60594a3..00000000000 --- a/scripts/esbuild/screenshot.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { getBanner } from '../utils/banner'; -import { BuildOptions } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { externalNodeModules, getBaseEsbuildOptions, getEsbuildAliases, runBuilds } from './utils'; - -const screenshotBuilds = { - 'Stencil Screenshot': 'index', - 'Stencil Screenshot Pixel Match': 'pixel-match', -}; - -export async function buildScreenshot(opts: BuildOptions) { - const inputScreenshotDir = join(opts.buildDir, 'screenshot'); - const inputScreenshotSrcDir = join(opts.srcDir, 'screenshot'); - - // copy @stencil/core/screenshot/index.d.ts - await fs.copy(inputScreenshotDir, opts.output.screenshotDir, { - filter: (f) => { - if (f.endsWith('.d.ts')) { - return true; - } - try { - return fs.statSync(f).isDirectory(); - } catch (e) {} - return false; - }, - }); - - // write @stencil/core/screenshot/package.json - writePkgJson(opts, opts.output.screenshotDir, { - description: 'Stencil Screenshot.', - files: ['compare/', 'index.js', 'connector.js', 'local-connector.js', 'pixel-match.js'], - main: 'index.js', - name: '@stencil/core/screenshot', - types: 'index.d.ts', - }); - - const aliases = getEsbuildAliases(); - const external = externalNodeModules; - const baseScreenshotOptions = { - ...getBaseEsbuildOptions(), - alias: aliases, - external, - format: 'cjs', - platform: 'node', - } satisfies ESBuildOptions; - - return runBuilds( - Object.entries(screenshotBuilds).map( - ([label, file]) => - ({ - ...baseScreenshotOptions, - banner: { - js: getBanner(opts, label), - }, - entryPoints: [join(inputScreenshotSrcDir, `${file}.ts`)], - outfile: join(opts.output.screenshotDir, `${file}.js`), - }) satisfies ESBuildOptions, - ), - opts, - ); -} diff --git a/scripts/esbuild/sys-node.ts b/scripts/esbuild/sys-node.ts deleted file mode 100644 index a58c1090ed2..00000000000 --- a/scripts/esbuild/sys-node.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { BuildOptions as ESBuildOptions } from 'esbuild'; -import fs from 'fs-extra'; -import path from 'path'; -import resolve from 'resolve'; -import webpack, { Configuration } from 'webpack'; - -import { getBanner } from '../utils/banner'; -import type { BuildOptions } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { externalAlias, externalNodeModules, getBaseEsbuildOptions, getEsbuildAliases, runBuilds } from './utils'; - -export async function buildSysNode(opts: BuildOptions) { - const inputDir = path.join(opts.buildDir, 'sys', 'node'); - const srcDir = path.join(opts.srcDir, 'sys', 'node'); - const inputFile = path.join(srcDir, 'index.ts'); - const outputFile = path.join(opts.output.sysNodeDir, 'index.js'); - - // clear out rollup stuff and ensure directory exists - await fs.emptyDir(opts.output.sysNodeDir); - - // create public d.ts - let dts = await fs.readFile(path.join(inputDir, 'public.d.ts'), 'utf8'); - dts = dts.replace('@stencil/core/internal', '../../internal/index'); - await fs.writeFile(path.join(opts.output.sysNodeDir, 'index.d.ts'), dts); - - // write @stencil/core/sys/node/package.json - writePkgJson(opts, opts.output.sysNodeDir, { - name: '@stencil/core/sys/node', - description: 'Stencil Node System.', - main: 'index.js', - types: 'index.d.ts', - }); - - const external = [ - ...externalNodeModules, - // normally you wouldn't externalize your "own" directory here, but since - // we build multiple things within `opts.output.sysNodeDir` which should - // externalize each other we need to do so - '../../compiler/stencil.js', - '../../sys/node/index.js', - './glob.js', - ]; - - const sysNodeAliases = { - ...getEsbuildAliases(), - '@stencil/core/compiler': '../../compiler/stencil.js', - '@sys-api-node': '../../sys/node/index.js', - }; - - const sysNodeOptions: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [inputFile], - bundle: true, - format: 'cjs', - outfile: outputFile, - platform: 'node', - logLevel: 'info', - external, - minify: true, - alias: sysNodeAliases, - banner: { js: getBanner(opts, `Stencil Node System`, true) }, - plugins: [externalAlias('graceful-fs', './graceful-fs.js')], - }; - - // sys/node/worker.js bundle - const inputWorkerFile = path.join(srcDir, 'worker.ts'); - const outputWorkerFile = path.join(opts.output.sysNodeDir, 'worker.js'); - - const workerOptions: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [inputWorkerFile], - bundle: true, - format: 'cjs', - outfile: outputWorkerFile, - platform: 'node', - logLevel: 'info', - external, - minify: true, - alias: sysNodeAliases, - banner: { js: getBanner(opts, `Stencil Node System Worker`, true) }, - }; - - await sysNodeExternalBundles(opts); - - return runBuilds([sysNodeOptions, workerOptions], opts); -} - -export const sysNodeBundleCacheDir = 'sys-node-bundle-cache'; -async function sysNodeExternalBundles(opts: BuildOptions) { - const cachedDir = path.join(opts.scriptsBuildDir, sysNodeBundleCacheDir); - - await fs.ensureDir(cachedDir); - await Promise.all([ - bundleExternal(opts, opts.output.sysNodeDir, cachedDir, 'autoprefixer.js'), - bundleExternal(opts, opts.output.sysNodeDir, cachedDir, 'glob.js'), - bundleExternal(opts, opts.output.sysNodeDir, cachedDir, 'graceful-fs.js'), - bundleExternal(opts, opts.output.sysNodeDir, cachedDir, 'node-fetch.js'), - bundleExternal(opts, opts.output.sysNodeDir, cachedDir, 'prompts.js'), - ]); - - /** - * Some of globs dependencies are using imports with a `node:` prefix which - * is not supported by Jest v26. This is a workaround to remove the `node:`. - * TODO(STENCIL-1323): remove once we deprecated Jest v26 support - */ - const globOutputPath = path.join(opts.output.sysNodeDir, 'glob.js'); - const glob = fs.readFileSync(globOutputPath, 'utf8'); - fs.writeFileSync(globOutputPath, glob.replace(/require\("node:/g, 'require("')); -} - -export function bundleExternal(opts: BuildOptions, outputDir: string, cachedDir: string, entryFileName: string) { - return new Promise(async (resolveBundle, rejectBundle) => { - const outputFile = path.join(outputDir, entryFileName); - const cachedFile = path.join(cachedDir, entryFileName) + (opts.isProd ? '.min.js' : ''); - - const cachedExists = fs.existsSync(cachedFile); - if (cachedExists) { - await fs.copyFile(cachedFile, outputFile); - resolveBundle(); - return; - } - - const whitelist = new Set(['child_process', 'os', 'typescript']); - const webpackConfig: Configuration = { - entry: path.join(opts.srcDir, 'sys', 'node', 'bundles', entryFileName), - output: { - path: outputDir, - filename: entryFileName, - libraryTarget: 'commonjs', - }, - target: 'node', - node: { - __dirname: false, - __filename: false, - }, - externals(data, callback) { - const { request } = data; - - if (request?.match(/^(\.{0,2})\//)) { - // absolute and relative paths are not externals - return callback(null, undefined); - } - - if (request === '@stencil/core/mock-doc') { - return callback(null, '../../mock-doc'); - } - - if (typeof request === 'string' && whitelist.has(request)) { - // we specifically do not want to bundle these imports - resolve.sync(request); - return callback(null, request); - } - - // bundle this import - callback(undefined, undefined); - }, - resolve: { - alias: { - '@utils': path.join(opts.buildDir, 'utils', 'index.js'), - postcss: path.join(opts.nodeModulesDir, 'postcss'), - 'source-map': path.join(opts.nodeModulesDir, 'source-map'), - chalk: path.join(opts.bundleHelpersDir, 'empty.js'), - }, - }, - optimization: { - minimize: false, - }, - mode: 'production', - }; - - console.log(`[sys-node] bundleExternal ${entryFileName} via webpack`); - webpack(webpackConfig, async (err, stats) => { - try { - console.log(`[sys-node] bundleExternal ${entryFileName} success, err: ${err}, stats: ${stats}`); - const { minify } = await import('terser'); - if (err && err.message) { - rejectBundle(err); - } else if (stats) { - const info = stats.toJson({ errors: true }); - if (stats.hasErrors() && info && info.errors) { - const webpackError = info.errors.join('\n'); - rejectBundle(webpackError); - } else { - let code = await fs.readFile(outputFile, 'utf8'); - - if (opts.isProd) { - try { - const minifyResults = await minify(code); - if (minifyResults.code) { - code = minifyResults.code; - } - } catch (e) { - rejectBundle(e); - return; - } - } - await fs.writeFile(cachedFile, code); - await fs.writeFile(outputFile, code); - - resolveBundle(); - } - } - } catch (e) { - rejectBundle(e); - } - }); - }); -} diff --git a/scripts/esbuild/testing.ts b/scripts/esbuild/testing.ts deleted file mode 100644 index bee3fafcb9f..00000000000 --- a/scripts/esbuild/testing.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { BuildOptions as ESBuildOptions, Plugin } from 'esbuild'; -import fs from 'fs-extra'; -import path from 'path'; - -import { getBanner } from '../utils/banner'; -import type { BuildOptions } from '../utils/options'; -import { writePkgJson } from '../utils/write-pkg-json'; -import { - externalAlias, - externalNodeModules, - getBaseEsbuildOptions, - getEsbuildAliases, - getFirstOutputFile, - runBuilds, -} from './utils'; - -const EXTERNAL_TESTING_MODULES = [ - 'constants', - 'rollup', - '@rollup/plugin-commonjs', - '@rollup/plugin-node-resolve', - 'yargs', - 'zlib', -]; - -export async function buildTesting(opts: BuildOptions) { - const inputDir = path.join(opts.buildDir, 'testing'); - const sourceDir = path.join(opts.srcDir, 'testing'); - await fs.emptyDir(opts.output.testingDir); - - await Promise.all([ - // copy jest testing entry files - fs.copy(path.join(opts.scriptsBundlesDir, 'helpers', 'jest'), opts.output.testingDir), - copyTestingInternalDts(opts, inputDir), - ]); - - // write package.json - writePkgJson(opts, opts.output.testingDir, { - name: '@stencil/core/testing', - description: 'Stencil testing suite.', - main: 'index.js', - types: 'index.d.ts', - }); - - const external = [ - ...EXTERNAL_TESTING_MODULES, - ...externalNodeModules, - '../internal/testing/*', - '../cli/index.cjs', - '../sys/node/index.js', - '../compiler/stencil.js', - ]; - - const aliases = getEsbuildAliases(); - const testingEsbuildOptions: ESBuildOptions = { - ...getBaseEsbuildOptions(), - entryPoints: [path.join(sourceDir, 'index.ts')], - bundle: true, - format: 'cjs', - outfile: path.join(opts.output.testingDir, 'index.js'), - platform: 'node', - logLevel: 'info', - external, - /** - * set `write: false` so that we can run the `onEnd` hook - * in `lazyRequirePlugin` and modify the imports - */ - write: false, - alias: aliases, - banner: { js: getBanner(opts, `Stencil Testing`, true) }, - plugins: [ - externalAlias('@app-data', '@stencil/core/internal/app-data'), - externalAlias('@platform', '@stencil/core/internal/testing'), - externalAlias('../internal/testing/index.js', '@stencil/core/internal/testing'), - externalAlias('@stencil/core/dev-server', '../dev-server/index.js'), - externalAlias('@stencil/core/mock-doc', '../mock-doc/index.cjs'), - lazyRequirePlugin(opts, [ - '@stencil/core/internal/app-data', - '@stencil/core/internal/testing', - '../dev-server/index.js', - '../internal/testing/index.js', - '../mock-doc/index.cjs', - ]), - ignorePuppeteerDependency(opts), - ], - }; - - return runBuilds([testingEsbuildOptions], opts); -} - -function getLazyRequireFn(opts: BuildOptions) { - return fs.readFileSync(path.join(opts.bundleHelpersDir, 'lazy-require.js'), 'utf8').trim(); -} - -function lazyRequirePlugin(opts: BuildOptions, moduleIds: string[]): Plugin { - return { - name: 'lazyRequirePlugin', - setup(build) { - build.onEnd(async (buildResult) => { - const bundle = getFirstOutputFile(buildResult); - let code = Buffer.from(bundle.contents).toString(); - - for (const moduleId of moduleIds) { - const str = `require("${moduleId}")`; - while (code.includes(str)) { - code = code.replace(str, `_lazyRequire("${moduleId}")`); - } - } - - code = code.replace(`"use strict";`, `"use strict";\n\n${getLazyRequireFn(opts)}`); - return fs.writeFile(bundle.path, code); - }); - }, - }; -} - -/** - * To avoid having user to install puppeteer for building their app (even if - * they don't use e2e testing), we ignore the puppeteer dependency in the - * generated d.ts file. - * - * @param opts build options - * @returns an ESbuild plugin - */ -function ignorePuppeteerDependency(opts: BuildOptions): Plugin { - return { - name: 'ignorePuppeteerDependency', - setup(build) { - build.onEnd(async () => { - await writePatchedPuppeteerDts(opts); - }); - }, - }; -} - -export async function copyTestingInternalDts(opts: BuildOptions, inputDir: string) { - // copy testing d.ts files - - await fs.copy(path.join(inputDir), path.join(opts.output.testingDir), { - filter: (f) => { - if (f.endsWith('.d.ts')) { - return true; - } - if (fs.statSync(f).isDirectory() && !f.includes('platform')) { - return true; - } - return false; - }, - }); -} - -/** - * Write a patched version of - * `src/testing/puppeteer/puppeteer-declarations.d.ts` which has a `@ts-ignore` - * added to prevent a type-checking error if a Stencil project does not have - * puppeteer installed. - * - * @param opts build options - */ -export async function writePatchedPuppeteerDts(opts: BuildOptions) { - const typeFilePath = path.join(opts.output.testingDir, 'puppeteer', 'puppeteer-declarations.d.ts'); - const updatedFileContent = (await fs.readFile(typeFilePath, 'utf8')) - .split('\n') - .reduce((lines, line) => { - if (line.endsWith(`from 'puppeteer';`)) { - lines.push('// @ts-ignore - avoid requiring puppeteer as dependency'); - } - lines.push(line); - return lines; - }, [] as string[]) - .join('\n'); - await fs.writeFile(typeFilePath, updatedFileContent); -} diff --git a/scripts/esbuild/utils/alias-plugin.ts b/scripts/esbuild/utils/alias-plugin.ts deleted file mode 100644 index 184c27f1512..00000000000 --- a/scripts/esbuild/utils/alias-plugin.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { join } from 'path'; -import type { PartialResolvedId, Plugin } from 'rollup'; - -import type { BuildOptions } from '../../utils/options'; - -/** - * Creates a rollup plugin for resolving identifiers while simultaneously externalizing them - * @param opts the options being used during a build - * @returns a rollup plugin with a build hook for resolving various identifiers - */ -export function aliasPlugin(opts: BuildOptions): Plugin { - const alias = new Map([ - ['@app-data', '@stencil/core/internal/app-data'], - ['@app-globals', '@stencil/core/internal/app-globals'], - ['@hydrate-factory', '@stencil/core/hydrate-factory'], - ['@stencil/core/mock-doc', '@stencil/core/mock-doc'], - ['@stencil/core/testing', '@stencil/core/testing'], - ['@dev-server-process', './server-process.js'], - ]); - - // ensure we use the same one - const helperResolvers = new Set(['is-resolvable', 'path-is-absolute']); - - // ensure we use the same one - const nodeResolvers = new Map([['source-map', join(opts.nodeModulesDir, 'source-map', 'source-map.js')]]); - - const empty = new Set([ - // we never use chalk, but many projects still pull it in - 'chalk', - ]); - - return { - name: 'aliasPlugin', - /** - * A rollup build hook for resolving identifiers. [Source](https://rollupjs.org/guide/en/#resolveid) - * @param id the importee exactly as it is written in an import statement in the source code - * @returns a resolution to an import to a different id, potentially externalizing it from the bundle simultaneously - */ - resolveId(id: string): PartialResolvedId | string | null | undefined { - const externalId = alias.get(id); - if (externalId) { - return { - id: externalId, - external: true, - }; - } - if (id === '@runtime') { - return join(opts.buildDir, 'runtime', 'index.js'); - } - if (id === '@utils') { - return join(opts.buildDir, 'utils', 'index.js'); - } - if (id === '@utils/shadow-css') { - return join(opts.buildDir, 'utils', 'shadow-css.js'); - } - if (id === '@environment') { - return join(opts.buildDir, 'compiler', 'sys', 'environment.js'); - } - if (id === '@sys-api-node') { - return join(opts.buildDir, 'sys', 'node', 'index.js'); - } - if (id === '@stencil/core/cli') { - return join(opts.buildDir, 'cli', 'index.js'); - } - if (helperResolvers.has(id)) { - return join(opts.bundleHelpersDir, `${id}.js`); - } - if (empty.has(id)) { - return join(opts.bundleHelpersDir, 'empty.js'); - } - if (nodeResolvers.has(id)) { - return nodeResolvers.get(id); - } - return null; - }, - }; -} diff --git a/scripts/esbuild/utils/content-types.ts b/scripts/esbuild/utils/content-types.ts deleted file mode 100644 index 2c83463f6b9..00000000000 --- a/scripts/esbuild/utils/content-types.ts +++ /dev/null @@ -1,36 +0,0 @@ -import fs from 'fs-extra'; -import { join } from 'path'; - -import type { BuildOptions } from '../../utils/options'; - -export async function createContentTypeData(opts: BuildOptions) { - // create a focused content-type lookup object from - // the mime db json file - const mimeDbSrcPath = join(opts.nodeModulesDir, 'mime-db', 'db.json'); - const mimeDbJson = await fs.readJson(mimeDbSrcPath); - - const extData: { ext: string; mimeType: string }[] = []; - - Object.keys(mimeDbJson).forEach((mimeType) => { - const mimeTypeData = mimeDbJson[mimeType]; - if (Array.isArray(mimeTypeData.extensions)) { - mimeTypeData.extensions.forEach((ext: string) => { - extData.push({ - ext, - mimeType, - }); - }); - } - }); - - const exts: Record = {}; - extData - .sort((a, b) => { - if (a.ext < b.ext) return -1; - if (a.ext > b.ext) return 1; - return 0; - }) - .forEach((x: any) => (exts[x.ext] = x.mimeType)); - - return `export default ${JSON.stringify(exts)}`; -} diff --git a/scripts/esbuild/utils/index.ts b/scripts/esbuild/utils/index.ts deleted file mode 100644 index cebf2edb7f0..00000000000 --- a/scripts/esbuild/utils/index.ts +++ /dev/null @@ -1,149 +0,0 @@ -import type { BuildOptions as ESBuildOptions, BuildResult as ESBuildResult, OutputFile, Plugin } from 'esbuild'; -import { build, context } from 'esbuild'; - -import { BuildOptions } from '../../utils/options'; - -/** - * Aliases which map the module identifiers we set in `paths` in `tsconfig.json` to - * bundles (built either with esbuild or with rollup). - * - * @returns a map of aliases to bundle entry points, relative to the root directory - */ -export function getEsbuildAliases(): Record { - return { - // node module redirection - chalk: 'ansi-colors', - - // mapping aliases to top-level bundle entrypoints - '@stencil/core/testing': '../testing/index.js', - '@stencil/core/compiler': '../compiler/stencil.js', - '@stencil/core/dev-server': '../dev-server/index.js', - '@stencil/core/mock-doc': '../mock-doc/index.cjs', - '@stencil/core/internal/testing': '../internal/testing/index.js', - '@stencil/core/cli': '../cli/index.cjs', - '@sys-api-node': '../sys/node/index.js', - }; -} - -/** - * Node modules which should be universally marked as external - * - * Note that we should not rely on this to mark node.js built-in modules as - * external. Doing so will override esbuild's automatic marking of those modules - * as side-effect-free, which allows imports from them to be properly - * tree-shaken. - */ -export const externalNodeModules = [ - '@jest/core', - '@jest/reporters', - '@microsoft/typescript-etw', - 'expect', - 'fsevents', - 'jest', - 'jest-cli', - 'jest-config', - 'jest-message-id', - 'jest-pnp-resolver', - 'jest-environment-node', - 'jest-runner', - 'puppeteer', - 'puppeteer-core', - 'yargs', -]; - -/** - * A helper which runs an array of esbuild, uh, _builds_ - * - * This accepts an array of build configurations and will either run a - * synchronous build _or_ run them all in watch mode, depending on the - * {@link BuildOptions['isWatch']} setting. - * - * @param builds the array of outputs to build - * @param opts Stencil build options - * @returns a Promise representing the execution of the builds - */ -export function runBuilds(builds: ESBuildOptions[], opts: BuildOptions): Promise<(void | ESBuildResult)[]> { - if (opts.isWatch) { - return Promise.all( - builds.map(async (buildConfig) => { - const ctx = await context(buildConfig); - return ctx.watch(); - }), - ); - } else { - return Promise.all(builds.map(build)); - } -} - -/** - * Get base esbuild options which we want to always have set for all of our - * bundles - * - * @returns a base set of options - */ -export function getBaseEsbuildOptions(): ESBuildOptions { - const options: ESBuildOptions = { - bundle: true, - legalComments: 'inline', - logLevel: 'info', - target: getEsbuildTargets(), - }; - - // if the `build` sub-command is called with the `DEBUG` env var, like - // - // DEBUG=true npm run build - // - // then we should produce sourcemaps. - if (process.env.DEBUG) { - options.sourcemap = 'linked'; - } - - return options; -} - -/** - * Get build targets with minimal supported browser versions - * @see https://stenciljs.com/docs/support-policy#browser-support - * @returns an array of build targets - */ -export function getEsbuildTargets(): string[] { - return ['node16', 'chrome79', 'edge79', 'firefox70', 'safari14']; -} - -/** - * Alias and mark a module as external at the same time - * - * @param moduleId the module ID to alias and externalize - * @param resolveToPath the path to which imports of the module should be rewritten - * @returns an Esbuild plugin - */ -export function externalAlias(moduleId: string, resolveToPath: string): Plugin { - return { - name: 'externalAliases', - setup(build) { - build.onResolve({ filter: new RegExp(`^${moduleId}$`) }, () => { - return { - path: resolveToPath, - external: true, - }; - }); - }, - }; -} - -/** - * Extract the first {@link OutputFile} record from an Esbuild - * {@link BuildResult}. This _may_ not be present, so in order to guarantee - * type safety this function throws if such an `OutputFile` cannot be found. - * - * @throws if no `OutputFile` can be found. - * @param buildResult the Esbuild build result in which to look - * @returns the OutputFile - */ -export function getFirstOutputFile(buildResult: ESBuildResult): OutputFile { - const bundle = buildResult.outputFiles?.[0]; - if (!bundle) { - throw new Error('Could not find an output file in the BuildResult!'); - } - return bundle; -} diff --git a/scripts/esbuild/utils/parse5.ts b/scripts/esbuild/utils/parse5.ts deleted file mode 100644 index 246b05e814b..00000000000 --- a/scripts/esbuild/utils/parse5.ts +++ /dev/null @@ -1,93 +0,0 @@ -import rollupCommonjs from '@rollup/plugin-commonjs'; -import rollupResolve from '@rollup/plugin-node-resolve'; -import fs from 'fs-extra'; -import { join } from 'path'; -import { rollup } from 'rollup'; - -import type { BuildOptions } from '../../utils/options'; -import { aliasPlugin } from './alias-plugin'; - -/** - * Bundles parse5 to be used in the Stencil output. Writes the results to disk and returns its contents. The file - * written to disk may be used as a simple cache to speed up subsequent build times. - * @param opts the options being used during a build of the Stencil compiler - * @returns a tuple holding 1) contents of the file containing parse5 and 2) the file path where it's written - */ -export async function bundleParse5(opts: BuildOptions): Promise<[contents: string, path: string]> { - const fileName = `parse5-${opts.parse5Version.replace(/\./g, '_')}-bundle-cache${opts.isProd ? '.min' : ''}.js`; - const cacheFile = join(opts.scriptsBuildDir, fileName); - - try { - return [await fs.readFile(cacheFile, 'utf8'), cacheFile]; - } catch (e) {} - - const rollupBuild = await rollup({ - input: '@parse5-entry', - plugins: [ - { - name: 'parse5EntryPlugin', - /** - * A rollup build hook for resolving @parse5-entry [Source](https://rollupjs.org/guide/en/#resolveid) - * @param id the importee exactly as it is written in an import statement in the source code - * @returns a string that resolves an import to some id - */ - resolveId(id: string): string | null { - if (id === '@parse5-entry') { - return id; - } - return null; - }, - /** - * A rollup build hook for intercepting how parse5's entry package is processed - * [Source](https://rollupjs.org/guide/en/#load) - * @param id the path of the module to load - * @returns source code to act as a proxy for @parse5-entry - */ - load(id: string): string | null { - if (id === '@parse5-entry') { - return `export { parse, parseFragment } from 'parse5';`; - } - return null; - }, - }, - aliasPlugin(opts), - rollupResolve(), - rollupCommonjs(), - ], - }); - - const { output } = await rollupBuild.generate({ - format: 'iife', - name: 'PARSE5', - footer: ['export const parse = PARSE5.parse;', 'export const parseFragment = PARSE5.parseFragment;'].join('\n'), - strict: false, - }); - - let code = output[0].code; - - const { minify } = await import('terser'); - - if (opts.isProd) { - const minified = await minify(code, { - ecma: 2018, - module: true, - compress: { - ecma: 2018, - passes: 2, - }, - format: { - ecma: 2018, - comments: false, - }, - }); - if (minified.code) { - code = minified.code; - } - } - - code = `// Parse5 ${opts.parse5Version}\n` + code; - - await fs.writeFile(cacheFile, code); - - return [code, cacheFile]; -} diff --git a/scripts/esbuild/utils/terser.ts b/scripts/esbuild/utils/terser.ts deleted file mode 100644 index 532996b93a6..00000000000 --- a/scripts/esbuild/utils/terser.ts +++ /dev/null @@ -1,62 +0,0 @@ -import fs from 'fs-extra'; -import { join } from 'path'; -import { rollup } from 'rollup'; - -import type { BuildOptions } from '../../utils/options'; - -/** - * Creates a bundle containing Terser - * @param opts the options being used during a build - * @returns a tuple containing the bundled Terser code and the path where it - * was written - */ -export async function bundleTerser(opts: BuildOptions): Promise<[content: string, path: string]> { - if (!opts.terserVersion) { - throw new Error('Terser version not set on build opts!'); - } - - const fileName = `terser-${opts.terserVersion.replace(/\./g, '_')}-bundle-cache${opts.isProd ? '.min' : ''}.js`; - const cacheFile = join(opts.scriptsBuildDir, fileName); - - try { - const content = await fs.readFile(cacheFile, 'utf8'); - return [content, cacheFile]; - } catch (e) {} - - const rollupBuild = await rollup({ - input: join(opts.nodeModulesDir, 'terser', 'main.js'), - external: ['source-map'], - }); - - const { output } = await rollupBuild.generate({ - format: 'es', - strict: false, - }); - - let code = output[0].code; - - const { minify } = await import('terser'); - - if (opts.isProd) { - const minified = await minify(code, { - ecma: 2018, - compress: { - ecma: 2018, - passes: 2, - }, - format: { - ecma: 2018, - comments: false, - }, - }); - if (minified.code) { - code = minified.code; - } - } - - code = `// Terser ${opts.terserVersion}\n` + code; - - await fs.writeFile(cacheFile, code); - - return [code, cacheFile]; -} diff --git a/scripts/esbuild/utils/typescript-source.ts b/scripts/esbuild/utils/typescript-source.ts deleted file mode 100644 index f18a5854dfa..00000000000 --- a/scripts/esbuild/utils/typescript-source.ts +++ /dev/null @@ -1,114 +0,0 @@ -import fs from 'fs-extra'; -import { join } from 'path'; - -import type { BuildOptions } from '../../utils/options'; - -/** - * Bundles the TypeScript compiler in the Stencil output. This function also performs several optimizations and - * modifications to the TypeScript source. - * @param tsPath a path to the TypeScript compiler - * @param opts the options being used during a build of the Stencil compiler - * @returns the modified TypeScript source - */ -export async function bundleTypeScriptSource(tsPath: string, opts: BuildOptions): Promise { - const cacheFile = tsCacheFilePath(opts); - - try { - // check if we've already cached this bundle - return await fs.readFile(cacheFile, 'utf8'); - } catch (e) {} - - // get the source typescript.js file to modify - let code = await fs.readFile(tsPath, 'utf8'); - - // As of 5.0, because typescript is now bundled with esbuild the structure of - // the file we're dealing with here (`lib/typescript.js`) has changed. - // Previously there was an iife which got an object as an argument and just - // stuck properties onto it, something like - // - // ```js - // var ts = (function (ts) { - // ts.someMethod = () => { ... }; - // })(ts || ts = {}); - // ``` - // - // as of 5.0 it instead looks (conceptually) something like: - // - // ```js - // var ts = (function () { - // const ts = {} - // const define = (name, value) => { - // Object.defineProperty(ts, name, value, { enumerable: true }) - // } - // define('someMethod', () => { ... }) - // return ts; - // })(); - // ``` - // - // Note that the call to `Object.defineProperty` does not set `configurable` to `true` - // (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description) - // which means that later calls to do something like - // - // ```ts - // import ts from 'typescript'; - // - // ts.someMethod = function myReplacementForSomeMethod () { - // ... - // }; - // ``` - // - // will fail because without `configurable: true` you can't re-assign - // properties. - // - // All well and good, except for the fact that our patching of typescript to - // use for instance the in-memory file system depends on us being able to - // monkey-patch typescript in exactly this way. So in order to retain our - // current approach to patching TypeScript we need to edit this file in order - // to add `configurable: true` to the options passed to - // `Object.defineProperty`: - const TS_PROP_DEFINER = `__defProp(target, name, { get: all[name], enumerable: true });`; - const TS_PROP_DEFINER_RECONFIGURABLE = `__defProp(target, name, { get: all[name], enumerable: true, configurable: true });`; - - code = code.replace(TS_PROP_DEFINER, TS_PROP_DEFINER_RECONFIGURABLE); - - const jestTypesciptFilename = join(opts.scriptsBuildDir, 'typescript-modified-for-jest.js'); - await fs.writeFile(jestTypesciptFilename, code); - - // TODO(STENCIL-839): investigate minification issue w/ typescript 5.0 - // const { minify } = await import('terser'); - - // if (opts.isProd) { - // const minified = await minify(code, { - // ecma: 2018, - // // module: true, - // compress: { - // ecma: 2018, - // passes: 2, - // }, - // format: { - // ecma: 2018, - // comments: false, - // }, - // }); - // code = minified.code; - // } - - await fs.writeFile(cacheFile, code); - - return code; -} - -/** - * Get the file path to which the cached, modified version of TypeScript will - * be written - * - * @param opts build options for the current Stencil build - * @returns the path where the modified TypeScript source can be found - */ -export function tsCacheFilePath(opts: BuildOptions): string { - const fileName = `typescript-${opts.typescriptVersion.replace(/\./g, '_')}-bundle-cache${ - opts.isProd ? '.min' : '' - }.js`; - const cacheFile = join(opts.scriptsBuildDir, fileName); - return cacheFile; -} diff --git a/scripts/index.ts b/scripts/index.ts deleted file mode 100644 index 16fdf493ad8..00000000000 --- a/scripts/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { join } from 'path'; - -import * as build from './build'; - -// This path is relative to the final location of the compiled script, not its TypeScript source -const stencilProjectRoot = join(__dirname, '..'); -const args = process.argv.slice(2); -build.run(stencilProjectRoot, args); diff --git a/scripts/release-tasks.ts b/scripts/release-tasks.ts deleted file mode 100644 index 18f75aba084..00000000000 --- a/scripts/release-tasks.ts +++ /dev/null @@ -1,220 +0,0 @@ -import color from 'ansi-colors'; -import Listr, { ListrTask } from 'listr'; - -import { buildAll } from './build'; -import { BuildOptions } from './utils/options'; -import { isPrereleaseVersion, isValidVersionInput, SEMVER_INCREMENTS, updateChangeLog } from './utils/release-utils'; - -/** - * We have to wrap `execa` in a promise to ensure it works with `Listr`. `Listr` uses rxjs under the hood which - * seems to have issues with `execa`'s `ResultPromise` as it never resolves a task. - * @param command command to run - * @param args arguments to pass to the command - * @param options `execa` options - * @returns a promise that resolves with the stdout and stderr of the command - */ -async function execa(command: string, args: string[], options?: any) { - /** - * consecutive imports are cached and don't have an impact on the execution speed - */ - const { execa: execaOrig } = await import('execa'); - - return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { - const run = execaOrig(command, args, options); - run.then( - ({ stdout, stderr }) => - resolve({ - stdout: stdout as unknown as string, - stderr: stderr as unknown as string, - }), - (err) => reject(err), - ); - }); -} - -/** - * Runs a litany of tasks used to ensure a safe release of a new version of Stencil - * @param opts build options containing the metadata needed to release a new version of Stencil - * @param args stringified arguments used to influence the release steps that are taken - */ -export async function runReleaseTasks(opts: BuildOptions, args: ReadonlyArray): Promise { - const rootDir = opts.rootDir; - const pkg = opts.packageJson; - const tasks: ListrTask[] = []; - const newVersion = opts.version; - const isDryRun = args.includes('--dry-run') || opts.version.includes('dryrun'); - let tagPrefix: string; - - if (isDryRun) { - console.log(color.bold.yellow(`\n 🏃‍ Dry Run!\n`)); - } - - if (!opts.isPublishRelease) { - /** - * For automated and manual releases, always verify that the version provided to the release scripts is a valid - * semver 'word' (e.g. 'major', 'minor', etc.) or version (e.g. 1.0.0) - */ - tasks.push({ - title: 'Validate version', - task: () => { - if (!isValidVersionInput(opts.version)) { - throw new Error(`Version should be either ${SEMVER_INCREMENTS.join(', ')}, or a valid semver version.`); - } - }, - skip: () => isDryRun, - }); - } - - if (opts.isPublishRelease) { - tasks.push({ - title: 'Check for pre-release version', - task: () => { - if (!pkg.private && isPrereleaseVersion(newVersion) && !opts.tag) { - throw new Error( - 'You must specify a dist-tag using --tag when publishing a pre-release version. This prevents accidentally tagging unstable versions as "latest". https://docs.npmjs.com/cli/dist-tag', - ); - } - }, - }); - } - - tasks.push({ - /** - * When we both pre-release and release, it's beneficial to ensure that the tag does not already exist in git. - * Doing so ought to catch out of the ordinary circumstances that ought to be investigated. - */ - title: 'Check git tag existence', - task: () => - execa('git', ['fetch']) - // Retrieve the prefix for a version string - https://docs.npmjs.com/cli/v7/using-npm/config#tag-version-prefix - .then(() => execa('npm', ['config', 'get', 'tag-version-prefix'])) - .then( - ({ stdout }) => (tagPrefix = stdout), - () => {}, - ) - // verify that a tag for the new version string does not already exist by checking the output of - // `git rev-parse --verify` - .then(() => execa('git', ['rev-parse', '--quiet', '--verify', `refs/tags/${tagPrefix}${newVersion}`])) - .then( - ({ stdout }) => { - if (stdout) { - throw new Error(`Git tag \`${tagPrefix}${newVersion}\` already exists.`); - } - }, - (err) => { - // Command fails with code 1 and no output if the tag does not exist, even though `--quiet` is provided - // https://github.com/sindresorhus/np/pull/73#discussion_r72385685 - if (err.stdout !== '' || err.stderr !== '') { - throw err; - } - }, - ), - skip: () => isDryRun, - }); - - tasks.push( - { - title: `Install npm dependencies ${color.dim('(npm ci)')}`, - task: () => execa('npm', ['ci'], { cwd: rootDir }), - // for pre-releases, this step will occur in GitHub after the PR has been created. - // for actual releases, we'll need to build + bundle stencil in order to publish it to npm. - skip: () => !opts.isPublishRelease, - }, - { - title: `Transpile Stencil ${color.dim('(tsc.prod)')}`, - task: () => execa('npm', ['run', 'tsc.prod'], { cwd: rootDir }), - // for pre-releases, this step will occur in GitHub after the PR has been created. - // for actual releases, we'll need to build + bundle stencil in order to publish it to npm. - skip: () => !opts.isPublishRelease, - }, - { - title: `Bundle @stencil/core ${color.dim('(' + opts.buildId + ')')}`, - task: () => buildAll(opts), - // for pre-releases, this step will occur in GitHub after the PR has been created. - // for actual releases, we'll need to build + bundle stencil in order to publish it to npm. - skip: () => !opts.isPublishRelease, - }, - ); - - if (!opts.isPublishRelease) { - tasks.push( - { - title: `Set package.json version to ${color.bold.yellow(opts.version)}`, - task: async () => { - // use `--no-git-tag-version` to ensure that the tag for the release is not prematurely created - await execa('npm', ['version', '--no-git-tag-version', opts.version], { cwd: rootDir }); - }, - }, - { - title: `Generate ${opts.version} Changelog ${opts.vermoji}`, - task: async () => { - await updateChangeLog(opts); - }, - }, - ); - } - - if (opts.isPublishRelease) { - tasks.push( - { - title: 'Publish @stencil/core to npm', - task: () => { - const cmd = 'npm'; - const cmdArgs = ['publish'].concat(opts.tag ? ['--tag', opts.tag] : []).concat(['--provenance']); - - if (isDryRun) { - return console.log(`[dry-run] ${cmd} ${cmdArgs.join(' ')}`); - } - return execa(cmd, cmdArgs, { cwd: rootDir }); - }, - }, - { - title: 'Tagging the latest git commit', - task: () => { - const cmd = 'git'; - const cmdArgs = ['tag', `v${opts.version}`]; - - if (isDryRun) { - return console.log(`[dry-run] ${cmd} ${cmdArgs.join(' ')}`); - } - return execa(cmd, cmdArgs, { cwd: rootDir }); - }, - }, - { - title: 'Pushing git tags', - task: () => { - const cmd = 'git'; - const cmdArgs = ['push', '--tags']; - - if (isDryRun) { - return console.log(`[dry-run] ${cmd} ${cmdArgs.join(' ')}`); - } - return execa(cmd, cmdArgs, { cwd: rootDir }); - }, - }, - ); - } - - const listr = new Listr(tasks); - - try { - await listr.run(); - } catch (err: any) { - console.log(`\n🤒 ${color.red(err)}\n`); - console.log(err); - process.exit(1); - } - if (opts.isPublishRelease) { - console.log( - `\n ${opts.vermoji} ${color.bold.magenta(pkg.name)} ${color.bold.yellow(newVersion)} published!! ${ - opts.vermoji - }\n`, - ); - } else { - console.log( - `\n ${opts.vermoji} ${color.bold.magenta(pkg.name)} ${color.bold.yellow( - newVersion, - )} prepared, check the diffs and commit ${opts.vermoji}\n`, - ); - } -} diff --git a/scripts/release.ts b/scripts/release.ts deleted file mode 100644 index 6c9d0fbbc9b..00000000000 --- a/scripts/release.ts +++ /dev/null @@ -1,127 +0,0 @@ -import color from 'ansi-colors'; -import fs from 'fs-extra'; -import { join } from 'path'; - -import { runReleaseTasks } from './release-tasks'; -import { BuildOptions, getOptions } from './utils/options'; -import { getNewVersion } from './utils/release-utils'; -import { getLatestVermoji } from './utils/vermoji'; - -/** - * Runner for creating a release of Stencil - * @param rootDir the root directory of the Stencil repository - * @param args stringified arguments used to influence the release steps that are taken - * @returns a void promise - */ -export async function release(rootDir: string, args: ReadonlyArray): Promise { - const buildDir = join(rootDir, 'build'); - - if (args.includes('--ci-prepare')) { - await fs.emptyDir(buildDir); - const prepareOpts = getOptions(rootDir, { - isCI: true, - isPublishRelease: false, - isProd: true, - }); - - const versionIdx = args.indexOf('--version'); - if (versionIdx === -1 || versionIdx === args.length) { - console.log(`\n${color.bold.red('No `--version [VERSION]` argument was found. Exiting')}\n`); - process.exit(1); - } - if (prepareOpts.packageJson.version) { - prepareOpts.version = getNewVersion(prepareOpts.packageJson.version, args[versionIdx + 1]); - } - - await prepareRelease(prepareOpts, args); - console.log(`${color.bold.blue('Release Prepared!')}`); - } - - if (args.includes('--ci-publish')) { - const prepareOpts = getOptions(rootDir, { - isCI: true, - isPublishRelease: false, - isProd: true, - }); - // this was bumped already, we just need to copy it from package.json into this field - if (prepareOpts.packageJson.version) { - prepareOpts.version = prepareOpts.packageJson.version; - } - - // we generated a vermoji during the preparation step, let's grab it from the changelog - prepareOpts.vermoji = getLatestVermoji(prepareOpts.changelogPath); - - const tagIdx = args.indexOf('--tag'); - let newTag = null; - if (tagIdx === -1 || tagIdx === args.length) { - console.log(`\n${color.bold.yellow('No `--tag [TAG]` argument was found.')}\n`); - } else if (args[tagIdx + 1] === 'use_pkg_json_version') { - console.log( - `\n${color.bold.green( - 'The default package.json version will be used for the tag. No additional tags will be applied.', - )}\n`, - ); - } else { - newTag = args[tagIdx + 1]; - console.log(`\n${color.bold.green(`Set '--tag' argument to '${newTag}'.`)}\n`); - } - - console.log(`${color.bold.blue(`Version: ${prepareOpts.version}`)}`); - console.log(`${color.bold.blue(`Tag: ${newTag}`)}`); - - const publishOpts = getOptions(rootDir, { - buildId: prepareOpts.buildId, - version: prepareOpts.version, - vermoji: prepareOpts.vermoji, - isCI: prepareOpts.isCI, - isPublishRelease: true, - isProd: true, - tag: newTag ?? undefined, - }); - return await publishRelease(publishOpts, args); - } -} - -/** - * Prepares a release of Stencil - * @param opts build options containing the metadata needed to release a new version of Stencil - * @param args stringified arguments used to influence the release steps that are taken - */ -async function prepareRelease(opts: BuildOptions, args: ReadonlyArray): Promise { - const pkg = opts.packageJson; - const oldVersion = opts.packageJson.version; - console.log( - `\nPrepare to publish ${opts.vermoji} ${color.bold.magenta(pkg.name)} ${color.dim(`(currently ${oldVersion})`)}\n`, - ); - - try { - await runReleaseTasks(opts, args); - } catch (err: any) { - console.log('\n', color.red(err), '\n'); - process.exit(0); - } -} - -/** - * Initiates publishing a Stencil release. - * @param opts build options containing the metadata needed to publish a new version of Stencil - * @param args stringified arguments used to influence the steps that are taken - * @returns a void promise - */ -async function publishRelease(opts: BuildOptions, args: ReadonlyArray): Promise { - const pkg = opts.packageJson; - if (opts.version !== pkg.version) { - throw new Error( - `Prepare release data (${opts.version}) and package.json (${pkg.version}) versions do not match. Try re-running release prepare.`, - ); - } - - console.log(`\nPublish ${opts.vermoji} ${color.bold.magenta(pkg.name)} ${color.yellow(`${opts.version}`)}\n`); - - try { - await runReleaseTasks(opts, args); - } catch (err: any) { - console.log('\n', color.red(err), '\n'); - process.exit(0); - } -} diff --git a/scripts/test/copy-readme.js b/scripts/test/copy-readme.js deleted file mode 100644 index 6635a93fe9e..00000000000 --- a/scripts/test/copy-readme.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * This script copies a supplemental README file to the location where it will be overwritten - * during the `docs-readme` output target test. The purpose of this step is to ensure that - * the file is in a known state before the test runs, avoiding issues with Git detecting - * unexpected changes to the file. - * - * Context: - * - During the `docs-readme` tests, a README file is overwritten as part of the test process. - * - The expected result of the test must be tracked by Git; otherwise, Git will detect a "dirty" - * state and the test will fail. - * - This behaviour can be used to our advantage: if the file is overwritten with the supplemental - * file but not overwritten back to the expected result, Git will detect a dirty state, causing - * the test to fail. This ensures that the correct action is taken by the code being tested. - * - * Usage: - * - This script is executed as part of the `prepare.readmes` npm script. - * - It copies `readme-supplemental.md` to `readme.md` in the appropriate directory. - */ - -const fs = require('fs'); -const path = require('path'); - -// Define source and destination paths -const src = path.resolve( - __dirname, - '../../test/docs-readme/custom-readme-output-overwrite/components/styleurls-component/readme-supplemental.md', -); -const dest = path.resolve( - __dirname, - '../../test/docs-readme/custom-readme-output-overwrite/components/styleurls-component/readme.md', -); - -// Copy the file -try { - fs.copyFileSync(src, dest); - console.log(`Copied ${src} to ${dest}`); -} catch (err) { - console.error(`Error copying file: ${err.message}`); - process.exit(1); -} diff --git a/scripts/test/validate-build.ts b/scripts/test/validate-build.ts deleted file mode 100644 index eed52d2beb0..00000000000 --- a/scripts/test/validate-build.ts +++ /dev/null @@ -1,410 +0,0 @@ -import fs from 'fs-extra'; -import { dirname, join, relative } from 'path'; -import { rollup } from 'rollup'; -import ts, { ModuleResolutionKind, ScriptTarget } from 'typescript'; -import url from 'url'; - -import { NODE_BUILTINS } from '../utils/constants'; -import { BuildOptions, getOptions } from '../utils/options'; -import { PackageData } from '../utils/write-pkg-json'; - -/** - * Used to triple check that the final build files - * ready to be published are good to go - */ -const pkgs: TestPackage[] = [ - { - // cli - packageJson: 'cli/package.json', - }, - { - // compiler - packageJson: 'compiler/package.json', - files: ['compiler/lib.d.ts', 'compiler/lib.dom.d.ts'], - }, - { - // dev-server - packageJson: 'dev-server/package.json', - files: [ - 'dev-server/static/favicon.ico', - 'dev-server/templates/directory-index.html', - 'dev-server/templates/initial-load.html', - 'dev-server/connector.html', - 'dev-server/server-process.js', - 'dev-server/server-worker-thread.js', - 'dev-server/visualstudio.vbs', - 'dev-server/xdg-open', - ], - }, - { - // internal/app-data - packageJson: 'internal/app-data/package.json', - }, - { - // internal/client - packageJson: 'internal/client/package.json', - files: ['internal/client/polyfills/'], - }, - { - // internal/hydrate - packageJson: 'internal/hydrate/package.json', - files: ['internal/hydrate/runner.d.ts', 'internal/hydrate/runner.js'], - }, - { - // internal/testing - packageJson: 'internal/testing/package.json', - }, - { - // internal - packageJson: 'internal/package.json', - files: [ - 'internal/stencil-core/index.cjs', - 'internal/stencil-core/index.js', - 'internal/stencil-core/index.d.ts', - 'internal/stencil-ext-modules.d.ts', - 'internal/stencil-private.d.ts', - 'internal/stencil-public-compiler.d.ts', - 'internal/stencil-public-docs.d.ts', - 'internal/stencil-public-runtime.d.ts', - ], - }, - { - // mock-doc - packageJson: 'mock-doc/package.json', - }, - { - // screenshot - packageJson: 'screenshot/package.json', - files: [ - 'screenshot/compare/', - 'screenshot/connector.js', - 'screenshot/local-connector.js', - 'screenshot/pixel-match.js', - ], - }, - { - // sys/node - packageJson: 'sys/node/package.json', - files: ['sys/node/autoprefixer.js', 'sys/node/graceful-fs.js', 'sys/node/node-fetch.js'], - }, - { - // testing - packageJson: 'testing/package.json', - files: [ - 'testing/jest-environment.js', - 'testing/jest-preprocessor.js', - 'testing/jest-preset.js', - 'testing/jest-runner.js', - 'testing/jest-setuptestframework.js', - ], - }, - { - // @stencil/core - packageJson: 'package.json', - packageJsonFiles: [ - 'bin/', - 'cli/', - 'compiler/', - 'dev-server/', - 'internal/', - 'mock-doc/', - 'screenshot/', - 'sys/', - 'testing/', - ], - files: ['CHANGELOG.md', 'LICENSE.md', 'readme.md'], - hasBin: true, - }, -]; - -/** - * Validate that certain files were written to disk during the build, and that - * these files tree-shake correctly. - * - * @param rootDir the root of the Stencil repository - */ -export async function validateBuild(rootDir: string): Promise { - const dtsEntries: string[] = []; - const opts = getOptions(rootDir); - pkgs.forEach((testPkg) => { - validatePackage(opts, testPkg, dtsEntries); - }); - console.log(`🐡 Validated packages`); - - validateDts(opts, dtsEntries); - - await validateCompiler(opts); - await validateTreeshaking(opts); -} - -/** - * Validates a bundled package/sub-module. Validation steps include verifying that various fields in `package.json` are - * filled out and file references are valid. - * @param opts build options to be used to validate a package - * @param testPkg the package to validate - * @param dtsEntries a reference to .d.ts files to collect while validating the package - */ -function validatePackage(opts: BuildOptions, testPkg: TestPackage, dtsEntries: string[]): void { - const rootDir = opts.rootDir; - - if (testPkg.packageJson) { - testPkg.packageJson = join(rootDir, testPkg.packageJson); - const pkgDir = dirname(testPkg.packageJson); - const pkgJson: PackageData = require(testPkg.packageJson); - - if (!pkgJson.name) { - throw new Error('missing package.json name: ' + testPkg.packageJson); - } - - if (!pkgJson.main) { - throw new Error('missing package.json main: ' + testPkg.packageJson); - } - - if (testPkg.packageJsonFiles) { - if (!Array.isArray(pkgJson.files)) { - throw new Error(testPkg.packageJson + ' missing "files" property'); - } - pkgJson.files.forEach((f) => { - if (f === '!**/*.map' || f === '!**/*.stub.ts' || f === '!**/*.stub.tsx') { - // skip sourcemaps, stub files - return; - } - const pkgFile = join(pkgDir, f); - fs.accessSync(pkgFile); - }); - testPkg.packageJsonFiles.forEach((testPkgFile) => { - if (!pkgJson.files?.includes(testPkgFile)) { - throw new Error(testPkg.packageJson + ' missing file ' + testPkgFile); - } - - const filePath = join(pkgDir, testPkgFile); - fs.accessSync(filePath); - }); - } - - if (testPkg.hasBin && !pkgJson.bin) { - throw new Error(testPkg.packageJson + ' missing bin'); - } - - if (pkgJson.bin) { - Object.keys(pkgJson.bin).forEach((k) => { - if (pkgJson.bin?.[k]) { - const binExe = join(pkgDir, pkgJson.bin[k]); - fs.accessSync(binExe); - } - }); - } - - const mainIndex = join(pkgDir, pkgJson.main); - fs.accessSync(mainIndex); - - if (pkgJson.module) { - const moduleIndex = join(pkgDir, pkgJson.module); - fs.accessSync(moduleIndex); - } - - if (pkgJson.browser) { - const browserIndex = join(pkgDir, pkgJson.browser); - fs.accessSync(browserIndex); - } - - if (pkgJson.types) { - const pkgTypes = join(pkgDir, pkgJson.types); - fs.accessSync(pkgTypes); - dtsEntries.push(pkgTypes); - } - } - - if (testPkg.files) { - testPkg.files.forEach((file) => { - const filePath = join(rootDir, file); - fs.statSync(filePath); - }); - } -} - -/** - * Validate the .d.ts files used in the output are semantically and syntactically correct - * @param opts build options to be used to validate .d.ts files - * @param dtsEntries the .d.ts files to validate - */ -function validateDts(opts: BuildOptions, dtsEntries: string[]): void { - const program = ts.createProgram(dtsEntries, { - baseUrl: '.', - paths: { - '@stencil/core/mock-doc': [join(opts.rootDir, 'mock-doc', 'index.d.ts')], - '@stencil/core/internal': [join(opts.rootDir, 'internal', 'index.d.ts')], - '@stencil/core/internal/testing': [join(opts.rootDir, 'internal', 'testing', 'index.d.ts')], - }, - moduleResolution: ModuleResolutionKind.NodeJs, - target: ScriptTarget.ES2016, - }); - - const tsDiagnostics = program.getSemanticDiagnostics().concat(program.getSyntacticDiagnostics()); - - if (tsDiagnostics.length > 0) { - const host = { - getCurrentDirectory: () => ts.sys.getCurrentDirectory(), - getNewLine: () => ts.sys.newLine, - getCanonicalFileName: (f: string) => f, - }; - throw new Error('🧨 ' + ts.formatDiagnostics(tsDiagnostics, host)); - } - console.log(`🐟 Validated dts files`); -} - -/** - * Validates the Stencil compiler. This includes verifying that the compiler, CLI and sys API can be instantiated, - * smoke testing the compiler's transpilation, and running a small task in the CLI. - * @param opts build options to be used to validate the compiler - */ -async function validateCompiler(opts: BuildOptions): Promise { - const compilerPath = url.pathToFileURL(join(opts.output.compilerDir, 'stencil.js')).pathname; - const cliPath = url.pathToFileURL(join(opts.output.cliDir, 'index.cjs')).pathname; - const sysNodePath = url.pathToFileURL(join(opts.output.sysNodeDir, 'index.js')).pathname; - - const compiler = await import(compilerPath); - const cli = await import(cliPath); - const sysNodeApi = await import(sysNodePath); - - const nodeLogger = sysNodeApi.createNodeLogger(); - const nodeSys = sysNodeApi.createNodeSys({ process }); - - if (!nodeSys || nodeSys.name !== 'node' || nodeSys.version.length < 4) { - throw new Error(`🧨 unable to validate sys node`); - } - console.log(`🐳 Validated sys node, current ${nodeSys.name} version: ${nodeSys.version}`); - - const validated = await compiler.loadConfig({ - config: { - logger: nodeLogger, - sys: nodeSys, - }, - }); - console.log(`${compiler.vermoji} Validated compiler: ${compiler.version}`); - - const transpileResults = compiler.transpileSync('const m: string = `transpile!`;', { - target: 'es5', - }); - if ( - !transpileResults || - transpileResults.diagnostics.length > 0 || - !transpileResults.code.startsWith(`var m = "transpile!";`) - ) { - console.error(transpileResults); - throw new Error(`🧨 transpileSync error`); - } - console.log(`🐋 Validated compiler.transpileSync()`); - - const orgConsoleLog = console.log; - let loggedVersion = ''; - console.log = (value: string) => (loggedVersion = value); - - // this runTask is intentionally not wrapped in telemetry helpers - await cli.runTask(compiler, validated.config, 'version'); - - console.log = orgConsoleLog; - - if (typeof loggedVersion !== 'string' || loggedVersion.length < 4) { - throw new Error(`🧨 unable to validate compiler. loggedVersion: "${loggedVersion}"`); - } - - console.log(`🐬 Validated cli`); -} - -/** - * Validate tree shaking for various modules in the output - * @param opts build options to be used to validate tree-shaking - */ -async function validateTreeshaking(opts: BuildOptions) { - await validateModuleTreeshake(opts, 'app-data', join(opts.output.internalDir, 'app-data', 'index.js')); - await validateModuleTreeshake(opts, 'client', join(opts.output.internalDir, 'client', 'index.js')); - await validateModuleTreeshake(opts, 'patch-browser', join(opts.output.internalDir, 'client', 'patch-browser.js')); - await validateModuleTreeshake(opts, 'shadow-css', join(opts.output.internalDir, 'client', 'shadow-css.js')); - await validateModuleTreeshake(opts, 'hydrate', join(opts.output.internalDir, 'hydrate', 'index.js')); - await validateModuleTreeshake(opts, 'stencil-core', join(opts.output.internalDir, 'stencil-core', 'index.js')); - await validateModuleTreeshake(opts, 'cli', join(opts.output.cliDir, 'index.js')); -} - -/** - * Validates tree-shaking for a single module & entrypoint - * @param opts build options to be used to validate tree-shaking for a specific module - * @param moduleName the module to validate - * @param entryModulePath the entrypoint to validate - */ -async function validateModuleTreeshake(opts: BuildOptions, moduleName: string, entryModulePath: string): Promise { - // this is a song, 'agadoo' by Black Lace - const virtualInputId = `@g@doo`; - const entryId = `@entry-module`; - const outputFile = join(opts.scriptsBuildDir, `treeshake_${moduleName}.js`); - - const bundle = await rollup({ - external: NODE_BUILTINS, - input: virtualInputId, - treeshake: { - moduleSideEffects: false, - }, - plugins: [ - { - name: 'stencilResolver', - resolveId(id) { - if (id === '@stencil/core/internal/client' || id === '@stencil/core') { - return join(opts.output.internalDir, 'client', 'index.js'); - } - if (id === '@stencil/core/internal/app-data') { - return join(opts.output.internalDir, 'app-data', 'index.js'); - } - if (id === '@stencil/core/internal/app-globals') { - return id; - } - if (id === virtualInputId) { - return id; - } - if (id === entryId) { - return entryModulePath; - } - }, - load(id) { - if (id === '@stencil/core/internal/app-globals') { - return 'export const globalScripts = () => {};\nexport const globalStyles = "";'; - } - if (id === virtualInputId) { - return `import "${entryId}";`; - } - }, - }, - ], - onwarn(warning) { - if (warning.code !== 'EMPTY_BUNDLE') { - throw warning; - } - }, - }); - - const o = await bundle.generate({ - format: 'es', - }); - - const output = o.output[0]; - const outputCode = output.code.trim(); - - await fs.writeFile(outputFile, outputCode); - - if (outputCode !== '') { - console.error(`\nTreeshake output: ${outputFile}\n`); - - throw new Error(`🧨 Not all code was not treeshaken (treeshooken? treeshaked?)`); - } - - console.log(`🌳 validated treeshake: ${relative(opts.rootDir, entryModulePath)}`); -} - -/** - * Represents a package/submodule of the bundled Stencil output to validate - */ -interface TestPackage { - packageJson?: string; - packageJsonFiles?: string[]; - files?: string[]; - hasBin?: boolean; -} diff --git a/scripts/test/validate-testing.js b/scripts/test/validate-testing.js deleted file mode 100644 index 2fbc6e13fd6..00000000000 --- a/scripts/test/validate-testing.js +++ /dev/null @@ -1,20 +0,0 @@ -const testing = require('../../testing/index.js'); - -const input = ` -import { Component, Prop } from '@stencil/core'; -@Component({ - tag: 'my-cmp' -}) -export class MyCmp { - @Prop() prop: boolean; -} -`; - -const output = testing.transpile(input); - -if (output.diagnostics.length > 0) { - const msg = output.diagnostics.map((d) => d.messageText).join('\n'); - throw new Error('Testing transpile error: \n' + msg); -} - -console.log(`🐠 Validated testing suite`); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json deleted file mode 100644 index 37b208eef2f..00000000000 --- a/scripts/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "alwaysStrict": true, - "strict": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "lib": [ - "dom", - "es2021" - ], - "module": "NodeNext", - "moduleResolution": "nodenext", - "skipLibCheck": true, - "outDir": "build/", - "pretty": true, - "target": "ES2020", - "incremental": false, - "useUnknownInCatchVariables": true - }, - "include": [ - "**/*.ts", - "types/*.d.ts" - ], - "exclude": [ - "**/*.spec.ts" - ] -} diff --git a/scripts/types/rollup-plugin-node-resolve.d.ts b/scripts/types/rollup-plugin-node-resolve.d.ts deleted file mode 100644 index 8f087d40ab1..00000000000 --- a/scripts/types/rollup-plugin-node-resolve.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '@rollup/plugin-node-resolve'; diff --git a/scripts/updateSelectorEngine.ts b/scripts/updateSelectorEngine.ts deleted file mode 100644 index a2e37716300..00000000000 --- a/scripts/updateSelectorEngine.ts +++ /dev/null @@ -1,105 +0,0 @@ -import cp from 'node:child_process'; -import fs from 'node:fs/promises'; -import path from 'node:path'; - -/** - * This script updates the JQuery selector engine for the mock-doc package - * to the latest version. - * - * To run it, use the following command: - * ```sh - * npm run tsc.scripts - * npm run build.updateSelectorEngine - * ``` - */ - -const rootDir = path.resolve(__dirname, '..'); -const jqueryDepDir = path.resolve(rootDir, 'node_modules', 'jquery'); -const WINDOW_MOCK = `{ - document: { - createElement() { - return {}; - }, - nodeType: 9, - documentElement: { - nodeType: 1, - nodeName: 'HTML' - } - } -}`; - -async function run() { - console.log('updating JQuery Selector engine...'); - - await runCommand(`npm install --ignore-scripts`, jqueryDepDir); - await runCommand(`npm run build -- --include=selector`, jqueryDepDir); - - const jqueryPkgJSON = JSON.parse(await fs.readFile(path.resolve(jqueryDepDir, 'package.json'), 'utf8')); - const thirdPartyDir = path.resolve(__dirname, '../../src/mock-doc/third-party'); - await fs.mkdir(thirdPartyDir, { recursive: true }); - - const originalContent = await fs.readFile(path.resolve(jqueryDepDir, 'dist', 'jquery.js'), 'utf8'); - - /** - * This is a hack to make the jQuery selector engine work with the mock-doc package. - * Using the original jQuery bundle would cause a "RegExpCompiler Allocation failed - process out of memory" - * error due to the way the jQuery object is constructed. The following tweaks to the jQuery bundle - * resolve the issue: - */ - const fixedJQuery = originalContent - /** - * Never run the short-circuiting code due to usage of too many RegExp objects - */ - .replace('if ( !seed ) {', 'if ( false ) {') - /** - * Make jQuery an object to have it garbage collected - */ - .replace('var version = "', `const jQuery = {} as { find: Function };\nvar version = "`) - /** - * Inject window mock directly into iife - */ - .replace(`typeof window !== "undefined" ? window : this`, WINDOW_MOCK) - /** - * Rename the original jQuery function to jQueryOrig - */ - .replace('jQuery = function( selector, context ) {', 'jQueryOrig = function( selector, context ) {') - /** - * Replace use of `jQuery.attr` with `elem.getAttribute` to avoid having to include - * the attributes plugin to the bundle - */ - .replace('jQuery.attr( elem, name )', 'elem.getAttribute( name )') - /** - * make it return jQuery directly rather than relying on a module system - */ - .replace('module.exports = factory( global, true );', 'return factory( global, true );') - .replace('if ( typeof module === "object" && typeof module.exports === "object" ) {', 'if (true) {'); - - const newContent = `/* eslint-disable */ -// @ts-nocheck - -/** - * ATTENTION: DO NOT MODIFY THIS FILE - * - * This file is generated by "scripts/updateSelectorEngine.ts" and can be overwritten - * at any time. Don't make changes in here as they will get lost! - */ -export default ${fixedJQuery}; -`; - fs.writeFile(path.resolve(thirdPartyDir, 'jquery.ts'), newContent, 'utf8'); - - console.log(`\nJQuery Selector engine updated to version ${jqueryPkgJSON.version}`); - console.log(`at ${thirdPartyDir} 🎉`); -} - -function runCommand(cmd: string, cwd: string) { - return new Promise((resolve, reject) => { - console.log(`> ${cmd}`); - const child = cp.spawn(cmd, { cwd, shell: true }); - child.on('error', reject); - child.on('exit', (code) => (code === 0 ? resolve(child) : reject())); - }); -} - -if (require.main === module) { - run(); -} diff --git a/scripts/utils/banner.ts b/scripts/utils/banner.ts deleted file mode 100644 index 098adc88002..00000000000 --- a/scripts/utils/banner.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BuildOptions } from './options'; - -export function getBanner(opts: BuildOptions, fileName: string, license = false) { - return [ - `/*${license ? '!' : ''}`, - ` ${fileName} v${opts.version} | MIT Licensed | https://stenciljs.com`, - ` */`, - ].join('\n'); -} diff --git a/scripts/utils/bundle-dts.ts b/scripts/utils/bundle-dts.ts deleted file mode 100644 index f95aae9b0a8..00000000000 --- a/scripts/utils/bundle-dts.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { EntryPointConfig, generateDtsBundle, OutputOptions } from 'dts-bundle-generator'; -import fs from 'fs-extra'; - -import { BuildOptions } from './options'; - -/** - * A thin wrapper for `dts-bundle-generator` which uses our build options to - * set a few things up - * - * **Note**: this file caches its output to disk, and will return any - * previously cached file if not in a prod environment! - * - * @param opts an object holding information about the current build of Stencil - * @param inputFile the path to the file which should be bundled - * @param outputOptions options for bundling the file - * @param useCache whether or not the bundled file should be cached to disk - * @returns a string containing the bundled typedef - */ -export async function bundleDts( - opts: BuildOptions, - inputFile: string, - outputOptions?: OutputOptions, - useCache = true, -): Promise { - const cachedDtsOutput = inputFile + '-bundled.d.ts'; - - if (!opts.isProd && useCache) { - try { - return await fs.readFile(cachedDtsOutput, 'utf8'); - } catch (e) {} - } - - const config: EntryPointConfig = { - filePath: inputFile, - }; - - if (outputOptions) { - config.output = outputOptions; - } - - const outputCode = cleanDts(generateDtsBundle([config]).join('\n')); - - if (useCache) { - await fs.writeFile(cachedDtsOutput, outputCode); - } - - return outputCode; -} - -export function cleanDts(dtsContent: string) { - dtsContent = dtsContent.replace(/\/\/\/ /g, ''); - - dtsContent = dtsContent.replace(/NodeJS.Process/g, 'any'); - - dtsContent = dtsContent.replace(/import \{ URL \} from \'url\';/g, ''); - - return dtsContent.trim() + '\n'; -} diff --git a/scripts/utils/constants.ts b/scripts/utils/constants.ts deleted file mode 100644 index ea8c73168ff..00000000000 --- a/scripts/utils/constants.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Node built-ins that we mark as external when building Stencil - */ -export const NODE_BUILTINS = [ - '_http_agent', - '_http_client', - '_http_common', - '_http_incoming', - '_http_outgoing', - '_http_server', - '_stream_duplex', - '_stream_passthrough', - '_stream_readable', - '_stream_transform', - '_stream_wrap', - '_stream_writable', - '_tls_common', - '_tls_wrap', - 'assert', - 'async_hooks', - 'buffer', - 'child_process', - 'cluster', - 'console', - 'constants', - 'crypto', - 'dgram', - 'dns', - 'domain', - 'events', - 'fs', - 'fs/promises', - 'http', - 'http2', - 'https', - 'inspector', - 'module', - 'net', - 'os', - 'path', - 'perf_hooks', - 'process', - 'punycode', - 'querystring', - 'readline', - 'repl', - 'stream', - 'string_decoder', - 'sys', - 'timers', - 'tls', - 'trace_events', - 'tty', - 'url', - 'util', - 'v8', - 'vm', - 'worker_threads', - 'zlib', -]; diff --git a/scripts/utils/conventional-changelog-config.js b/scripts/utils/conventional-changelog-config.js deleted file mode 100644 index 5ca61c9489d..00000000000 --- a/scripts/utils/conventional-changelog-config.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Options for [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog), which is - * used to generate the Stencil changelog at release time. - */ -module.exports = { - parserOpts: { - /** - * Override the conventional-changelog parser default configuration and any provided preset (e.g. 'Angular') for - * detecting issues. Stencil uses the "Angular preset", which defaults the "issuesPrefixes" field to a single pound - * sign ('#'). This sometimes gets mistaken by the changelog generator as an issue that is fixed, when it fact it's - * cross-reference to another issue. - * - * Note: Only the git commit message is being parsed, not the GitHub Issue summary. For any of the values below to - * be picked up by conventional-changelog, they must be added to the git commit message. - * - * Reference for this property: [GitHub README]{@link https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#issueprefixes} - * By default, [these are case-insensitive]{@link https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#issueprefixescasesensitive)} - */ - issuePrefixes: [ - 'fixes: #', - 'fixes:#', - 'fixes- #', - 'fixes-#', - 'fixes #', - 'fixes#', - 'closes: #', - 'closes:#', - 'closes- #', - 'closes-#', - 'closes #', - 'closes#', - ], - }, -}; diff --git a/scripts/utils/options.ts b/scripts/utils/options.ts deleted file mode 100644 index 748c7211804..00000000000 --- a/scripts/utils/options.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { execSync } from 'child_process'; -import { readFileSync } from 'fs-extra'; -import { join } from 'path'; - -import { getVermoji } from './vermoji'; -import { PackageData } from './write-pkg-json'; - -/** - * Retrieves information used during a 'process' that requires knowledge of various project file paths, Stencil version - * information, and GitHub repo metadata. A 'process' may include, but is not limited to: - * - generating a new release - * - regenerating a license file - * - validating a build - * @param rootDir the root directory of the project - * @param inputOpts any build options to override manually - * @returns an entity containing various fields to be used by some process - */ -export function getOptions(rootDir: string, inputOpts: Partial = {}): BuildOptions { - const srcDir = join(rootDir, 'src'); - const packageJsonPath = join(rootDir, 'package.json'); - const packageLockJsonPath = join(rootDir, 'package-lock.json'); - const changelogPath = join(rootDir, 'CHANGELOG.md'); - const nodeModulesDir = join(rootDir, 'node_modules'); - const typescriptDir = join(nodeModulesDir, 'typescript'); - const typescriptLibDir = join(typescriptDir, 'lib'); - const buildDir = join(rootDir, 'build'); - const scriptsDir = join(rootDir, 'scripts'); - const scriptsBuildDir = join(scriptsDir, 'build'); - const scriptsBundlesDir = join(scriptsDir, 'esbuild'); - const bundleHelpersDir = join(scriptsBundlesDir, 'helpers'); - const packageJson: PackageData = JSON.parse(readFileSync(packageJsonPath, 'utf8')); - const buildId = inputOpts.buildId ?? getBuildId(); - const version = inputOpts.version ?? getDevVersionId({ buildId, semverVersion: packageJson?.version }); - - const vermoji = - inputOpts.isProd && !inputOpts.vermoji - ? getVermoji(inputOpts.changelogPath ?? changelogPath) - : inputOpts.vermoji ?? '💎'; - - const typescriptPkg = require(join(typescriptDir, 'package.json')); - const typescriptVersion = typescriptPkg.version; - - const terserPkg = getPkg(nodeModulesDir, 'terser'); - const terserVersion = terserPkg.version; - - const rollupPkg = getPkg(nodeModulesDir, 'rollup'); - const rollupVersion = rollupPkg.version; - - const parse5Pkg = getPkg(nodeModulesDir, 'parse5'); - const parse5Version = parse5Pkg.version; - - const jqueryPkg = getPkg(nodeModulesDir, 'jquery'); - const jqueryVersion = jqueryPkg.version; - - const opts: BuildOptions = { - ghRepoOrg: 'ionic-team', - ghRepoName: 'stencil', - rootDir, - srcDir, - packageJsonPath, - packageLockJsonPath, - changelogPath, - nodeModulesDir, - typescriptDir, - typescriptLibDir, - packageJson, - buildDir, - scriptsDir, - scriptsBuildDir, - scriptsBundlesDir, - bundleHelpersDir, - output: { - cliDir: join(rootDir, 'cli'), - compilerDir: join(rootDir, 'compiler'), - devServerDir: join(rootDir, 'dev-server'), - internalDir: join(rootDir, 'internal'), - mockDocDir: join(rootDir, 'mock-doc'), - screenshotDir: join(rootDir, 'screenshot'), - sysNodeDir: join(rootDir, 'sys', 'node'), - testingDir: join(rootDir, 'testing'), - }, - version, - buildId, - isProd: false, - isCI: false, - isWatch: false, - isPublishRelease: false, - vermoji, - tag: 'dev', - jqueryVersion, - parse5Version, - rollupVersion, - terserVersion, - typescriptVersion, - }; - - Object.assign(opts, inputOpts); - - if (opts.isPublishRelease) { - if (!opts.isProd) { - throw new Error('release must also be a prod build'); - } - } - - return opts; -} - -/** - * Generates an object containing versioning information of various packages - * installed at build time - * - * **NOTE** this will mutate the `opts` parameter, adding information about - * the versions used for various dependencies - * - * @param opts the options being used during a build - * @returns an object that contains package names/versions installed at the time a build was invoked - */ -export function createReplaceData(opts: BuildOptions): Record { - const CACHE_BUSTER = 7; - - const typescriptPkg = require(join(opts.typescriptDir, 'package.json')); - const transpileId = typescriptPkg.name + typescriptPkg.version + '_' + CACHE_BUSTER; - - const terserPkg = getPkg(opts.nodeModulesDir, 'terser'); - const minifyJsId = terserPkg.name + terserPkg.version + '_' + CACHE_BUSTER; - - const rollupPkg = getPkg(opts.nodeModulesDir, 'rollup'); - const bundlerId = rollupPkg.name + rollupPkg.version + '_' + CACHE_BUSTER; - - const autoprefixerPkg = getPkg(opts.nodeModulesDir, 'autoprefixer'); - const postcssPkg = getPkg(opts.nodeModulesDir, 'postcss'); - - const optimizeCssId = - autoprefixerPkg.name + autoprefixerPkg.version + '_' + postcssPkg.name + postcssPkg.version + '_' + CACHE_BUSTER; - - return { - __BUILDID__: opts.buildId, - '__BUILDID:BUNDLER__': bundlerId, - '__BUILDID:MINIFYJS__': minifyJsId, - '__BUILDID:OPTIMIZECSS__': optimizeCssId, - '__BUILDID:TRANSPILE__': transpileId, - - '__VERSION:STENCIL__': opts.version, - '__VERSION:PARSE5__': opts.parse5Version, - '__VERSION:ROLLUP__': opts.rollupVersion, - '__VERSION:JQUERY__': opts.jqueryVersion, - '__VERSION:TERSER__': opts.terserVersion, - '__VERSION:TYPESCRIPT__': opts.typescriptVersion, - - __VERMOJI__: opts.vermoji, - }; -} - -type VersionedPackageData = PackageData & { version: string }; - -/** - * Retrieves a package from the `node_modules` directory in the given `opts` parameter - * @param nodeModulesDir the node modules directory to search - * @param pkgName the name of the NPM package to retrieve - * @returns information about the retrieved package - */ -function getPkg(nodeModulesDir: string, pkgName: string): VersionedPackageData { - const packageJson = require(join(nodeModulesDir, pkgName, 'package.json')); - if (!packageJson.version) { - throw Error(`Didn't find a version in the packageJson for ${pkgName}!`); - } - return packageJson; -} - -export interface BuildOptions { - buildDir: string; - bundleHelpersDir: string; - ghRepoName: string; - ghRepoOrg: string; - nodeModulesDir: string; - rootDir: string; - scriptsBuildDir: string; - scriptsBundlesDir: string; - scriptsDir: string; - srcDir: string; - typescriptDir: string; - typescriptLibDir: string; - - output: { - cliDir: string; - compilerDir: string; - devServerDir: string; - internalDir: string; - mockDocDir: string; - screenshotDir: string; - sysNodeDir: string; - testingDir: string; - }; - - buildId: string; - changelogPath: string; - isCI: boolean; - isProd: boolean; - isPublishRelease: boolean; - isWatch: boolean; - jqueryVersion: string; - packageJson: PackageData; - packageJsonPath: string; - packageLockJsonPath: string; - parse5Version: string; - rollupVersion: string; - tag: string; - terserVersion: string; - typescriptVersion: string; - vermoji: string; - version: string; -} - -/** - * Generate a build identifier, which is the Epoch Time in seconds - * @returns the generated build ID - */ -function getBuildId(): string { - return Date.now().toString(10).slice(0, -3); -} - -/** - * Describes the contents of a version string for Stencil used in 'non-production' builds (e.g. a one-off dev build) - */ -interface DevVersionContents { - /** - * The build identifier string, used to uniquely identify when the build was generated - */ - buildId: string; - /** - * A semver-compliant string to add to the one-off build version sting, used to identify a base version of Stencil - * that was used in the build. - */ - semverVersion: string | undefined; -} - -/** - * Helper function to return the first seven characters of a git SHA - * - * We use the first seven characters for two reasons: - * 1. Seven characters _should_ be enough to uniquely ID a commit in Stencil - * 2. It matches the number of characters used in our CHANGELOG.md - * - * @returns the seven character SHA - */ -function getSevenCharGitSha(): string { - return execSync('git rev-parse HEAD').toString().trim().slice(0, 7); -} - -/** - * Helper function to generate a dev build version string of the format: - * - * [BASE_VERSION]-dev.[BUILD_IDENTIFIER].[GIT_SHA] - * - * where: - * - BASE_VERSION is the version of Stencil currently assigned in `package.json` - * - BUILD_IDENTIFIER is a unique identifier for this particular build - * - GIT_SHA is the SHA of the HEAD of the branch this build was created from - * - * @param devVersionContents an object containing the necessary arguments to build a dev-version identifier - * @returns the generated version string - */ -function getDevVersionId(devVersionContents: DevVersionContents): string { - const { buildId, semverVersion } = devVersionContents; - // if `package.json#package` is empty, default to a value that doesn't imply any particular version of Stencil - const version = semverVersion ?? '0.0.0'; - // '-' and '-dev.' are a magic substrings that may get checked on startup of a Stencil process. - return version + '-dev.' + buildId + '.' + getSevenCharGitSha(); -} diff --git a/scripts/utils/postcss-bundle b/scripts/utils/postcss-bundle deleted file mode 100755 index edeac42ff8f..00000000000 --- a/scripts/utils/postcss-bundle +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -cd ../build -#rm -rf ./postcss -#git clone https://github.com/postcss/postcss.git --depth 1 -cd ../.. -echo $PWD -./node_modules/.bin/rollup -c ./scripts/utils/postcss-rollup.js \ No newline at end of file diff --git a/scripts/utils/postcss-rollup.js b/scripts/utils/postcss-rollup.js deleted file mode 100644 index fa038e612c4..00000000000 --- a/scripts/utils/postcss-rollup.js +++ /dev/null @@ -1,27 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; - -const input = require.resolve('postcss'); -const output = path.join(__dirname, '..', 'bundles', 'helpers', 'postcss.js'); -const postcssPkg = fs.readJSONSync(path.join(input, '..', '..', 'package.json')); - -export default { - input, - output: { - format: 'esm', - file: output, - banner: `// postcss esm build from ${postcssPkg.version}`, - }, - plugins: [ - { - resolveId(importee, importer) { - if (importee.startsWith('.')) { - if (importer && importer.endsWith('.es6')) { - const dir = path.dirname(importer); - return path.join(dir, importee + '.es6'); - } - } - }, - }, - ], -}; diff --git a/scripts/utils/release-utils.ts b/scripts/utils/release-utils.ts deleted file mode 100644 index 2b3825cf2cd..00000000000 --- a/scripts/utils/release-utils.ts +++ /dev/null @@ -1,172 +0,0 @@ -import color from 'ansi-colors'; -import fs from 'fs-extra'; -import { join } from 'path'; -import semver from 'semver'; - -import { BuildOptions } from './options'; - -export const SEMVER_INCREMENTS: ReadonlyArray = [ - 'patch', - 'minor', - 'major', - 'prepatch', - 'preminor', - 'premajor', - 'prerelease', -]; - -export const PRERELEASE_VERSIONS: ReadonlyArray = ['prepatch', 'preminor', 'premajor', 'prerelease']; - -/** - * Helper function to help determine if a version is valid semver - * @param input the version string to validate - * @returns true if the `input` is valid semver, false otherwise - */ -export const isValidVersion = (input: string) => Boolean(semver.valid(input)); - -/** - * Determines whether or not a version string is valid. A version string is considered to be 'valid' if it meets one of - * two criteria: - * - it is a valid semver name (e.g. 'patch', 'major', etc.) - * - it is a valid semver string (e.g. '1.0.2') - * @param input the version string to validate - * @returns true if the string is valid, false otherwise - */ -export const isValidVersionInput = (input: string): boolean => - SEMVER_INCREMENTS.indexOf(input) !== -1 || isValidVersion(input); - -/** - * Determines if the provided `version` is a semver pre-release or not - * @param version the version string to evaluate - * @returns true if the `version` is a pre-release, false otherwise - */ -export const isPrereleaseVersion = (version: string): boolean => - PRERELEASE_VERSIONS.indexOf(version) !== -1 || Boolean(semver.prerelease(version)); - -/** - * Determine the 'next' version string for a release. The next version can take one of two formats: - * 1. An alphabetic string that is a valid semver name (e.g. 'patch', 'major', etc.) - * 2. A valid semver string (e.g. '1.0.2') - * The value returned by this function is predicated on the format of `oldVersion`. If `oldVersion` is an alphabetic - * semver name, a semver name will be returned (e.g. 'major'). If a valid semver string is provided (e.g. 1.0.2), the - * incremented semver string will be returned (e.g. 2.0.0) - * @param oldVersion the old/current version of the library - * @param input the desired increment unit - * @returns new version's string - */ -export function getNewVersion(oldVersion: string, input: any): string { - const isValidSemverName = SEMVER_INCREMENTS.indexOf(input) === -1; - const incrementedSemverString = semver.inc(oldVersion, input); - - if (isValidSemverName) return input; - if (incrementedSemverString !== null) return incrementedSemverString; - throw new Error(`Version should be either ${SEMVER_INCREMENTS.join(', ')} or a valid semver version.`); -} - -/** - * Pretty printer for a new version of the library. Generates a new version string based on `inc` - * @param oldVersion the old/current version of Stencil - * @param inc the unit of increment for the new version - * @returns a pretty printed string containing the new version number - */ -export function prettyVersionDiff(oldVersion: string, inc: any): string { - const newVersion = getNewVersion(oldVersion, inc).split('.'); - const splitOldVersion = oldVersion.split('.'); - let firstVersionChange = false; - const output = []; - - for (let i = 0; i < newVersion.length; i++) { - if (newVersion[i] !== splitOldVersion[i] && !firstVersionChange) { - output.push(`${color.dim.cyan(newVersion[i])}`); - firstVersionChange = true; - } else if (newVersion[i].indexOf('-') >= 1) { - let preVersion = []; - preVersion = newVersion[i].split('-'); - output.push(`${color.dim.cyan(`${preVersion[0]}-${preVersion[1]}`)}`); - } else { - output.push(color.reset.dim(newVersion[i])); - } - } - return output.join(color.reset.dim('.')); -} - -/** - * Write changes to the local CHANGELOG.md on disk. - * - * Stencil uses the Angular-variant of conventional commits; commits must be formatted accordingly in order to be added - * to the changelog properly. - * @param opts build options to be used to update the changelog - */ -export async function updateChangeLog(opts: BuildOptions): Promise { - const ccPath = join(opts.nodeModulesDir, '.bin', 'conventional-changelog'); - const ccConfigPath = join(__dirname, 'conventional-changelog-config.js'); - const { execa } = await import('execa'); - // API Docs for conventional-changelog: https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-core#api - await execa( - 'node', - [ - ccPath, - '--preset', - 'angular', - '--infile', - opts.changelogPath, - '--outfile', - '--same-file', - '--config', - ccConfigPath, - ], - { - cwd: opts.rootDir, - }, - ); - - let changelog = await fs.readFile(opts.changelogPath, 'utf8'); - changelog = changelog.replace(/\# \[/, '# ' + opts.vermoji + ' ['); - await fs.writeFile(opts.changelogPath, changelog); -} - -/** - * Generate a GitHub release and create it. This function assumes that the CHANGELOG.md file has been written to disk. - * @param opts build options to be used to create a GitHub release - */ -export async function postGithubRelease(opts: BuildOptions): Promise { - const versionTag = `v${opts.version}`; - const title = `${opts.vermoji} ${opts.version}`; - - const lines = (await fs.readFile(opts.changelogPath, 'utf8')).trim().split('\n'); - - let body = ''; - for (let i = 1; i < 500; i++) { - const currentLine = lines[i]; - - if (currentLine == undefined) { - // we don't test this as `!currentLine`, as an empty string is permitted in the changelog - break; - } - - const isMajorOrMinorVersionHeader = currentLine.startsWith('# '); - const isPatchVersionHeader = currentLine.startsWith('## '); - if (isMajorOrMinorVersionHeader || isPatchVersionHeader) { - break; - } - body += currentLine + '\n'; - } - - // https://docs.github.com/en/github/administering-a-repository/automation-for-release-forms-with-query-parameters - const url = new URL(`https://github.com/${opts.ghRepoOrg}/${opts.ghRepoName}/releases/new`); - url.searchParams.set('tag', versionTag); - - const timestamp = new Date().toISOString().substring(0, 10); - - // this will be automatically encoded for us, no need to call `encodeURIComponent` here. doing so will result in a - // double encoding, which does not render properly in GitHub - url.searchParams.set('title', `${title} (${timestamp})`); - - url.searchParams.set('body', body.trim()); - if (opts.tag === 'next' || opts.tag === 'test') { - url.searchParams.set('prerelease', '1'); - } - - const open = (await import('open')).default; - await open(url.href); -} diff --git a/scripts/utils/test/options.spec.ts b/scripts/utils/test/options.spec.ts deleted file mode 100644 index 0a0a3d52d71..00000000000 --- a/scripts/utils/test/options.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -import path from 'path'; - -import { BuildOptions, getOptions } from '../options'; -import * as Vermoji from '../vermoji'; - -describe('release options', () => { - describe('getOptions', () => { - const ROOT_DIR = path.join(__dirname, '../../..'); - // Friday, February 24, 2023 2:42:09.123 PM, GMT - const FAKE_SYSTEM_TIME_MS = 1677249729123; - const FAKE_SYSTEM_TIME_S = FAKE_SYSTEM_TIME_MS.toString(10).slice(0, -3); - - beforeEach(() => { - jest.useFakeTimers(); - jest.setSystemTime(FAKE_SYSTEM_TIME_MS); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it('returns the correct default value', () => { - const buildOpts = getOptions(ROOT_DIR); - - expect(buildOpts).toEqual({ - buildDir: path.join(ROOT_DIR, 'build'), - // More focused tests for `buildId` can be found in another testing suite in this file - buildId: expect.any(String), - bundleHelpersDir: path.join(ROOT_DIR, 'scripts', 'esbuild', 'helpers'), - changelogPath: path.join(ROOT_DIR, 'CHANGELOG.md'), - ghRepoName: 'stencil', - ghRepoOrg: 'ionic-team', - isCI: false, - isProd: false, - isPublishRelease: false, - isWatch: false, - nodeModulesDir: path.join(ROOT_DIR, 'node_modules'), - output: { - cliDir: path.join(ROOT_DIR, 'cli'), - compilerDir: path.join(ROOT_DIR, 'compiler'), - devServerDir: path.join(ROOT_DIR, 'dev-server'), - internalDir: path.join(ROOT_DIR, 'internal'), - mockDocDir: path.join(ROOT_DIR, 'mock-doc'), - screenshotDir: path.join(ROOT_DIR, 'screenshot'), - sysNodeDir: path.join(ROOT_DIR, 'sys', 'node'), - testingDir: path.join(ROOT_DIR, 'testing'), - }, - // reads in package.json, skip it verifying it - packageJson: expect.any(Object), - packageJsonPath: path.join(ROOT_DIR, 'package.json'), - packageLockJsonPath: path.join(ROOT_DIR, 'package-lock.json'), - rootDir: ROOT_DIR, - scriptsBuildDir: path.join(ROOT_DIR, 'scripts', 'build'), - scriptsBundlesDir: path.join(ROOT_DIR, 'scripts', 'esbuild'), - scriptsDir: path.join(ROOT_DIR, 'scripts'), - srcDir: path.join(ROOT_DIR, 'src'), - tag: 'dev', - typescriptDir: path.join(ROOT_DIR, 'node_modules', 'typescript'), - typescriptLibDir: path.join(ROOT_DIR, 'node_modules', 'typescript', 'lib'), - vermoji: '💎', - // More focused tests for `version` can be found in another testing suite in this file - version: expect.any(String), - jqueryVersion: expect.any(String), - parse5Version: expect.any(String), - terserVersion: expect.any(String), - rollupVersion: expect.any(String), - typescriptVersion: expect.any(String), - }); - }); - - describe('buildId', () => { - it('defaults the buildId if none is provided', () => { - const { buildId } = getOptions(ROOT_DIR); - - expect(buildId).toBeDefined(); - expect(buildId).toBe(FAKE_SYSTEM_TIME_S); - }); - - it('uses the provided the buildId', () => { - const expectedBuildId = 'test-build-id'; - const { buildId } = getOptions(ROOT_DIR, { buildId: expectedBuildId }); - - expect(buildId).toBeDefined(); - expect(buildId).toBe(expectedBuildId); - }); - }); - - describe('version', () => { - it('defaults the version if none is provided', () => { - const { version } = getOptions(ROOT_DIR); - - expect(version).toBeDefined(); - // Expect a version string with the format 0.0.0-dev-[EPOCH_TIME]-[GIT_SHA_7_CHARS] - // or, contain a possible pre-release string like 0.0.0-beta.0-dev-[EPOCH_TIME]-[GIT_SHA_7_CHARS] - - expect(version).toMatch(new RegExp(`\\d+\\.\\d+\\.\\d+(-(.{1,}))?-dev.${FAKE_SYSTEM_TIME_S}.\\w{7}`)); - }); - - it('uses the provided version', () => { - const expectedVersion = '3.0.0-dev-1234'; - const { version } = getOptions(ROOT_DIR, { version: expectedVersion }); - - expect(version).toBeDefined(); - expect(version).toBe(expectedVersion); - }); - }); - - describe('publish + prod check', () => { - it("throws an error if 'isPublishRelease' is set, but Stencil is not built for 'isProd'", () => { - expect(() => getOptions(ROOT_DIR, { isProd: false, isPublishRelease: true })).toThrow( - 'release must also be a prod build', - ); - }); - - it.each>([ - { isProd: false }, - { isPublishRelease: false }, - { isProd: false, isPublishRelease: false }, - { isProd: true, isPublishRelease: false }, - { isProd: true, isPublishRelease: true }, - ])("does not throw an error for other combinations of 'isPublishRelease' and 'isProd'", (buildOpts) => { - expect(() => getOptions(ROOT_DIR, buildOpts)).not.toThrow(); - }); - }); - - describe('vermoji', () => { - let getVermojiSpy: jest.SpyInstance, Parameters>; - - beforeEach(() => { - getVermojiSpy = jest.spyOn(Vermoji, 'getVermoji'); - getVermojiSpy.mockImplementation((_changelogPath) => '🧀'); - }); - - afterEach(() => { - getVermojiSpy.mockRestore(); - }); - - it('defaults to 💎 for non-prod builds', () => { - expect(getOptions(ROOT_DIR).vermoji).toBe('💎'); - }); - - it.each>([ - { isProd: true, vermoji: '🦄' }, - { isProd: false, vermoji: '🦄' }, - ])("uses the provided vermoji, regardless of 'isProd'", () => { - const expectedVermoji = '🦄'; - const { vermoji } = getOptions(ROOT_DIR, { vermoji: expectedVermoji }); - expect(vermoji).toEqual('🦄'); - }); - - it('picks a new vermoji when none is provided for prod builds', () => { - const { vermoji } = getOptions(ROOT_DIR, { isProd: true }); - expect(vermoji).toEqual('🧀'); - }); - }); - }); -}); diff --git a/scripts/utils/test/release-utils.spec.ts b/scripts/utils/test/release-utils.spec.ts deleted file mode 100644 index f0cd4b7f0c6..00000000000 --- a/scripts/utils/test/release-utils.spec.ts +++ /dev/null @@ -1,168 +0,0 @@ -import fs from 'fs-extra'; - -import { BuildOptions } from '../options'; - -// `open` must be mocked before importing the module under test -const openMock = jest.fn(); -jest.mock('open', () => openMock); - -import { postGithubRelease } from '../release-utils'; - -describe('release-utils', () => { - describe('postGithubRelease', () => { - jest.useFakeTimers().setSystemTime(new Date('2022-01-01').getTime()); - - let buildOptions: BuildOptions; - - let mockReadFile: jest.SpyInstance, Parameters>; - - beforeEach(() => { - mockReadFile = jest.spyOn(fs, 'readFile'); - - buildOptions = { - changelogPath: 'some/mock/CHANGELOG.md', - ghRepoName: 'stencil', - ghRepoOrg: 'ionic-team', - tag: 'dev', - vermoji: '🚗', - version: '0.0.0', - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - afterAll(() => { - jest.resetAllMocks(); - }); - - it('creates an empty body if the changelog is empty', async () => { - // Jest isn't smart enough to pick the correct overloaded method, so we must do type assertions to get our spy to - // return a string (as if we called the original with an encoding argument) - mockReadFile.mockResolvedValue('' as unknown as Buffer); - - await postGithubRelease(buildOptions); - - expect(openMock).toHaveBeenCalledTimes(1); - expect(openMock).toHaveBeenCalledWith( - 'https://github.com/ionic-team/stencil/releases/new?tag=v0.0.0&title=%F0%9F%9A%97+0.0.0+%282022-01-01%29&body=', - ); - }); - - it('splits a minor release from a previous patch release', async () => { - const minorReleaseFollowingPatch = `# 🍣 [2.13.0](https://github.com/ionic-team/stencil/compare/v2.12.1...v2.13.0) (2022-01-24) - - -### Features - -* **mock-doc:** add simple MockEvent#composedPath() impl ([#3204](https://github.com/ionic-team/stencil/issues/3204)) ([7b47d96](https://github.com/ionic-team/stencil/commit/7b47d96e1e3c6c821d5c416fbe987646b4cd1551)) -* **test:** jest 27 support ([#3189](https://github.com/ionic-team/stencil/issues/3189)) ([10efeb6](https://github.com/ionic-team/stencil/commit/10efeb6f74888f05a13a47d8afc00b5e83a3f3db)) - - - -## 🍔 [2.12.1](https://github.com/ionic-team/stencil/compare/v2.12.0...v2.12.1) (2022-01-04) - - -### Bug Fixes - -* **vdom:** properly warn for step attr on input ([#3196](https://github.com/ionic-team/stencil/issues/3196)) ([7ffc02e](https://github.com/ionic-team/stencil/commit/7ffc02e5d07b05de45cbaf4f0cce3f3e165b3eb0)) - - -### Features - -* **typings:** add optional key and ref to slot elements ([#3177](https://github.com/ionic-team/stencil/issues/3177)) ([ce27a18](https://github.com/ionic-team/stencil/commit/ce27a18ba8ecdb2cc5401470747a7e9d91e40a44)) -`; - - // Jest isn't smart enough to pick the correct overloaded method, so we must do type assertions to get our spy to - // return a string (as if we called the original with an encoding argument) - mockReadFile.mockResolvedValue(minorReleaseFollowingPatch as unknown as Buffer); - - await postGithubRelease(buildOptions); - - expect(openMock).toHaveBeenCalledTimes(1); - expect(openMock).toHaveBeenCalledWith( - 'https://github.com/ionic-team/stencil/releases/new?tag=v0.0.0&title=%F0%9F%9A%97+0.0.0+%282022-01-01%29&body=%23%23%23+Features%0A%0A*+**mock-doc%3A**+add+simple+MockEvent%23composedPath%28%29+impl+%28%5B%233204%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F3204%29%29+%28%5B7b47d96%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fcommit%2F7b47d96e1e3c6c821d5c416fbe987646b4cd1551%29%29%0A*+**test%3A**+jest+27+support+%28%5B%233189%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F3189%29%29+%28%5B10efeb6%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fcommit%2F10efeb6f74888f05a13a47d8afc00b5e83a3f3db%29%29', - ); - }); - - it('splits a minor release from a previous minor release', async () => { - const minorReleaseFollowingMinor = `# ⛸ [2.12.0](https://github.com/ionic-team/stencil/compare/v2.11.0...v2.12.0) (2021-12-13) - - -### Bug Fixes - -* **cli:** wait for help task to finish before exiting ([#3160](https://github.com/ionic-team/stencil/issues/3160)) ([f10cee1](https://github.com/ionic-team/stencil/commit/f10cee12a8d00e7581fcf13216f01ded46227f49)) -* **mock-doc:** make Node.contains() return true for self ([#3150](https://github.com/ionic-team/stencil/issues/3150)) ([f164407](https://github.com/ionic-team/stencil/commit/f164407f7463faba7a3c39afca942c2a26210b82)) -* **mock-doc:** allow urls as css values ([#2857](https://github.com/ionic-team/stencil/issues/2857)) ([6faa5f2](https://github.com/ionic-team/stencil/commit/6faa5f2f196ff786ffc4b818ac09708ba5de9b35)) -* **sourcemaps:** do not encode inline sourcemaps ([#3163](https://github.com/ionic-team/stencil/issues/3163)) ([b2eb083](https://github.com/ionic-team/stencil/commit/b2eb083306802645ee6e31987917dea942882e46)), closes [#3147](https://github.com/ionic-team/stencil/issues/3147) - - -### Features - -* **dist-custom-elements-bundle:** add deprecation warning ([#3167](https://github.com/ionic-team/stencil/issues/3167)) ([c7b07c6](https://github.com/ionic-team/stencil/commit/c7b07c65265c7d4715f29835632cc6538ea63585)) - - - -# 🐌 [2.11.0](https://github.com/ionic-team/stencil/compare/v2.11.0-0...v2.11.0) (2021-11-22) - - -### Bug Fixes - -* **dist-custom-elements:** add ssr checks ([#3131](https://github.com/ionic-team/stencil/issues/3131)) ([9a232ea](https://github.com/ionic-team/stencil/commit/9a232ea368324f49993bd079cfdbc344abd0c69e)) - - -### Features - -* **css:** account for escaped ':' in css selectors ([#3087](https://github.com/ionic-team/stencil/issues/3087)) ([6000681](https://github.com/ionic-team/stencil/commit/600068168c86dba9ea610b5e8a0dbba00ff4d1f4)) -`; - - // Jest isn't smart enough to pick the correct overloaded method, so we must do type assertions to get our spy to - // return a string (as if we called the original with an encoding argument) - mockReadFile.mockResolvedValue(minorReleaseFollowingMinor as unknown as Buffer); - - await postGithubRelease(buildOptions); - - expect(openMock).toHaveBeenCalledTimes(1); - expect(openMock).toHaveBeenCalledWith( - 'https://github.com/ionic-team/stencil/releases/new?tag=v0.0.0&title=%F0%9F%9A%97+0.0.0+%282022-01-01%29&body=%23%23%23+Bug+Fixes%0A%0A*+**cli%3A**+wait+for+help+task+to+finish+before+exiting+%28%5B%233160%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F3160%29%29+%28%5Bf10cee1%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fcommit%2Ff10cee12a8d00e7581fcf13216f01ded46227f49%29%29%0A*+**mock-doc%3A**+make+Node.contains%28%29+return+true+for+self+%28%5B%233150%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F3150%29%29+%28%5Bf164407%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fcommit%2Ff164407f7463faba7a3c39afca942c2a26210b82%29%29%0A*+**mock-doc%3A**+allow+urls+as+css+values+%28%5B%232857%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F2857%29%29+%28%5B6faa5f2%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fcommit%2F6faa5f2f196ff786ffc4b818ac09708ba5de9b35%29%29%0A*+**sourcemaps%3A**+do+not+encode+inline+sourcemaps+%28%5B%233163%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F3163%29%29+%28%5Bb2eb083%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fcommit%2Fb2eb083306802645ee6e31987917dea942882e46%29%29%2C+closes+%5B%233147%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F3147%29%0A%0A%0A%23%23%23+Features%0A%0A*+**dist-custom-elements-bundle%3A**+add+deprecation+warning+%28%5B%233167%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F3167%29%29+%28%5Bc7b07c6%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fcommit%2Fc7b07c65265c7d4715f29835632cc6538ea63585%29%29', - ); - }); - - it('splits a patch release from a previous patch release', async () => { - const patchReleaseFollowingPatch = `## ♨️ [2.12.2](https://github.com/ionic-team/stencil/compare/v2.12.1...v2.12.2) (2022-01-24) - - -### Features - -* **mock-doc:** add simple MockEvent#composedPath() impl ([#3204](https://github.com/ionic-team/stencil/issues/3204)) ([7b47d96](https://github.com/ionic-team/stencil/commit/7b47d96e1e3c6c821d5c416fbe987646b4cd1551)) -* **test:** jest 27 support ([#3189](https://github.com/ionic-team/stencil/issues/3189)) ([10efeb6](https://github.com/ionic-team/stencil/commit/10efeb6f74888f05a13a47d8afc00b5e83a3f3db)) - - - -## 🍔 [2.12.1](https://github.com/ionic-team/stencil/compare/v2.12.0...v2.12.1) (2022-01-04) - - -### Bug Fixes - -* **vdom:** properly warn for step attr on input ([#3196](https://github.com/ionic-team/stencil/issues/3196)) ([7ffc02e](https://github.com/ionic-team/stencil/commit/7ffc02e5d07b05de45cbaf4f0cce3f3e165b3eb0)) - - -### Features - -* **typings:** add optional key and ref to slot elements ([#3177](https://github.com/ionic-team/stencil/issues/3177)) ([ce27a18](https://github.com/ionic-team/stencil/commit/ce27a18ba8ecdb2cc5401470747a7e9d91e40a44)) -`; - - // Jest isn't smart enough to pick the correct overloaded method, so we must do type assertions to get our spy to - // return a string (as if we called the original with an encoding argument) - mockReadFile.mockResolvedValue(patchReleaseFollowingPatch as unknown as Buffer); - - await postGithubRelease(buildOptions); - - expect(openMock).toHaveBeenCalledTimes(1); - expect(openMock).toHaveBeenCalledWith( - 'https://github.com/ionic-team/stencil/releases/new?tag=v0.0.0&title=%F0%9F%9A%97+0.0.0+%282022-01-01%29&body=%23%23%23+Features%0A%0A*+**mock-doc%3A**+add+simple+MockEvent%23composedPath%28%29+impl+%28%5B%233204%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F3204%29%29+%28%5B7b47d96%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fcommit%2F7b47d96e1e3c6c821d5c416fbe987646b4cd1551%29%29%0A*+**test%3A**+jest+27+support+%28%5B%233189%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fissues%2F3189%29%29+%28%5B10efeb6%5D%28https%3A%2F%2Fgithub.com%2Fionic-team%2Fstencil%2Fcommit%2F10efeb6f74888f05a13a47d8afc00b5e83a3f3db%29%29', - ); - }); - }); -}); diff --git a/scripts/utils/vermoji.ts b/scripts/utils/vermoji.ts deleted file mode 100644 index 00eecfff63c..00000000000 --- a/scripts/utils/vermoji.ts +++ /dev/null @@ -1,371 +0,0 @@ -import fs from 'fs-extra'; - -const UNKNOWN_VERMOJI = '❓'; - -let vermojis = [ - '💯', - '☀️', - '☕️', - '♨️', - '✈️', - '✨', - '❄️', - '❤️', - '☎️', - '⚡️', - '⚽️', - '⚾️', - '⛄️', - '⛑', - '⛰', - '⛱', - '⛲️', - '⛳️', - '⛴', - '⛵️', - '⛷', - '⛸', - '⛹', - '⛺️', - '⭐️', - '🌀', - '🌁', - '🌃', - '🌄', - '🌅', - '🌇', - '🌈', - '🌍', - '🌎', - '🌏', - '🌐', - '🌙', - '🌜', - '🌝', - '🌞', - '🌟', - '🌪', - '🌭', - '🌮', - '🌯', - '🌱', - '🌲', - '🌳', - '🌴', - '🌵', - '🌶', - '🌷', - '🌸', - '🌹', - '🌺', - '🌻', - '🌼', - '🍀', - '🍁', - '🍅', - '🍇', - '🍈', - '🍉', - '🍊', - '🍋', - '🍌', - '🍍', - '🍎', - '🍏', - '🍐', - '🍒', - '🍓', - '🍔', - '🍕', - '🍖', - '🍗', - '🍜', - '🍝', - '🍞', - '🍟', - '🍡', - '🍣', - '🍤', - '🍦', - '🍧', - '🍨', - '🍩', - '🍪', - '🍫', - '🍬', - '🍭', - '🍮', - '🍯', - '🍰', - '🍲', - '🍵', - '🍷', - '🍸', - '🍹', - '🍺', - '🍻', - '🥃', - '🍾', - '🍿', - '🎀', - '🎁', - '🎂', - '🎆', - '🎇', - '🎈', - '🎉', - '🎊', - '🎖', - '🎙', - '🎠', - '🎡', - '🎢', - '🎤', - '🎨', - '🎩', - '🎪', - '🎬', - '🎭', - '🎯', - '🎰', - '🎱', - '🎲', - '🎳', - '🎷', - '🎸', - '🎹', - '🎺', - '🎻', - '🎾', - '🎿', - '🏀', - '🏁', - '🏂', - '🏃', - '🏄', - '🏅', - '🏆', - '🏇', - '🏈', - '🏉', - '🏊', - '🏋', - '🏌', - '🏍', - '🏎', - '🏏', - '🏐', - '🏑', - '🏒', - '🏓', - '🏔', - '🏕', - '🏖', - '🏙', - '🏜', - '🏝', - '🏰', - '🏵', - '🏸', - '🏹', - '🐁', - '🐂', - '🐄', - '🐅', - '🐆', - '🐇', - '🐈', - '🐉', - '🐊', - '🐋', - '🐌', - '🐍', - '🐎', - '🐏', - '🐐', - '🐒', - '🐓', - '🐔', - '🐕', - '🐖', - '🐗', - '🐘', - '🐙', - '🐚', - '🐛', - '🐝', - '🐞', - '🐟', - '🐠', - '🐡', - '🐣', - '🐤', - '🐥', - '🐦', - '🐧', - '🐨', - '🐩', - '🐫', - '🐬', - '🐭', - '🐮', - '🐯', - '🐰', - '🐱', - '🐳', - '🐴', - '🐵', - '🐶', - '🐷', - '🐸', - '🐹', - '🐺', - '🐻', - '🐼', - '🐽', - '🐿', - '👑', - '👒', - '👻', - '👽', - '👾', - '💍', - '💙', - '💚', - '💛', - '💡', - '💥', - '💪', - '💫', - '💾', - '💿', - '📌', - '📍', - '📟', - '🛰', - '📢', - '📣', - '📬', - '📷', - '📺', - '📻', - '🔈', - '🔋', - '🔔', - '🔥', - '🔬', - '🔭', - '🔮', - '🕊', - '🕹', - '🖍', - '🗻', - '😀', - '😃', - '😄', - '😈', - '😊', - '😋', - '😎', - '😛', - '😜', - '😸', - '🤓', - '🤖', - '🚀', - '🚁', - '🚂', - '🚃', - '🚅', - '🚋', - '🚌', - '🚍', - '🚎', - '🚐', - '🚑', - '🚒', - '🚓', - '🚔', - '🚕', - '🚖', - '🚗', - '🚘', - '🚙', - '🚚', - '🚛', - '🚜', - '🚞', - '🚟', - '🚠', - '🚡', - '🚢', - '🚣', - '🚤', - '🚦', - '🚨', - '🚩', - '🛠', - '🛥', - '🛩', - '🛳', - '🤘', - '🦀', - '🦁', - '🦂', - '🦃', - '🦄', - '🧀', -]; - -// filter out the 'unknown version vermoji' -vermojis = vermojis.filter((vermoji) => vermoji !== UNKNOWN_VERMOJI); - -export function getVermoji(changelogPath: string) { - const changelog = fs.readFileSync(changelogPath, 'utf8'); - - while (true) { - const randomIndex = Math.floor(Math.random() * vermojis.length); - const vermoji = vermojis[randomIndex]; - if (changelog.includes(vermoji)) { - vermojis.splice(randomIndex, 1); - - if (vermojis.length === 0) { - console.warn(`We're out of Vermoji! Create a task to add some more!`); - return UNKNOWN_VERMOJI; - } - } else { - return vermoji; - } - } -} - -/** - * Pull the most recently used vermoji for the provided changelog path - * @param changelogPath the path to the changelog to parse - * @returns the vermoji found in the changelog, otherwise use a default value. - */ -export function getLatestVermoji(changelogPath: string) { - let changelogContents = null; - try { - changelogContents = fs.readFileSync(changelogPath, 'utf8'); - } catch (err: unknown) { - console.error(`Unable to read the changelog at path '${changelogPath}' - ${err}.`); - console.error(`Defaulting to ${UNKNOWN_VERMOJI}`); - return UNKNOWN_VERMOJI; - } - - if (!changelogContents) { - console.error(`The changelog at '${changelogPath}' was empty!`); - console.error(`Defaulting to ${UNKNOWN_VERMOJI}`); - return UNKNOWN_VERMOJI; - } - - // grab the first line of the changelog - const firstLine = changelogContents.trimStart().split('\n')[0]; - // match the first line of the changelog with a string that has: - // - one or more pound signs (#), followed by a space - // - capture the first non-space character(s) - const match = firstLine.match(/^#+\s(\S+)/); - // if a match was found, return the value in the first capture group. otherwise, use the default vermoji - return match ? match[1] : UNKNOWN_VERMOJI; -} diff --git a/scripts/utils/write-pkg-json.ts b/scripts/utils/write-pkg-json.ts deleted file mode 100644 index 1883e9ef1ba..00000000000 --- a/scripts/utils/write-pkg-json.ts +++ /dev/null @@ -1,76 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; - -import { BuildOptions } from './options'; - -export function writePkgJson(opts: BuildOptions, pkgDir: string, pkgData: PackageData) { - pkgData.version = opts.version; - pkgData.private = true; - - if (pkgData.main && !pkgData.main.startsWith('.')) { - pkgData.main = `./${pkgData.main}`; - } - if (pkgData.module && !pkgData.module.startsWith('.')) { - pkgData.module = `./${pkgData.module}`; - } - if (pkgData.types && !pkgData.types.startsWith('.')) { - pkgData.types = `./${pkgData.types}`; - } - - if (pkgData.module && pkgData.main) { - pkgData.type = 'module'; - pkgData.exports = { - import: pkgData.module, - require: pkgData.main, - }; - } - - // idk, i just like a nice pretty standardized order of package.json properties - const formatedPkg: any = {}; - PROPS_ORDER.forEach((pkgProp) => { - if (pkgProp in pkgData) { - formatedPkg[pkgProp] = pkgData[pkgProp as keyof PackageData]; - } - }); - - fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify(formatedPkg, null, 2) + '\n'); -} - -const PROPS_ORDER = [ - 'name', - 'version', - 'description', - 'bin', - 'main', - 'module', - 'browser', - 'types', - 'exports', - 'type', - 'files', - 'private', - 'sideEffects', -]; - -export interface PackageData { - name: string; - description: string; - main: string; - module?: string; - browser?: string; - exports?: any; - type?: string; - types?: string; - version?: string; - dependencies?: string[]; - private?: boolean; - license?: string | any; - licenses?: string | any; - author?: string | any; - contributors?: string | any; - homepage?: string; - repository?: any; - files?: string[]; - bin?: { [key: string]: string }; - sideEffects?: false; -} diff --git a/src/app-data/index.ts b/src/app-data/index.ts deleted file mode 100644 index 682e97095ba..00000000000 --- a/src/app-data/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { BuildConditionals } from '@stencil/core/internal'; - -/** - * A collection of default build flags for a Stencil project. - * - * This collection can be found throughout the Stencil codebase, often imported from the `@app-data` module like so: - * ```ts - * import { BUILD } from '@app-data'; - * ``` - * and is used to determine if a portion of the output of a Stencil _project_'s compilation step can be eliminated. - * - * e.g. When `BUILD.allRenderFn` evaluates to `false`, the compiler will eliminate conditional statements like: - * ```ts - * if (BUILD.allRenderFn) { - * // some code that will be eliminated if BUILD.allRenderFn is false - * } - * ``` - * - * `@app-data`, the module that `BUILD` is imported from, is an alias for the `@stencil/core/internal/app-data`, and is - * partially referenced by {@link STENCIL_APP_DATA_ID}. The `src/compiler/bundle/app-data-plugin.ts` references - * `STENCIL_APP_DATA_ID` uses it to replace these defaults with {@link BuildConditionals} that are derived from a - * Stencil project's contents (i.e. metadata from the components). This replacement happens at a Stencil project's - * compile time. Such code can be found at `src/compiler/app-core/app-data.ts`. - */ -export const BUILD: BuildConditionals = { - allRenderFn: false, - element: true, - event: true, - hasRenderFn: true, - hostListener: true, - hostListenerTargetWindow: true, - hostListenerTargetDocument: true, - hostListenerTargetBody: true, - hostListenerTargetParent: false, - hostListenerTarget: true, - member: true, - method: true, - mode: true, - observeAttribute: true, - prop: true, - propMutable: true, - reflect: true, - scoped: true, - shadowDom: true, - slot: true, - cssAnnotations: true, - state: true, - style: true, - formAssociated: false, - svg: true, - updatable: true, - vdomAttribute: true, - vdomXlink: true, - vdomClass: true, - vdomFunctional: true, - vdomKey: true, - vdomListener: true, - vdomRef: true, - vdomPropOrAttr: true, - vdomRender: true, - vdomStyle: true, - vdomText: true, - propChangeCallback: true, - taskQueue: true, - hotModuleReplacement: false, - isDebug: false, - isDev: false, - isTesting: false, - hydrateServerSide: false, - hydrateClientSide: false, - lifecycleDOMEvents: false, - lazyLoad: false, - profile: false, - slotRelocation: true, - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - appendChildSlotFix: false, - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - cloneNodeFix: false, - hydratedAttribute: false, - hydratedClass: true, - // TODO(STENCIL-1305): remove this option - scriptDataOpts: false, - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - scopedSlotTextContentFix: false, - // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field - shadowDomShim: false, - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - slotChildNodesFix: false, - invisiblePrehydration: true, - propBoolean: true, - propNumber: true, - propString: true, - constructableCSS: true, - devTools: false, - shadowDelegatesFocus: true, - shadowSlotAssignmentManual: false, - initializeNextTick: false, - asyncLoading: true, - asyncQueue: false, - // TODO: deprecated in favour of `setTagTransformer` and `transformTag`. Remove in 5.0 - transformTagName: false, - attachStyles: true, - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - experimentalSlotFixes: false, -}; - -export const Env = {}; - -export const NAMESPACE = /* default */ 'app' as string; diff --git a/src/cli/check-version.ts b/src/cli/check-version.ts deleted file mode 100644 index 7b03e1cb206..00000000000 --- a/src/cli/check-version.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { isFunction } from '@utils'; - -import type { ValidatedConfig } from '../declarations'; - -/** - * Retrieve a reference to the active `CompilerSystem`'s `checkVersion` function - * @param config the Stencil configuration associated with the currently compiled project - * @param currentVersion the Stencil compiler's version string - * @returns a reference to `checkVersion`, or `null` if one does not exist on the current `CompilerSystem` - */ -export const startCheckVersion = async ( - config: ValidatedConfig, - currentVersion: string, -): Promise<(() => void) | null> => { - if (config.devMode && !config.flags.ci && !currentVersion.includes('-dev.') && isFunction(config.sys.checkVersion)) { - return config.sys.checkVersion(config.logger, currentVersion); - } - return null; -}; - -/** - * Print the results of running the provided `versionChecker`. - * - * Does not print if no `versionChecker` is provided. - * - * @param versionChecker the function to invoke. - */ -export const printCheckVersionResults = async (versionChecker: Promise<(() => void) | null>): Promise => { - if (versionChecker) { - const checkVersionResults = await versionChecker; - if (isFunction(checkVersionResults)) { - checkVersionResults(); - } - } -}; diff --git a/src/cli/config-flags.ts b/src/cli/config-flags.ts deleted file mode 100644 index 308313ab7ec..00000000000 --- a/src/cli/config-flags.ts +++ /dev/null @@ -1,366 +0,0 @@ -import type { LogLevel, TaskCommand } from '@stencil/core/declarations'; - -/** - * All the Boolean options supported by the Stencil CLI - */ -export const BOOLEAN_CLI_FLAGS = [ - 'build', - 'cache', - 'checkVersion', - 'ci', - 'compare', - 'debug', - 'dev', - 'devtools', - 'docs', - // @deprecated - integrated testing will be removed in Stencil v5. See https://github.com/stenciljs/core/issues/6584. - 'e2e', - 'es5', - 'esm', - 'help', - 'log', - 'open', - 'prerender', - 'prerenderExternal', - 'prod', - 'profile', - 'serviceWorker', - // @deprecated - screenshot testing will be removed in Stencil v5. See https://github.com/stenciljs/core/issues/6584. - 'screenshot', - 'serve', - 'skipNodeCheck', - // @deprecated - integrated testing will be removed in Stencil v5. See https://github.com/stenciljs/core/issues/6584. - 'spec', - 'ssr', - // @deprecated - screenshot testing will be removed in Stencil v5. See https://github.com/stenciljs/core/issues/6584. - 'updateScreenshot', - 'verbose', - 'version', - 'watch', - - // @deprecated - all JEST CLI options below are only used by integrated testing, which will be removed in Stencil v5. - // See https://github.com/stenciljs/core/issues/6584. - // JEST CLI OPTIONS - 'all', - 'automock', - 'bail', - // 'cache', Stencil already supports this argument - 'changedFilesWithAncestor', - // 'ci', Stencil already supports this argument - 'clearCache', - 'clearMocks', - 'collectCoverage', - 'color', - 'colors', - 'coverage', - // 'debug', Stencil already supports this argument - 'detectLeaks', - 'detectOpenHandles', - 'errorOnDeprecated', - 'expand', - 'findRelatedTests', - 'forceExit', - 'init', - 'injectGlobals', - 'json', - 'lastCommit', - 'listTests', - 'logHeapUsage', - 'noStackTrace', - 'notify', - 'onlyChanged', - 'onlyFailures', - 'passWithNoTests', - 'resetMocks', - 'resetModules', - 'restoreMocks', - 'runInBand', - 'runTestsByPath', - 'showConfig', - 'silent', - 'skipFilter', - 'testLocationInResults', - 'updateSnapshot', - 'useStderr', - // 'verbose', Stencil already supports this argument - // 'version', Stencil already supports this argument - // 'watch', Stencil already supports this argument - 'watchAll', - 'watchman', -] as const; - -/** - * All the Number options supported by the Stencil CLI - */ -export const NUMBER_CLI_FLAGS = [ - 'port', - // @deprecated - all JEST CLI args below are only used by integrated testing, which will be removed in Stencil v5. - // See https://github.com/stenciljs/core/issues/6584. - // JEST CLI ARGS - 'maxConcurrency', - 'testTimeout', -] as const; - -/** - * All the String options supported by the Stencil CLI - */ -export const STRING_CLI_FLAGS = [ - 'address', - 'config', - 'docsApi', - 'docsJson', - 'emulate', - 'root', - // @deprecated - screenshot testing will be removed in Stencil v5. See https://github.com/stenciljs/core/issues/6584. - 'screenshotConnector', - - // @deprecated - all JEST CLI args below are only used by integrated testing, which will be removed in Stencil v5. - // See https://github.com/stenciljs/core/issues/6584. - // JEST CLI ARGS - 'cacheDirectory', - 'changedSince', - 'collectCoverageFrom', - // 'config', Stencil already supports this argument - 'coverageDirectory', - 'coverageThreshold', - 'env', - 'filter', - 'globalSetup', - 'globalTeardown', - 'globals', - 'haste', - 'moduleNameMapper', - 'notifyMode', - 'outputFile', - 'preset', - 'prettierPath', - 'resolver', - 'rootDir', - 'runner', - 'testEnvironment', - 'testEnvironmentOptions', - 'testFailureExitCode', - 'testNamePattern', - 'testResultsProcessor', - 'testRunner', - 'testSequencer', - 'testURL', - 'timers', - 'transform', -] as const; - -// @deprecated - all entries below are JEST CLI args only used by integrated testing, which will be removed in Stencil v5. -// See https://github.com/stenciljs/core/issues/6584. -export const STRING_ARRAY_CLI_FLAGS = [ - 'collectCoverageOnlyFrom', - 'coveragePathIgnorePatterns', - 'coverageReporters', - 'moduleDirectories', - 'moduleFileExtensions', - 'modulePathIgnorePatterns', - 'modulePaths', - 'projects', - 'reporters', - 'roots', - 'selectProjects', - 'setupFiles', - 'setupFilesAfterEnv', - 'snapshotSerializers', - 'testMatch', - 'testPathIgnorePatterns', - 'testPathPattern', - 'testRegex', - 'transformIgnorePatterns', - 'unmockedModulePathPatterns', - 'watchPathIgnorePatterns', -] as const; - -/** - * All the CLI arguments which may have string or number values - * - * `maxWorkers` is an argument which is used both by Stencil _and_ by Jest, - * which means that we need to support parsing both string and number values. - */ -export const STRING_NUMBER_CLI_FLAGS = ['maxWorkers'] as const; - -/** - * All the CLI arguments which may have boolean or string values. - */ -export const BOOLEAN_STRING_CLI_FLAGS = [ - /** - * `headless` is an argument passed through to Puppeteer (which is passed to Chrome) for end-to-end testing. - * - * {@see https://developer.chrome.com/blog/chrome-headless-shell/} - */ - 'headless', - /** - * `stats` is an argument that can optionally accept a file path where stats should be written. - * When used as a boolean (--stats), it defaults to 'stencil-stats.json'. - * When used with a path (--stats dist/stats.json), it writes to that path. - */ - 'stats', -] as const; - -/** - * All the LogLevel-type options supported by the Stencil CLI - * - * This is a bit silly since there's only one such argument atm, - * but this approach lets us make sure that we're handling all - * our arguments in a type-safe way. - */ -export const LOG_LEVEL_CLI_FLAGS = ['logLevel'] as const; - -/** - * A type which gives the members of a `ReadonlyArray` as - * an enum-like type which can be used for e.g. keys in a `Record` - * (as in the `AliasMap` type below) - */ -type ArrayValuesAsUnion> = T[number]; - -export type BooleanCLIFlag = ArrayValuesAsUnion; -export type StringCLIFlag = ArrayValuesAsUnion; -export type StringArrayCLIFlag = ArrayValuesAsUnion; -export type NumberCLIFlag = ArrayValuesAsUnion; -export type StringNumberCLIFlag = ArrayValuesAsUnion; -export type BooleanStringCLIFlag = ArrayValuesAsUnion; -export type LogCLIFlag = ArrayValuesAsUnion; - -export type KnownCLIFlag = - | BooleanCLIFlag - | StringCLIFlag - | StringArrayCLIFlag - | NumberCLIFlag - | StringNumberCLIFlag - | BooleanStringCLIFlag - | LogCLIFlag; - -type AliasMap = Partial>; - -/** - * For a small subset of CLI options we support a short alias e.g. `'h'` for `'help'` - */ -export const CLI_FLAG_ALIASES: AliasMap = { - c: 'config', - h: 'help', - p: 'port', - v: 'version', - - // JEST SPECIFIC CLI FLAGS - // these are defined in - // https://github.com/facebook/jest/blob/4156f86/packages/jest-cli/src/args.ts - b: 'bail', - e: 'expand', - f: 'onlyFailures', - i: 'runInBand', - o: 'onlyChanged', - t: 'testNamePattern', - u: 'updateSnapshot', - w: 'maxWorkers', -}; - -/** - * A regular expression which can be used to match a CLI flag for one of our - * short aliases. - */ -export const CLI_FLAG_REGEX = new RegExp(`^-[chpvbewofitu]{1}$`); - -/** - * Given two types `K` and `T` where `K` extends `ReadonlyArray`, - * construct a type which maps the strings in `K` as keys to values of type `T`. - * - * Because we use types derived this way to construct an interface (`ConfigFlags`) - * for which we want optional keys, we make all the properties optional (w/ `'?'`) - * and possibly null. - */ -type ObjectFromKeys, T> = { - [key in K[number]]?: T | null; -}; - -/** - * Type containing the possible Boolean configuration flags, to be included - * in ConfigFlags, below - */ -type BooleanConfigFlags = ObjectFromKeys; - -/** - * Type containing the possible String configuration flags, to be included - * in ConfigFlags, below - */ -type StringConfigFlags = ObjectFromKeys; - -/** - * Type containing the possible String Array configuration flags. This is - * one of the 'constituent types' for `ConfigFlags`. - */ -type StringArrayConfigFlags = ObjectFromKeys; - -/** - * Type containing the possible numeric configuration flags, to be included - * in ConfigFlags, below - */ -type NumberConfigFlags = ObjectFromKeys; - -/** - * Type containing the configuration flags which may be set to either string - * or number values. - */ -type StringNumberConfigFlags = ObjectFromKeys; - -/** - * Type containing the configuration flags which may be set to either string - * or boolean values. - */ -type BooleanStringConfigFlags = ObjectFromKeys; - -/** - * Type containing the possible LogLevel configuration flags, to be included - * in ConfigFlags, below - */ -type LogLevelFlags = ObjectFromKeys; - -/** - * The configuration flags which can be set by the user on the command line. - * This interface captures both known arguments (which are enumerated and then - * parsed according to their types) and unknown arguments which the user may - * pass at the CLI. - * - * Note that this interface is constructed by extending `BooleanConfigFlags`, - * `StringConfigFlags`, etc. These types are in turn constructed from types - * extending `ReadonlyArray` which we declare in another module. This - * allows us to record our known CLI arguments in one place, using a - * `ReadonlyArray` to get both a type-level representation of what CLI - * options we support and a runtime list of strings which can be used to match - * on actual flags passed by the user. - */ -export interface ConfigFlags - extends BooleanConfigFlags, - StringConfigFlags, - StringArrayConfigFlags, - NumberConfigFlags, - StringNumberConfigFlags, - BooleanStringConfigFlags, - LogLevelFlags { - task: TaskCommand | null; - args: string[]; - knownArgs: string[]; - unknownArgs: string[]; -} - -/** - * Helper function for initializing a `ConfigFlags` object. Provide any overrides - * for default values and off you go! - * - * @param init an object with any overrides for default values - * @returns a complete CLI flag object - */ -export const createConfigFlags = (init: Partial = {}): ConfigFlags => { - const flags: ConfigFlags = { - task: null, - args: [], - knownArgs: [], - unknownArgs: [], - ...init, - }; - - return flags; -}; diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100644 index 2ee3e728ac6..00000000000 --- a/src/cli/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { BOOLEAN_CLI_FLAGS, ConfigFlags } from './config-flags'; -export { parseFlags } from './parse-flags'; -export { run, runTask } from './run'; diff --git a/src/cli/ionic-config.ts b/src/cli/ionic-config.ts deleted file mode 100644 index 0014d490bff..00000000000 --- a/src/cli/ionic-config.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type * as d from '../declarations'; -import { readJson, UUID_REGEX, uuidv4 } from './telemetry/helpers'; - -export const isTest = () => process.env.JEST_WORKER_ID !== undefined; - -export const defaultConfig = (sys: d.CompilerSystem) => - sys.resolvePath(`${sys.homeDir()}/.ionic/${isTest() ? 'tmp-config.json' : 'config.json'}`); - -export const defaultConfigDirectory = (sys: d.CompilerSystem) => sys.resolvePath(`${sys.homeDir()}/.ionic`); - -/** - * Reads an Ionic configuration file from disk, parses it, and performs any necessary corrections to it if certain - * values are deemed to be malformed - * @param sys The system where the command is invoked - * @returns the config read from disk that has been potentially been updated - */ -export async function readConfig(sys: d.CompilerSystem): Promise { - let config: d.TelemetryConfig = await readJson(sys, defaultConfig(sys)); - - if (!config) { - config = { - 'tokens.telemetry': uuidv4(), - 'telemetry.stencil': true, - }; - - await writeConfig(sys, config); - } else if (!config['tokens.telemetry'] || !UUID_REGEX.test(config['tokens.telemetry'])) { - const newUuid = uuidv4(); - await writeConfig(sys, { ...config, 'tokens.telemetry': newUuid }); - config['tokens.telemetry'] = newUuid; - } - - return config; -} - -/** - * Writes an Ionic configuration file to disk. - * @param sys The system where the command is invoked - * @param config The config passed into the Stencil command - * @returns boolean If the command was successful - */ -export async function writeConfig(sys: d.CompilerSystem, config: d.TelemetryConfig): Promise { - let result = false; - try { - await sys.createDir(defaultConfigDirectory(sys), { recursive: true }); - await sys.writeFile(defaultConfig(sys), JSON.stringify(config, null, 2)); - result = true; - } catch (error) { - console.error(`Stencil Telemetry: couldn't write configuration file to ${defaultConfig(sys)} - ${error}.`); - } - - return result; -} - -/** - * Update a subset of the Ionic config. - * @param sys The system where the command is invoked - * @param newOptions The new options to save - * @returns boolean If the command was successful - */ -export async function updateConfig(sys: d.CompilerSystem, newOptions: d.TelemetryConfig): Promise { - const config = await readConfig(sys); - return await writeConfig(sys, Object.assign(config, newOptions)); -} diff --git a/src/cli/public.ts b/src/cli/public.ts deleted file mode 100644 index 75337990120..00000000000 --- a/src/cli/public.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CliInitOptions, Config, Logger, TaskCommand } from '@stencil/core/internal'; - -import type { ConfigFlags } from './config-flags'; - -/** - * Runs the CLI with the given options. This is used by Stencil's default `bin/stencil` file, - * but can be used externally too. - * @param init a set of initialization options needed to run Stencil from its CLI - * @returns an empty promise - */ -export declare function run(init: CliInitOptions): Promise; - -/** - * Run individual CLI tasks. - * @param coreCompiler The core Stencil compiler to be used. The `run()` method handles loading the core compiler, however, `runTask()` must be passed it. - * @param config Assumes the config has already been validated and has the "sys" and "logger" properties. - * @param task The task command to run, such as `build`. - * @returns an empty promise - */ -export declare function runTask(coreCompiler: any, config: Config, task: TaskCommand): Promise; - -export declare function parseFlags(args: string[]): ConfigFlags; - -export { Config, ConfigFlags, Logger, TaskCommand }; diff --git a/src/cli/run.ts b/src/cli/run.ts deleted file mode 100644 index ea727235974..00000000000 --- a/src/cli/run.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { hasError, isFunction, result, shouldIgnoreError } from '@utils'; - -import type * as d from '../declarations'; -import { ValidatedConfig } from '../declarations'; -import { createConfigFlags } from './config-flags'; -import { findConfig } from './find-config'; -import { CoreCompiler, loadCoreCompiler } from './load-compiler'; -import { loadedCompilerLog, startupLog, startupLogVersion } from './logs'; -import { parseFlags } from './parse-flags'; -import { taskBuild } from './task-build'; -import { taskDocs } from './task-docs'; -import { taskGenerate } from './task-generate'; -import { taskHelp } from './task-help'; -import { taskInfo } from './task-info'; -import { taskPrerender } from './task-prerender'; -import { taskServe } from './task-serve'; -import { taskTelemetry } from './task-telemetry'; -import { taskTest } from './task-test'; -import { telemetryAction } from './telemetry/telemetry'; - -/** - * Main entry point for the Stencil CLI - * - * Take care of parsing CLI arguments, initializing various components needed - * by the rest of the program, and kicking off the correct task (build, test, - * etc). - * - * @param init initial CLI options - * @returns an empty promise - */ -export const run = async (init: d.CliInitOptions) => { - const { args, logger, sys } = init; - - try { - const flags = parseFlags(args); - const task = flags.task; - - if (flags.debug || flags.verbose) { - logger.setLevel('debug'); - } - - if (flags.ci) { - logger.enableColors(false); - } - - if (isFunction(sys.applyGlobalPatch)) { - sys.applyGlobalPatch(sys.getCurrentDirectory()); - } - - if ((task && task === 'version') || flags.version) { - // we need to load the compiler here to get the version, but we don't - // want to load it in the case that we're going to just log the help - // message and then exit below (if there's no `task` defined) so we load - // it just within our `if` scope here. - const coreCompiler = await loadCoreCompiler(sys); - console.log(coreCompiler.version); - return; - } - - if (!task || task === 'help' || flags.help) { - await taskHelp(createConfigFlags({ task: 'help', args }), logger, sys); - - return; - } - - startupLog(logger, task); - - const findConfigResults = await findConfig({ sys, configPath: flags.config }); - if (findConfigResults.isErr) { - logger.printDiagnostics(findConfigResults.value); - return sys.exit(1); - } - - const coreCompiler = await loadCoreCompiler(sys); - - startupLogVersion(logger, task, coreCompiler); - - loadedCompilerLog(sys, logger, flags, coreCompiler); - - if (task === 'info') { - taskInfo(coreCompiler, sys, logger); - return; - } - - const foundConfig = result.unwrap(findConfigResults); - const validated = await coreCompiler.loadConfig({ - config: { - flags, - }, - configPath: foundConfig.configPath, - logger, - sys, - }); - - if (validated.diagnostics.length > 0) { - logger.printDiagnostics(validated.diagnostics); - if (hasError(validated.diagnostics)) { - return sys.exit(1); - } - } - - if (isFunction(sys.applyGlobalPatch)) { - sys.applyGlobalPatch(validated.config.rootDir); - } - - await telemetryAction(sys, validated.config, coreCompiler, async () => { - await runTask(coreCompiler, validated.config, task, sys); - }); - } catch (e) { - if (!shouldIgnoreError(e)) { - const details = `${logger.getLevel() === 'debug' && e instanceof Error ? e.stack : ''}`; - logger.error(`uncaught cli error: ${e}${details}`); - return sys.exit(1); - } - } -}; - -/** - * Run a specified task - * - * @param coreCompiler an instance of a minimal, bootstrap compiler for running the specified task - * @param config a configuration for the Stencil project to apply to the task run - * @param task the task to run - * @param sys the {@link d.CompilerSystem} for interacting with the operating system - * @public - * @returns a void promise - */ -export const runTask = async ( - coreCompiler: CoreCompiler, - config: d.Config, - task: d.TaskCommand, - sys: d.CompilerSystem, -): Promise => { - const flags = createConfigFlags(config.flags ?? { task }); - config.flags = flags; - - if (!config.sys) { - config.sys = sys; - } - const strictConfig: ValidatedConfig = coreCompiler.validateConfig(config, {}).config; - - switch (task) { - case 'build': - await taskBuild(coreCompiler, strictConfig); - break; - - case 'docs': - await taskDocs(coreCompiler, strictConfig); - break; - - case 'generate': - case 'g': - await taskGenerate(strictConfig); - break; - - case 'help': - await taskHelp(strictConfig.flags, strictConfig.logger, sys); - break; - - case 'prerender': - await taskPrerender(coreCompiler, strictConfig); - break; - - case 'serve': - await taskServe(strictConfig); - break; - - case 'telemetry': - await taskTelemetry(strictConfig.flags, sys, strictConfig.logger); - break; - - case 'test': - await taskTest(strictConfig); - break; - - case 'version': - console.log(coreCompiler.version); - break; - - default: - strictConfig.logger.error( - `${strictConfig.logger.emoji('❌ ')}Invalid stencil command, please see the options below:`, - ); - await taskHelp(strictConfig.flags, strictConfig.logger, sys); - return config.sys.exit(1); - } -}; diff --git a/src/cli/task-build.ts b/src/cli/task-build.ts deleted file mode 100644 index cbe03761b1c..00000000000 --- a/src/cli/task-build.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type * as d from '../declarations'; -import { printCheckVersionResults, startCheckVersion } from './check-version'; -import type { CoreCompiler } from './load-compiler'; -import { startupCompilerLog } from './logs'; -import { runPrerenderTask } from './task-prerender'; -import { taskWatch } from './task-watch'; -import { telemetryBuildFinishedAction } from './telemetry/telemetry'; - -export const taskBuild = async (coreCompiler: CoreCompiler, config: d.ValidatedConfig) => { - if (config.flags.watch) { - // watch build - await taskWatch(coreCompiler, config); - return; - } - - // one-time build - let exitCode = 0; - - try { - startupCompilerLog(coreCompiler, config); - - const versionChecker = startCheckVersion(config, coreCompiler.version); - - const compiler = await coreCompiler.createCompiler(config); - const results = await compiler.build(); - - await telemetryBuildFinishedAction(config.sys, config, coreCompiler, results); - - await compiler.destroy(); - - if (results.hasError) { - exitCode = 1; - } else if (config.flags.prerender) { - const prerenderDiagnostics = await runPrerenderTask( - coreCompiler, - config, - results.hydrateAppFilePath, - results.componentGraph, - undefined, - ); - config.logger.printDiagnostics(prerenderDiagnostics); - - if (prerenderDiagnostics.some((d) => d.level === 'error')) { - exitCode = 1; - } - } - - await printCheckVersionResults(versionChecker); - } catch (e) { - exitCode = 1; - config.logger.error(e); - } - - if (exitCode > 0) { - return config.sys.exit(exitCode); - } -}; diff --git a/src/cli/task-prerender.ts b/src/cli/task-prerender.ts deleted file mode 100644 index 1e4bc341641..00000000000 --- a/src/cli/task-prerender.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { catchError } from '@utils'; - -import type { BuildResultsComponentGraph, Diagnostic, ValidatedConfig } from '../declarations'; -import type { CoreCompiler } from './load-compiler'; -import { startupCompilerLog } from './logs'; - -export const taskPrerender = async (coreCompiler: CoreCompiler, config: ValidatedConfig) => { - startupCompilerLog(coreCompiler, config); - - const hydrateAppFilePath = config.flags.unknownArgs[0]; - - if (typeof hydrateAppFilePath !== 'string') { - config.logger.error(`Missing hydrate app script path`); - return config.sys.exit(1); - } - - const srcIndexHtmlPath = config.srcIndexHtml; - - const diagnostics = await runPrerenderTask(coreCompiler, config, hydrateAppFilePath, undefined, srcIndexHtmlPath); - config.logger.printDiagnostics(diagnostics); - - if (diagnostics.some((d) => d.level === 'error')) { - return config.sys.exit(1); - } -}; - -export const runPrerenderTask = async ( - coreCompiler: CoreCompiler, - config: ValidatedConfig, - hydrateAppFilePath?: string, - componentGraph?: BuildResultsComponentGraph, - srcIndexHtmlPath?: string, -) => { - const diagnostics: Diagnostic[] = []; - - try { - const prerenderer = await coreCompiler.createPrerenderer(config); - const results = await prerenderer.start({ - hydrateAppFilePath, - componentGraph, - srcIndexHtmlPath, - }); - - diagnostics.push(...results.diagnostics); - } catch (e: any) { - catchError(diagnostics, e); - } - - return diagnostics; -}; diff --git a/src/cli/task-serve.ts b/src/cli/task-serve.ts deleted file mode 100644 index 80e4a9e6376..00000000000 --- a/src/cli/task-serve.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { isString } from '@utils'; - -import type { ValidatedConfig } from '../declarations'; - -export const taskServe = async (config: ValidatedConfig) => { - config.suppressLogs = true; - - config.flags.serve = true; - config.devServer.openBrowser = !!config.flags.open; - config.devServer.reloadStrategy = null; - config.devServer.initialLoadUrl = '/'; - config.devServer.websocket = false; - config.maxConcurrentWorkers = 1; - config.devServer.root = isString(config.flags.root) ? config.flags.root : config.sys.getCurrentDirectory(); - - if (!config.sys.getDevServerExecutingPath || !config.sys.dynamicImport || !config.sys.onProcessInterrupt) { - throw new Error( - `Environment doesn't provide required functions: getDevServerExecutingPath, dynamicImport, onProcessInterrupt`, - ); - } - - const devServerPath = config.sys.getDevServerExecutingPath(); - const { start }: typeof import('@stencil/core/dev-server') = await config.sys.dynamicImport(devServerPath); - const devServer = await start(config.devServer, config.logger); - - console.log(`${config.logger.cyan(' Root:')} ${devServer.root}`); - console.log(`${config.logger.cyan(' Address:')} ${devServer.address}`); - console.log(`${config.logger.cyan(' Port:')} ${devServer.port}`); - console.log(`${config.logger.cyan(' Server:')} ${devServer.browserUrl}`); - console.log(``); - - config.sys.onProcessInterrupt(() => { - if (devServer) { - config.logger.debug(`dev server close: ${devServer.browserUrl}`); - devServer.close(); - } - }); -}; diff --git a/src/cli/task-test.ts b/src/cli/task-test.ts deleted file mode 100644 index 2073ac5cb1b..00000000000 --- a/src/cli/task-test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { TestingRunOptions, ValidatedConfig } from '../declarations'; - -/** - * Entrypoint for any Stencil tests - * @param config a validated Stencil configuration entity - * @returns a void promise - */ -export const taskTest = async (config: ValidatedConfig): Promise => { - config.logger.warn( - config.logger.yellow( - `[DEPRECATION] Stencil's integrated testing (the 'test' task, --spec and --e2e flags) is deprecated and will be removed in Stencil v5. ` + - `Migrate spec tests to @stencil/vitest (https://github.com/stenciljs/vitest) and ` + - `e2e / browser tests to either @stencil/vitest (https://github.com/stenciljs/vitest) or ` + - `@stencil/playwright (https://github.com/stenciljs/playwright). ` + - `See https://github.com/stenciljs/core/issues/6584 for full details.`, - ), - ); - config.buildDocs = false; - const testingRunOpts: TestingRunOptions = { - e2e: !!config.flags.e2e, - screenshot: !!config.flags.screenshot, - spec: !!config.flags.spec, - updateScreenshot: !!config.flags.updateScreenshot, - }; - - // always ensure we have jest modules installed - const ensureModuleIds = ['@types/jest', 'jest', 'jest-cli']; - - if (testingRunOpts.e2e) { - // if it's an e2e test, also make sure we're got - // puppeteer modules installed and if browserExecutablePath is provided don't download Chromium use only puppeteer-core instead - const puppeteer = config.testing.browserExecutablePath ? 'puppeteer-core' : 'puppeteer'; - - ensureModuleIds.push(puppeteer); - - if (testingRunOpts.screenshot) { - // ensure we've got pixelmatch for screenshots - config.logger.warn( - config.logger.yellow( - `EXPERIMENTAL: screenshot visual diff testing is currently under heavy development and has not reached a stable status. However, any assistance testing would be appreciated.`, - ), - ); - } - } - - // ensure we've got the required modules installed - const diagnostics = await config.sys.lazyRequire?.ensure(config.rootDir, ensureModuleIds); - if (diagnostics && diagnostics.length > 0) { - config.logger.printDiagnostics(diagnostics); - return config.sys.exit(1); - } - - try { - /** - * We dynamically import the testing submodule here in order for Stencil's lazy module checking to work properly. - * - * Prior to this call, we create a collection of string-based node module names and ensure that they're installed & - * on disk. The testing submodule includes `jest` (amongst other) testing libraries in its dependency chain. We need - * to run the lazy module check _before_ we include `jest` et al. in our dependency chain otherwise, the lazy module - * checking would fail to run properly (because we'd import `jest`, which wouldn't exist, before we even checked if - * it was installed). - */ - const { createTesting } = await import('@stencil/core/testing'); - const testing = await createTesting(config); - const passed = await testing.run(testingRunOpts); - await testing.destroy(); - - if (!passed) { - return config.sys.exit(1); - } - } catch (e) { - config.logger.error(e); - return config.sys.exit(1); - } -}; diff --git a/src/cli/task-watch.ts b/src/cli/task-watch.ts deleted file mode 100644 index 64305be4e35..00000000000 --- a/src/cli/task-watch.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { DevServer, ValidatedConfig } from '../declarations'; -import { printCheckVersionResults, startCheckVersion } from './check-version'; -import type { CoreCompiler } from './load-compiler'; -import { startupCompilerLog } from './logs'; - -export const taskWatch = async (coreCompiler: CoreCompiler, config: ValidatedConfig) => { - let devServer: DevServer | null = null; - let exitCode = 0; - - try { - startupCompilerLog(coreCompiler, config); - - const versionChecker = startCheckVersion(config, coreCompiler.version); - - const compiler = await coreCompiler.createCompiler(config); - const watcher = await compiler.createWatcher(); - - if (!config.sys.getDevServerExecutingPath || !config.sys.dynamicImport || !config.sys.onProcessInterrupt) { - throw new Error( - `Environment doesn't provide required functions: getDevServerExecutingPath, dynamicImport, onProcessInterrupt`, - ); - } - - if (config.flags.serve) { - const devServerPath = config.sys.getDevServerExecutingPath(); - const { start }: typeof import('@stencil/core/dev-server') = await config.sys.dynamicImport(devServerPath); - devServer = await start(config.devServer, config.logger, watcher); - } - - config.sys.onProcessInterrupt(() => { - config.logger.debug(`close watch`); - compiler && compiler.destroy(); - }); - - const rmVersionCheckerLog = watcher.on('buildFinish', async () => { - // log the version check one time - rmVersionCheckerLog(); - printCheckVersionResults(versionChecker); - }); - - if (devServer) { - const rmDevServerLog = watcher.on('buildFinish', () => { - // log the dev server url one time - rmDevServerLog(); - const url = devServer?.browserUrl ?? 'UNKNOWN URL'; - config.logger.info(`${config.logger.cyan(url)}\n`); - }); - } - - const closeResults = await watcher.start(); - if (closeResults.exitCode > 0) { - exitCode = closeResults.exitCode; - } - } catch (e) { - exitCode = 1; - config.logger.error(e); - } - - if (devServer) { - await devServer.close(); - } - - if (exitCode > 0) { - return config.sys.exit(exitCode); - } -}; diff --git a/src/cli/telemetry/helpers.ts b/src/cli/telemetry/helpers.ts deleted file mode 100644 index b74733c036f..00000000000 --- a/src/cli/telemetry/helpers.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type * as d from '../../declarations'; -import { ConfigFlags } from '../config-flags'; - -export const tryFn = async Promise, R>(fn: T, ...args: any[]): Promise => { - try { - return await fn(...args); - } catch { - // ignore - } - - return null; -}; - -export const isInteractive = (sys: d.CompilerSystem, flags: ConfigFlags, object?: d.TerminalInfo): boolean => { - const terminalInfo = - object || - Object.freeze({ - tty: sys.isTTY() ? true : false, - ci: - ['CI', 'BUILD_ID', 'BUILD_NUMBER', 'BITBUCKET_COMMIT', 'CODEBUILD_BUILD_ARN'].filter( - (v) => !!sys.getEnvironmentVar?.(v), - ).length > 0 || !!flags.ci, - }); - - return terminalInfo.tty && !terminalInfo.ci; -}; - -export const UUID_REGEX = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); - -// Plucked from https://github.com/ionic-team/capacitor/blob/b893a57aaaf3a16e13db9c33037a12f1a5ac92e0/cli/src/util/uuid.ts -export function uuidv4(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c == 'x' ? r : (r & 0x3) | 0x8; - - return v.toString(16); - }); -} - -/** - * Reads and parses a JSON file from the given `path` - * @param sys The system where the command is invoked - * @param path the path on the file system to read and parse - * @returns the parsed JSON - */ -export async function readJson(sys: d.CompilerSystem, path: string): Promise { - const file = await sys.readFile(path); - return !!file && JSON.parse(file); -} - -/** - * Does the command have the debug flag? - * @param flags The configuration flags passed into the Stencil command - * @returns true if --debug has been passed, otherwise false - */ -export function hasDebug(flags: ConfigFlags): boolean { - return !!flags.debug; -} - -/** - * Does the command have the verbose and debug flags? - * @param flags The configuration flags passed into the Stencil command - * @returns true if both --debug and --verbose have been passed, otherwise false - */ -export function hasVerbose(flags: ConfigFlags): boolean { - return !!flags.verbose && hasDebug(flags); -} diff --git a/src/cli/telemetry/shouldTrack.ts b/src/cli/telemetry/shouldTrack.ts deleted file mode 100644 index f4b0074d518..00000000000 --- a/src/cli/telemetry/shouldTrack.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as d from '../../declarations'; -import { isInteractive } from './helpers'; -import { checkTelemetry } from './telemetry'; - -/** - * Used to determine if tracking should occur. - * @param config The config passed into the Stencil command - * @param sys The system where the command is invoked - * @param ci whether or not the process is running in a Continuous Integration (CI) environment - * @returns true if telemetry should be sent, false otherwise - */ -export async function shouldTrack(config: d.ValidatedConfig, sys: d.CompilerSystem, ci?: boolean) { - return !ci && isInteractive(sys, config.flags) && (await checkTelemetry(sys)); -} diff --git a/src/cli/telemetry/telemetry.ts b/src/cli/telemetry/telemetry.ts deleted file mode 100644 index d50f9b082b1..00000000000 --- a/src/cli/telemetry/telemetry.ts +++ /dev/null @@ -1,493 +0,0 @@ -import { isOutputTargetHydrate, WWW } from '@utils'; - -import type * as d from '../../declarations'; -import { readConfig, updateConfig, writeConfig } from '../ionic-config'; -import { CoreCompiler } from '../load-compiler'; -import { hasDebug, hasVerbose, readJson, tryFn, uuidv4 } from './helpers'; -import { shouldTrack } from './shouldTrack'; - -/** - * Used to within taskBuild to provide the component_count property. - * - * @param sys The system where the command is invoked - * @param config The config passed into the Stencil command - * @param coreCompiler The compiler used to do builds - * @param result The results of a compiler build. - */ -export async function telemetryBuildFinishedAction( - sys: d.CompilerSystem, - config: d.ValidatedConfig, - coreCompiler: CoreCompiler, - result: d.CompilerBuildResults, -) { - const tracking = await shouldTrack(config, sys, !!config.flags.ci); - - if (!tracking) { - return; - } - - const component_count = result.componentGraph ? Object.keys(result.componentGraph).length : undefined; - - const data = await prepareData(coreCompiler, config, sys, result.duration, component_count); - - await sendMetric(sys, config, 'stencil_cli_command', data); - - config.logger.debug(`${config.logger.blue('Telemetry')}: ${config.logger.gray(JSON.stringify(data))}`); -} - -/** - * A function to wrap a compiler task function around. Will send telemetry if, and only if, the machine allows. - * - * @param sys The system where the command is invoked - * @param config The config passed into the Stencil command - * @param coreCompiler The compiler used to do builds - * @param action A Promise-based function to call in order to get the duration of any given command. - * @returns void - */ -export async function telemetryAction( - sys: d.CompilerSystem, - config: d.ValidatedConfig, - coreCompiler: CoreCompiler, - action?: d.TelemetryCallback, -) { - const tracking = await shouldTrack(config, sys, !!config.flags.ci); - - let duration = undefined; - let error: any; - - if (action) { - const start = new Date(); - - try { - await action(); - } catch (e) { - error = e; - } - - const end = new Date(); - duration = end.getTime() - start.getTime(); - } - - // We'll get componentCount details inside the taskBuild, so let's not send two messages. - if (!tracking || (config.flags.task == 'build' && !config.flags.args.includes('--watch'))) { - return; - } - - const data = await prepareData(coreCompiler, config, sys, duration); - - await sendMetric(sys, config, 'stencil_cli_command', data); - config.logger.debug(`${config.logger.blue('Telemetry')}: ${config.logger.gray(JSON.stringify(data))}`); - - if (error) { - throw error; - } -} - -/** - * Helper function to determine if a Stencil configuration builds an application. - * - * This function is a rough approximation whether an application is generated as a part of a Stencil build, based on - * contents of the project's `stencil.config.ts` file. - * - * @param config the configuration used by the Stencil project - * @returns true if we believe the project generates an application, false otherwise - */ -export function hasAppTarget(config: d.ValidatedConfig): boolean { - return config.outputTargets.some( - (target) => target.type === WWW && (!!target.serviceWorker || (!!target.baseUrl && target.baseUrl !== '/')), - ); -} - -export function isUsingYarn(sys: d.CompilerSystem) { - return sys.getEnvironmentVar?.('npm_execpath')?.includes('yarn') || false; -} - -/** - * Build a list of the different types of output targets used in a Stencil configuration. - * - * Duplicate entries will not be returned from the list - * - * @param config the configuration used by the Stencil project - * @returns a unique list of output target types found in the Stencil configuration - */ -export function getActiveTargets(config: d.ValidatedConfig): string[] { - const result = config.outputTargets.map((t) => t.type); - return Array.from(new Set(result)); -} - -/** - * Prepare data for telemetry - * - * @param coreCompiler the core compiler - * @param config the current Stencil config - * @param sys the compiler system instance in use - * @param duration_ms the duration of the action being tracked - * @param component_count the number of components being built (optional) - * @returns a Promise wrapping data for the telemetry endpoint - */ -export const prepareData = async ( - coreCompiler: CoreCompiler, - config: d.ValidatedConfig, - sys: d.CompilerSystem, - duration_ms: number | undefined, - component_count: number | undefined = undefined, -): Promise => { - const { typescript, rollup } = coreCompiler.versions || { typescript: 'unknown', rollup: 'unknown' }; - const { packages, packagesNoVersions } = await getInstalledPackages(sys, config); - const targets = getActiveTargets(config); - const yarn = isUsingYarn(sys); - const stencil = coreCompiler.version || 'unknown'; - const system = `${sys.name} ${sys.version}`; - const os_name = sys.details?.platform; - const os_version = sys.details?.release; - const cpu_model = sys.details?.cpuModel; - const build = coreCompiler.buildId || 'unknown'; - const has_app_pwa_config = hasAppTarget(config); - const anonymizedConfig = anonymizeConfigForTelemetry(config); - - return { - arguments: config.flags.args, - build, - component_count, - config: anonymizedConfig, - cpu_model, - duration_ms, - has_app_pwa_config, - os_name, - os_version, - packages, - packages_no_versions: packagesNoVersions, - rollup, - stencil, - system, - system_major: getMajorVersion(system), - targets, - task: config.flags.task, - typescript, - yarn, - }; -}; - -// Setting a key type to `never` excludes it from a mapped type, so we -// can get only keys which map to a string value by excluding all keys `K` -// where `d.Config[K]` does not extend `string`. -type ConfigStringKeys = keyof { - [K in keyof d.Config as Required[K] extends string ? K : never]: d.Config[K]; -}; - -// props in output targets for which we retain their original values when -// preparing a config for telemetry -// -// we omit the values of all other fields on output targets. -const OUTPUT_TARGET_KEYS_TO_KEEP: ReadonlyArray = ['type']; - -// top-level config props that we anonymize for telemetry -const CONFIG_PROPS_TO_ANONYMIZE: ReadonlyArray = [ - 'rootDir', - 'fsNamespace', - 'packageJsonFilePath', - 'namespace', - 'srcDir', - 'srcIndexHtml', - 'buildLogFilePath', - 'cacheDir', - 'configPath', - 'tsconfig', -]; - -// Props we delete entirely from the config for telemetry -// -// TODO(STENCIL-469): Investigate improving anonymization for tsCompilerOptions and devServer -const CONFIG_PROPS_TO_DELETE: ReadonlyArray = [ - 'commonjs', - 'devServer', - 'env', - 'logger', - 'rollupConfig', - 'sys', - 'testing', - 'tsCompilerOptions', -]; - -/** - * Anonymize the config for telemetry, replacing potentially revealing config props - * with a placeholder string if they are present (this lets us still track how frequently - * these config options are being used) - * - * @param config the config to anonymize - * @returns an anonymized copy of the same config - */ -export const anonymizeConfigForTelemetry = (config: d.ValidatedConfig): d.Config => { - const anonymizedConfig: d.Config = { ...config }; - - for (const prop of CONFIG_PROPS_TO_ANONYMIZE) { - if (anonymizedConfig[prop] !== undefined) { - anonymizedConfig[prop] = 'omitted'; - } - } - - anonymizedConfig.outputTargets = config.outputTargets.map((target) => { - // Anonymize the outputTargets on our configuration, taking advantage of the - // optional 2nd argument to `JSON.stringify`. If anything is not a string - // we retain it so that any nested properties are handled, else we check - // whether it's in our 'keep' list to decide whether to keep it or replace it - // with `"omitted"`. - const anonymizedOT = JSON.parse( - JSON.stringify(target, (key, value) => { - if (!(typeof value === 'string')) { - return value; - } - if (OUTPUT_TARGET_KEYS_TO_KEEP.includes(key)) { - return value; - } - return 'omitted'; - }), - ); - - // this prop has to be handled separately because it is an array - // so the replace function above will be called with all of its - // members, giving us `["omitted", "omitted", ...]`. - // - // Instead, we check for its presence and manually copy over. - if (isOutputTargetHydrate(target) && target.external) { - anonymizedOT['external'] = target.external.concat(); - } - return anonymizedOT; - }); - - // TODO(STENCIL-469): Investigate improving anonymization for tsCompilerOptions and devServer - for (const prop of CONFIG_PROPS_TO_DELETE) { - delete anonymizedConfig[prop]; - } - - return anonymizedConfig; -}; - -/** - * Reads package-lock.json, yarn.lock, and package.json files in order to cross-reference - * the dependencies and devDependencies properties. Pulls up the current installed version - * of each package under the @stencil, @ionic, and @capacitor scopes. - * - * @param sys the system instance where telemetry is invoked - * @param config the Stencil configuration associated with the current task that triggered telemetry - * @returns an object listing all dev and production dependencies under the aforementioned scopes - */ -async function getInstalledPackages( - sys: d.CompilerSystem, - config: d.ValidatedConfig, -): Promise<{ packages: string[]; packagesNoVersions: string[] }> { - let packages: string[] = []; - let packagesNoVersions: string[] = []; - const yarn = isUsingYarn(sys); - - try { - // Read package.json and package-lock.json - const appRootDir = sys.getCurrentDirectory(); - - const packageJson: d.PackageJsonData | null = await tryFn( - readJson, - sys, - sys.resolvePath(appRootDir + '/package.json'), - ); - - // They don't have a package.json for some reason? Eject button. - if (!packageJson) { - return { packages, packagesNoVersions }; - } - - const rawPackages: [string, string][] = Object.entries({ - ...packageJson.devDependencies, - ...packageJson.dependencies, - }); - - // Collect packages only in the stencil, ionic, or capacitor org's: - // https://www.npmjs.com/org/stencil - const ionicPackages = rawPackages.filter( - ([k]) => k.startsWith('@stencil/') || k.startsWith('@ionic/') || k.startsWith('@capacitor/'), - ); - - try { - packages = yarn ? await yarnPackages(sys, ionicPackages) : await npmPackages(sys, ionicPackages); - } catch (e) { - packages = ionicPackages.map(([k, v]) => `${k}@${v.replace('^', '')}`); - } - - packagesNoVersions = ionicPackages.map(([k]) => `${k}`); - - return { packages, packagesNoVersions }; - } catch (err) { - hasDebug(config.flags) && console.error(err); - return { packages, packagesNoVersions }; - } -} - -/** - * Visits the npm lock file to find the exact versions that are installed - * @param sys The system where the command is invoked - * @param ionicPackages a list of the found packages matching `@stencil`, `@capacitor`, or `@ionic` from the package.json file. - * @returns an array of strings of all the packages and their versions. - */ -async function npmPackages(sys: d.CompilerSystem, ionicPackages: [string, string][]): Promise { - const appRootDir = sys.getCurrentDirectory(); - const packageLockJson: any = await tryFn(readJson, sys, sys.resolvePath(appRootDir + '/package-lock.json')); - - return ionicPackages.map(([k, v]) => { - let version = packageLockJson?.dependencies[k]?.version ?? packageLockJson?.devDependencies[k]?.version ?? v; - version = version.includes('file:') ? sanitizeDeclaredVersion(v) : version; - return `${k}@${version}`; - }); -} - -/** - * Visits the yarn lock file to find the exact versions that are installed - * @param sys The system where the command is invoked - * @param ionicPackages a list of the found packages matching `@stencil`, `@capacitor`, or `@ionic` from the package.json file. - * @returns an array of strings of all the packages and their versions. - */ -async function yarnPackages(sys: d.CompilerSystem, ionicPackages: [string, string][]): Promise { - const appRootDir = sys.getCurrentDirectory(); - const yarnLock = sys.readFileSync(sys.resolvePath(appRootDir + '/yarn.lock')); - const yarnLockYml = sys.parseYarnLockFile?.(yarnLock); - - return ionicPackages.map(([k, v]) => { - const identifiedVersion = `${k}@${v}`; - let version = yarnLockYml?.object[identifiedVersion]?.version; - version = version && version.includes('undefined') ? sanitizeDeclaredVersion(identifiedVersion) : version; - return `${k}@${version}`; - }); -} - -/** - * This function is used for fallback purposes, where an npm or yarn lock file doesn't exist in the consumers directory. - * This will strip away '*', '^' and '~' from the declared package versions in a package.json. - * @param version the raw semver pattern identifier version string - * @returns a cleaned up representation without any qualifiers - */ -function sanitizeDeclaredVersion(version: string): string { - return version.replace(/[*^~]/g, ''); -} - -/** - * If telemetry is enabled, send a metric to an external data store - * - * @param sys the system instance where telemetry is invoked - * @param config the Stencil configuration associated with the current task that triggered telemetry - * @param name the name of a trackable metric. Note this name is not necessarily a scalar value to track, like - * "Stencil Version". For example, "stencil_cli_command" is a name that is used to track all CLI command information. - * @param value the data to send to the external data store under the provided name argument - */ -export async function sendMetric( - sys: d.CompilerSystem, - config: d.ValidatedConfig, - name: string, - value: d.TrackableData, -): Promise { - const session_id = await getTelemetryToken(sys); - - const message: d.Metric = { - name, - timestamp: new Date().toISOString(), - source: 'stencil_cli', - value, - session_id, - }; - - await sendTelemetry(sys, config, message); -} - -/** - * Used to read the config file's tokens.telemetry property. - * - * @param sys The system where the command is invoked - * @returns string - */ -async function getTelemetryToken(sys: d.CompilerSystem) { - const config = await readConfig(sys); - if (config['tokens.telemetry'] === undefined) { - config['tokens.telemetry'] = uuidv4(); - await writeConfig(sys, config); - } - return config['tokens.telemetry']; -} - -/** - * Issues a request to the telemetry server. - * @param sys The system where the command is invoked - * @param config The config passed into the Stencil command - * @param data Data to be tracked - */ -async function sendTelemetry(sys: d.CompilerSystem, config: d.ValidatedConfig, data: d.Metric): Promise { - try { - const now = new Date().toISOString(); - - const body = { - metrics: [data], - sent_at: now, - }; - - if (!sys.fetch) { - throw new Error('No fetch implementation available'); - } - - // This request is only made if telemetry is on. - const response = await sys.fetch('https://api.ionicjs.com/events/metrics', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - - hasVerbose(config.flags) && - console.debug('\nSent %O metric to events service (status: %O)', data.name, response.status, '\n'); - - if (response.status !== 204) { - hasVerbose(config.flags) && - console.debug('\nBad response from events service. Request body: %O', response.body.toString(), '\n'); - } - } catch (e) { - hasVerbose(config.flags) && console.debug('Telemetry request failed:', e); - } -} - -/** - * Checks if telemetry is enabled on this machine - * @param sys The system where the command is invoked - * @returns true if telemetry is enabled, false otherwise - */ -export async function checkTelemetry(sys: d.CompilerSystem): Promise { - const config = await readConfig(sys); - if (config['telemetry.stencil'] === undefined) { - config['telemetry.stencil'] = true; - await writeConfig(sys, config); - } - return config['telemetry.stencil']; -} - -/** - * Writes to the config file, enabling telemetry for this machine. - * @param sys The system where the command is invoked - * @returns true if writing the file was successful, false otherwise - */ -export async function enableTelemetry(sys: d.CompilerSystem): Promise { - return await updateConfig(sys, { 'telemetry.stencil': true }); -} - -/** - * Writes to the config file, disabling telemetry for this machine. - * @param sys The system where the command is invoked - * @returns true if writing the file was successful, false otherwise - */ -export async function disableTelemetry(sys: d.CompilerSystem): Promise { - return await updateConfig(sys, { 'telemetry.stencil': false }); -} - -/** - * Takes in a semver string in order to return the major version. - * @param version The fully qualified semver version - * @returns a string of the major version - */ -function getMajorVersion(version: string): string { - const parts = version.split('.'); - return parts[0]; -} diff --git a/src/cli/telemetry/test/helpers.spec.ts b/src/cli/telemetry/test/helpers.spec.ts deleted file mode 100644 index bdcc0cfad9c..00000000000 --- a/src/cli/telemetry/test/helpers.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { createSystem } from '../../../compiler/sys/stencil-sys'; -import { ConfigFlags, createConfigFlags } from '../../config-flags'; -import { hasDebug, hasVerbose, isInteractive, tryFn, uuidv4 } from '../helpers'; - -describe('hasDebug', () => { - it('returns true when the "debug" flag is true', () => { - const flags = createConfigFlags({ - debug: true, - }); - - expect(hasDebug(flags)).toBe(true); - }); - - it('returns false when the "debug" flag is false', () => { - const flags = createConfigFlags({ - debug: false, - }); - - expect(hasDebug(flags)).toBe(false); - }); - - it('returns false when a flag is not passed', () => { - const flags = createConfigFlags({}); - - expect(hasDebug(flags)).toBe(false); - }); -}); - -describe('hasVerbose', () => { - it.each>([ - { debug: true, verbose: false }, - { debug: false, verbose: true }, - { debug: false, verbose: false }, - ])('returns false when debug=$debug and verbose=$verbose', (flagOverrides) => { - const flags = createConfigFlags(flagOverrides); - - expect(hasVerbose(flags)).toBe(false); - }); - - it('returns true when debug=true and verbose=true', () => { - const flags = createConfigFlags({ - debug: true, - verbose: true, - }); - - expect(hasVerbose(flags)).toBe(true); - }); -}); - -describe('uuidv4', () => { - it('outputs a UUID', () => { - const pattern = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); - const uuid = uuidv4(); - expect(!!uuid.match(pattern)).toBe(true); - }); -}); - -describe('isInteractive', () => { - const sys = createSystem(); - - it('returns false by default', () => { - const result = isInteractive(sys, createConfigFlags({ ci: false }), { ci: false, tty: false }); - expect(result).toBe(false); - }); - - it('returns false when tty is false', () => { - const result = isInteractive(sys, createConfigFlags({ ci: true }), { ci: true, tty: false }); - expect(result).toBe(false); - }); - - it('returns false when ci is true', () => { - const result = isInteractive(sys, createConfigFlags({ ci: true }), { ci: true, tty: true }); - expect(result).toBe(false); - }); - - it('returns true when tty is true and ci is false', () => { - const result = isInteractive(sys, createConfigFlags({ ci: false }), { ci: false, tty: true }); - expect(result).toBe(true); - }); -}); - -describe('tryFn', () => { - it('handles failures correctly', async () => { - const result = await tryFn(async () => { - throw new Error('Uh oh!'); - }); - - expect(result).toBe(null); - }); - - it('handles success correctly', async () => { - const result = await tryFn(async () => { - return true; - }); - - expect(result).toBe(true); - }); - - it('handles returning false correctly', async () => { - const result = await tryFn(async () => { - return false; - }); - - expect(result).toBe(false); - }); -}); diff --git a/src/cli/test/parse-flags.spec.ts b/src/cli/test/parse-flags.spec.ts deleted file mode 100644 index b6228514cd2..00000000000 --- a/src/cli/test/parse-flags.spec.ts +++ /dev/null @@ -1,471 +0,0 @@ -import { toDashCase } from '@utils'; - -import { LogLevel } from '../../declarations'; -import { - BOOLEAN_CLI_FLAGS, - BOOLEAN_STRING_CLI_FLAGS, - BooleanStringCLIFlag, - ConfigFlags, - NUMBER_CLI_FLAGS, - STRING_ARRAY_CLI_FLAGS, - STRING_CLI_FLAGS, - StringArrayCLIFlag, -} from '../config-flags'; -import { Empty, parseEqualsArg, parseFlags } from '../parse-flags'; - -describe('parseFlags', () => { - it('should get known and unknown args', () => { - const args = ['serve', '--address', '127.0.0.1', '--potatoArgument', '--flimflammery', 'test.spec.ts']; - - const flags = parseFlags(args); - expect(flags.task).toBe('serve'); - expect(flags.args[0]).toBe('--address'); - expect(flags.args[1]).toBe('127.0.0.1'); - expect(flags.args[2]).toBe('--potatoArgument'); - expect(flags.args[3]).toBe('--flimflammery'); - expect(flags.args[4]).toBe('test.spec.ts'); - expect(flags.knownArgs).toEqual(['--address', '127.0.0.1']); - expect(flags.unknownArgs[0]).toBe('--potatoArgument'); - expect(flags.unknownArgs[1]).toBe('--flimflammery'); - expect(flags.unknownArgs[2]).toBe('test.spec.ts'); - }); - - it('should parse cli for dev server', () => { - // user command line args - // $ npm run serve --port 4444 - - // args.slice(2) - // [ 'serve', '--address', '127.0.0.1', '--port', '4444' ] - - const args = ['serve', '--address', '127.0.0.1', '--port', '4444']; - - const flags = parseFlags(args); - expect(flags.task).toBe('serve'); - expect(flags.address).toBe('127.0.0.1'); - expect(flags.port).toBe(4444); - expect(flags.knownArgs).toEqual(['--address', '127.0.0.1', '--port', '4444']); - }); - - it('should parse task', () => { - const flags = parseFlags(['build']); - expect(flags.task).toBe('build'); - }); - - it('should parse no task', () => { - const flags = parseFlags(['--flag']); - expect(flags.task).toBe(null); - }); - - /** - * these comprehensive tests of all the supported boolean args serve as - * regression tests against duplicating any of the arguments in the arrays. - * Because of the way that the arg parsing algorithm works having a dupe - * will result in a value like `[true, true]` being set on ConfigFlags, which - * will cause these tests to start failing. - */ - describe.each(BOOLEAN_CLI_FLAGS)('should parse boolean flag %s', (cliArg) => { - it('should parse arg', () => { - const flags = parseFlags([`--${cliArg}`]); - expect(flags.knownArgs).toEqual([`--${cliArg}`]); - expect(flags[cliArg]).toBe(true); - }); - - it(`should parse --no${cliArg}`, () => { - const negativeFlag = '--no' + cliArg.charAt(0).toUpperCase() + cliArg.slice(1); - const flags = parseFlags([negativeFlag]); - expect(flags.knownArgs).toEqual([negativeFlag]); - expect(flags[cliArg]).toBe(false); - }); - - it(`should override --${cliArg} with --no${cliArg}`, () => { - const negativeFlag = '--no' + cliArg.charAt(0).toUpperCase() + cliArg.slice(1); - const flags = parseFlags([`--${cliArg}`, negativeFlag]); - expect(flags.knownArgs).toEqual([`--${cliArg}`, negativeFlag]); - expect(flags[cliArg]).toBe(false); - }); - - it('should not set value if not present', () => { - const flags = parseFlags([]); - expect(flags.knownArgs).toEqual([]); - expect(flags[cliArg]).toBe(undefined); - }); - - it.each([true, false])(`should set the value with --${cliArg}=%p`, (value) => { - const flags = parseFlags([`--${cliArg}=${value}`]); - expect(flags.knownArgs).toEqual([`--${cliArg}`, String(value)]); - expect(flags[cliArg]).toBe(value); - }); - }); - - describe.each(STRING_CLI_FLAGS)('should parse string flag %s', (cliArg) => { - it(`should parse "--${cliArg} value"`, () => { - const flags = parseFlags([`--${cliArg}`, 'test-value']); - expect(flags.knownArgs).toEqual([`--${cliArg}`, 'test-value']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toBe('test-value'); - }); - - it(`should parse "--${cliArg}=value"`, () => { - const flags = parseFlags([`--${cliArg}=path/to/file.js`]); - expect(flags.knownArgs).toEqual([`--${cliArg}`, 'path/to/file.js']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toBe('path/to/file.js'); - }); - - it(`should parse "--${toDashCase(cliArg)} value"`, () => { - const flags = parseFlags([`--${toDashCase(cliArg)}`, 'path/to/file.js']); - expect(flags.knownArgs).toEqual([`--${toDashCase(cliArg)}`, 'path/to/file.js']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toBe('path/to/file.js'); - }); - - it(`should parse "--${toDashCase(cliArg)}=value"`, () => { - const flags = parseFlags([`--${toDashCase(cliArg)}=path/to/file.js`]); - expect(flags.knownArgs).toEqual([`--${toDashCase(cliArg)}`, 'path/to/file.js']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toBe('path/to/file.js'); - }); - }); - - it.each(NUMBER_CLI_FLAGS)('should parse number flag %s', (cliArg) => { - const flags = parseFlags([`--${cliArg}`, '42']); - expect(flags.knownArgs).toEqual([`--${cliArg}`, '42']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toBe(42); - }); - - it('should override --config with second --config', () => { - const args = ['--config', '/config-1.js', '--config', '/config-2.js']; - const flags = parseFlags(args); - expect(flags.config).toBe('/config-2.js'); - }); - - describe.each(BOOLEAN_STRING_CLI_FLAGS)('boolean-string flag - %s', (cliArg: BooleanStringCLIFlag) => { - it('parses a boolean-string flag as a boolean with no arg', () => { - const args = [`--${cliArg}`]; - const flags = parseFlags(args); - expect(flags[cliArg]).toBe(true); - expect(flags.knownArgs).toEqual([`--${cliArg}`]); - }); - - it(`parses a boolean-string flag as a falsy boolean with "no" arg - --no-${cliArg}`, () => { - const args = [`--no-${cliArg}`]; - const flags = parseFlags(args); - expect(flags[cliArg]).toBe(false); - expect(flags.knownArgs).toEqual([`--no-${cliArg}`]); - }); - - it(`parses a boolean-string flag as a falsy boolean with "no" arg - --no${ - cliArg.charAt(0).toUpperCase() + cliArg.slice(1) - }`, () => { - const negativeFlag = '--no' + cliArg.charAt(0).toUpperCase() + cliArg.slice(1); - const flags = parseFlags([negativeFlag]); - expect(flags[cliArg]).toBe(false); - expect(flags.knownArgs).toEqual([negativeFlag]); - }); - - it('parses a boolean-string flag as a string with a string arg', () => { - const args = [`--${cliArg}`, 'shell']; - const flags = parseFlags(args); - expect(flags[cliArg]).toBe('shell'); - expect(flags.knownArgs).toEqual([`--${cliArg}`, 'shell']); - }); - - it('parses a boolean-string flag as a string with a string arg using equality', () => { - const args = [`--${cliArg}=shell`]; - const flags = parseFlags(args); - expect(flags[cliArg]).toBe('shell'); - expect(flags.knownArgs).toEqual([`--${cliArg}`, 'shell']); - }); - }); - - describe.each(['info', 'warn', 'error', 'debug'])('logLevel %s', (level) => { - it("should parse '--logLevel %s'", () => { - const args = ['--logLevel', level]; - const flags = parseFlags(args); - expect(flags.logLevel).toBe(level); - }); - - it('should parse --logLevel=%s', () => { - const args = [`--logLevel=${level}`]; - const flags = parseFlags(args); - expect(flags.logLevel).toBe(level); - }); - - it("should parse '--log-level %s'", () => { - const flags = parseFlags(['--log-level', level]); - expect(flags.logLevel).toBe(level); - }); - - it('should parse --log-level=%s', () => { - const flags = parseFlags([`--log-level=${level}`]); - expect(flags.logLevel).toBe(level); - }); - }); - - /** - * maxWorkers is (as of this writing) our only StringNumberCLIArg, meaning it - * may be a string (like "50%") or a number (like 4). For this reason we have - * some tests just for it. - */ - describe('maxWorkers', () => { - it.each([ - ['--maxWorkers', '4'], - ['--maxWorkers=4'], - ['--max-workers', '4'], - ['--maxWorkers', '4e+0'], - ['--maxWorkers', '40e-1'], - ])('should parse %p, %p', (...args) => { - const flags = parseFlags(args); - expect(flags.maxWorkers).toBe(4); - }); - - it('should parse --maxWorkers 4', () => { - const flags = parseFlags(['--maxWorkers', '4']); - expect(flags.maxWorkers).toBe(4); - }); - - it('should parse --maxWorkers=4', () => { - const flags = parseFlags(['--maxWorkers=4']); - expect(flags.maxWorkers).toBe(4); - }); - - it('should parse --max-workers 4', () => { - const flags = parseFlags(['--max-workers', '4']); - expect(flags.maxWorkers).toBe(4); - }); - - it('should parse --maxWorkers=50%', function () { - // see https://jestjs.io/docs/27.x/cli#--maxworkersnumstring - const flags = parseFlags(['--maxWorkers=50%']); - expect(flags.maxWorkers).toBe('50%'); - }); - - it('should parse --max-workers=1', () => { - const flags = parseFlags(['--max-workers=1']); - expect(flags.maxWorkers).toBe(1); - }); - - it('should not parse --max-workers', () => { - const flags = parseFlags([]); - expect(flags.maxWorkers).toBe(undefined); - }); - }); - - describe('aliases', () => { - describe('-p (alias for port)', () => { - it('should parse -p=4444', () => { - const flags = parseFlags(['-p=4444']); - expect(flags.port).toBe(4444); - }); - it('should parse -p 4444', () => { - const flags = parseFlags(['-p', '4444']); - expect(flags.port).toBe(4444); - }); - }); - - it('should parse -h (alias for help)', () => { - const flags = parseFlags(['-h']); - expect(flags.help).toBe(true); - }); - - it('should parse -v (alias for version)', () => { - const flags = parseFlags(['-v']); - expect(flags.version).toBe(true); - }); - - describe('-c alias for config', () => { - it('should parse -c /my-config.js', () => { - const flags = parseFlags(['-c', '/my-config.js']); - expect(flags.config).toBe('/my-config.js'); - expect(flags.knownArgs).toEqual(['--config', '/my-config.js']); - }); - - it('should parse -c=/my-config.js', () => { - const flags = parseFlags(['-c=/my-config.js']); - expect(flags.config).toBe('/my-config.js'); - expect(flags.knownArgs).toEqual(['--config', '/my-config.js']); - }); - }); - - describe('Jest aliases', () => { - it.each([ - ['w', 'maxWorkers', '4'], - ['t', 'testNamePattern', 'testname'], - ])('should support the string Jest alias %p for %p', (alias, fullArgument, value) => { - const flags = parseFlags([`-${alias}`, value]); - expect(flags.knownArgs).toEqual([`--${fullArgument}`, value]); - expect(flags.unknownArgs).toHaveLength(0); - }); - - it.each([ - ['w', 'maxWorkers', '4'], - ['t', 'testNamePattern', 'testname'], - ])('should support the string Jest alias %p for %p in an AliasEqualsArg', (alias, fullArgument, value) => { - const flags = parseFlags([`-${alias}=${value}`]); - expect(flags.knownArgs).toEqual([`--${fullArgument}`, value]); - expect(flags.unknownArgs).toHaveLength(0); - }); - - it.each<[string, keyof ConfigFlags]>([ - ['b', 'bail'], - ['e', 'expand'], - ['o', 'onlyChanged'], - ['f', 'onlyFailures'], - ['i', 'runInBand'], - ['u', 'updateSnapshot'], - ])('should support the boolean Jest alias %p for %p', (alias, fullArgument) => { - const flags = parseFlags([`-${alias}`]); - expect(flags.knownArgs).toEqual([`--${fullArgument}`]); - expect(flags[fullArgument]).toBe(true); - expect(flags.unknownArgs).toHaveLength(0); - }); - }); - }); - - it('should parse many', () => { - const args = ['-v', '--help', '-c=./myconfig.json']; - const flags = parseFlags(args); - expect(flags.version).toBe(true); - expect(flags.help).toBe(true); - expect(flags.config).toBe('./myconfig.json'); - }); - - describe('parseEqualsArg', () => { - it.each([ - ['--fooBar=baz', '--fooBar', 'baz'], - ['--foo-bar=4', '--foo-bar', '4'], - ['--fooBar=twenty=3*4', '--fooBar', 'twenty=3*4'], - ['--fooBar', '--fooBar', Empty], - ['--foo-bar', '--foo-bar', Empty], - ['--foo-bar=""', '--foo-bar', '""'], - ])('should parse %s correctly', (testArg, expectedArg, expectedValue) => { - const [arg, value] = parseEqualsArg(testArg); - expect(arg).toBe(expectedArg); - expect(value).toEqual(expectedValue); - }); - }); - - describe.each(STRING_ARRAY_CLI_FLAGS)('should parse string flag %s', (cliArg: StringArrayCLIFlag) => { - it(`should parse single value: "--${cliArg} test-value"`, () => { - const flags = parseFlags([`--${cliArg}`, 'test-value']); - expect(flags.knownArgs).toEqual([`--${cliArg}`, 'test-value']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toEqual(['test-value']); - }); - - it(`should parse multiple values: "--${cliArg} test-value"`, () => { - const flags = parseFlags([`--${cliArg}`, 'test-value', `--${cliArg}`, 'second-test-value']); - expect(flags.knownArgs).toEqual([`--${cliArg}`, 'test-value', `--${cliArg}`, 'second-test-value']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toEqual(['test-value', 'second-test-value']); - }); - - it(`should parse "--${cliArg}=value"`, () => { - const flags = parseFlags([`--${cliArg}=path/to/file.js`]); - expect(flags.knownArgs).toEqual([`--${cliArg}`, 'path/to/file.js']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toEqual(['path/to/file.js']); - }); - - it(`should parse multiple values: "--${cliArg}=test-value"`, () => { - const flags = parseFlags([`--${cliArg}=test-value`, `--${cliArg}=second-test-value`]); - expect(flags.knownArgs).toEqual([`--${cliArg}`, 'test-value', `--${cliArg}`, 'second-test-value']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toEqual(['test-value', 'second-test-value']); - }); - - it(`should parse "--${toDashCase(cliArg)} value"`, () => { - const flags = parseFlags([`--${toDashCase(cliArg)}`, 'path/to/file.js']); - expect(flags.knownArgs).toEqual([`--${toDashCase(cliArg)}`, 'path/to/file.js']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toEqual(['path/to/file.js']); - }); - - it(`should parse multiple values: "--${toDashCase(cliArg)} test-value"`, () => { - const flags = parseFlags([ - `--${toDashCase(cliArg)}`, - 'test-value', - `--${toDashCase(cliArg)}`, - 'second-test-value', - ]); - expect(flags.knownArgs).toEqual([ - `--${toDashCase(cliArg)}`, - 'test-value', - `--${toDashCase(cliArg)}`, - 'second-test-value', - ]); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toEqual(['test-value', 'second-test-value']); - }); - - it(`should parse "--${toDashCase(cliArg)}=value"`, () => { - const flags = parseFlags([`--${toDashCase(cliArg)}=path/to/file.js`]); - expect(flags.knownArgs).toEqual([`--${toDashCase(cliArg)}`, 'path/to/file.js']); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toEqual(['path/to/file.js']); - }); - - it(`should parse multiple values: "--${toDashCase(cliArg)}=test-value"`, () => { - const flags = parseFlags([`--${toDashCase(cliArg)}=test-value`, `--${toDashCase(cliArg)}=second-test-value`]); - expect(flags.knownArgs).toEqual([ - `--${toDashCase(cliArg)}`, - 'test-value', - `--${toDashCase(cliArg)}`, - 'second-test-value', - ]); - expect(flags.unknownArgs).toEqual([]); - expect(flags[cliArg]).toEqual(['test-value', 'second-test-value']); - }); - }); - - describe('error reporting', () => { - it('should throw if you pass no argument to a string flag', () => { - expect(() => { - parseFlags(['--cacheDirectory', '--someOtherFlag']); - }).toThrow('when parsing CLI flag "--cacheDirectory": expected a string argument but received nothing'); - }); - - it('should throw if you pass no argument to a string array flag', () => { - expect(() => { - parseFlags(['--reporters', '--someOtherFlag']); - }).toThrow('when parsing CLI flag "--reporters": expected a string argument but received nothing'); - }); - - it('should throw if you pass no argument to a number flag', () => { - expect(() => { - parseFlags(['--port', '--someOtherFlag']); - }).toThrow('when parsing CLI flag "--port": expected a number argument but received nothing'); - }); - - it('should throw if you pass a non-number argument to a number flag', () => { - expect(() => { - parseFlags(['--port', 'stringy']); - }).toThrow('when parsing CLI flag "--port": expected a number but received "stringy"'); - }); - - it('should throw if you pass a bad number argument to a number flag', () => { - expect(() => { - parseFlags(['--port=NaN']); - }).toThrow('when parsing CLI flag "--port": expected a number but received "NaN"'); - }); - - it('should throw if you pass no argument to a string/number flag', () => { - expect(() => { - parseFlags(['--maxWorkers']); - }).toThrow('when parsing CLI flag "--maxWorkers": expected a string or a number but received nothing'); - }); - - it('should throw if you pass an invalid log level for --logLevel', () => { - expect(() => { - parseFlags(['--logLevel', 'potato']); - }).toThrow('when parsing CLI flag "--logLevel": expected to receive a valid log level but received "potato"'); - }); - - it('should throw if you pass no argument to --logLevel', () => { - expect(() => { - parseFlags(['--logLevel']); - }).toThrow('when parsing CLI flag "--logLevel": expected to receive a valid log level but received nothing'); - }); - }); -}); diff --git a/src/cli/test/run.spec.ts b/src/cli/test/run.spec.ts deleted file mode 100644 index 1b48e1993d0..00000000000 --- a/src/cli/test/run.spec.ts +++ /dev/null @@ -1,309 +0,0 @@ -import * as coreCompiler from '@stencil/core/compiler'; -import { mockCompilerSystem, mockConfig, mockLogger as createMockLogger } from '@stencil/core/testing'; - -import type * as d from '../../declarations'; -import { createTestingSystem } from '../../testing/testing-sys'; -import { createConfigFlags } from '../config-flags'; -import * as ParseFlags from '../parse-flags'; -import { run, runTask } from '../run'; -import * as BuildTask from '../task-build'; -import * as DocsTask from '../task-docs'; -import * as GenerateTask from '../task-generate'; -import * as HelpTask from '../task-help'; -import * as PrerenderTask from '../task-prerender'; -import * as ServeTask from '../task-serve'; -import * as TelemetryTask from '../task-telemetry'; -import * as TestTask from '../task-test'; - -describe('run', () => { - describe('run()', () => { - let cliInitOptions: d.CliInitOptions; - let mockLogger: d.Logger; - let mockSystem: d.CompilerSystem; - - let parseFlagsSpy: jest.SpyInstance< - ReturnType, - Parameters - >; - - beforeEach(() => { - mockLogger = createMockLogger(); - mockSystem = createTestingSystem(); - - cliInitOptions = { - args: [], - logger: mockLogger, - sys: mockSystem, - }; - - parseFlagsSpy = jest.spyOn(ParseFlags, 'parseFlags'); - parseFlagsSpy.mockReturnValue( - createConfigFlags({ - // use the 'help' task as a reasonable default for all calls to this function. - // code paths that require a different task can always override this value as needed. - task: 'help', - }), - ); - }); - - afterEach(() => { - parseFlagsSpy.mockRestore(); - }); - - describe('help task', () => { - let taskHelpSpy: jest.SpyInstance, Parameters>; - - beforeEach(() => { - taskHelpSpy = jest.spyOn(HelpTask, 'taskHelp'); - taskHelpSpy.mockReturnValue(Promise.resolve()); - }); - - afterEach(() => { - taskHelpSpy.mockRestore(); - }); - - it("calls the help task when the 'task' field is set to 'help'", async () => { - await run(cliInitOptions); - - expect(taskHelpSpy).toHaveBeenCalledTimes(1); - expect(taskHelpSpy).toHaveBeenCalledWith( - { - task: 'help', - args: [], - knownArgs: [], - unknownArgs: [], - }, - mockLogger, - mockSystem, - ); - - taskHelpSpy.mockRestore(); - }); - - it("calls the help task when the 'task' field is set to null", async () => { - parseFlagsSpy.mockReturnValue( - createConfigFlags({ - task: null, - }), - ); - - await run(cliInitOptions); - - expect(taskHelpSpy).toHaveBeenCalledTimes(1); - expect(taskHelpSpy).toHaveBeenCalledWith( - { - task: 'help', - args: [], - knownArgs: [], - unknownArgs: [], - }, - mockLogger, - mockSystem, - ); - - taskHelpSpy.mockRestore(); - }); - - it("calls the help task when the 'help' field is set on flags", async () => { - parseFlagsSpy.mockReturnValue( - createConfigFlags({ - help: true, - }), - ); - - await run(cliInitOptions); - - expect(taskHelpSpy).toHaveBeenCalledTimes(1); - expect(taskHelpSpy).toHaveBeenCalledWith( - { - task: 'help', - args: [], - unknownArgs: [], - knownArgs: [], - }, - mockLogger, - mockSystem, - ); - - taskHelpSpy.mockRestore(); - }); - }); - }); - - describe('runTask()', () => { - let sys: d.CompilerSystem; - let unvalidatedConfig: d.UnvalidatedConfig; - - let taskBuildSpy: jest.SpyInstance, Parameters>; - let taskDocsSpy: jest.SpyInstance, Parameters>; - let taskGenerateSpy: jest.SpyInstance< - ReturnType, - Parameters - >; - let taskHelpSpy: jest.SpyInstance, Parameters>; - let taskPrerenderSpy: jest.SpyInstance< - ReturnType, - Parameters - >; - let taskServeSpy: jest.SpyInstance, Parameters>; - let taskTelemetrySpy: jest.SpyInstance< - ReturnType, - Parameters - >; - let taskTestSpy: jest.SpyInstance, Parameters>; - - beforeEach(() => { - sys = mockCompilerSystem(); - sys.exit = jest.fn(); - - unvalidatedConfig = mockConfig({ outputTargets: [], sys, fsNamespace: 'testing' }); - - taskBuildSpy = jest.spyOn(BuildTask, 'taskBuild'); - taskBuildSpy.mockResolvedValue(); - - taskDocsSpy = jest.spyOn(DocsTask, 'taskDocs'); - taskDocsSpy.mockResolvedValue(); - - taskGenerateSpy = jest.spyOn(GenerateTask, 'taskGenerate'); - taskGenerateSpy.mockResolvedValue(); - - taskHelpSpy = jest.spyOn(HelpTask, 'taskHelp'); - taskHelpSpy.mockResolvedValue(); - - taskPrerenderSpy = jest.spyOn(PrerenderTask, 'taskPrerender'); - taskPrerenderSpy.mockResolvedValue(); - - taskServeSpy = jest.spyOn(ServeTask, 'taskServe'); - taskServeSpy.mockResolvedValue(); - - taskTelemetrySpy = jest.spyOn(TelemetryTask, 'taskTelemetry'); - taskTelemetrySpy.mockResolvedValue(); - - taskTestSpy = jest.spyOn(TestTask, 'taskTest'); - taskTestSpy.mockResolvedValue(); - }); - - afterEach(() => { - taskBuildSpy.mockRestore(); - taskDocsSpy.mockRestore(); - taskGenerateSpy.mockRestore(); - taskHelpSpy.mockRestore(); - taskPrerenderSpy.mockRestore(); - taskServeSpy.mockRestore(); - taskTelemetrySpy.mockRestore(); - taskTestSpy.mockRestore(); - }); - - describe('default configuration', () => { - describe('sys property', () => { - it('uses the sys argument if one is provided', async () => { - // remove the `CompilerSystem` on the config, just to be sure we don't accidentally use it - unvalidatedConfig.sys = undefined; - - await runTask(coreCompiler, unvalidatedConfig, 'build', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, { sys }); - - // first validate there was one call, and that call had two arguments - expect(taskBuildSpy).toHaveBeenCalledTimes(1); - expect(taskBuildSpy).toHaveBeenCalledWith(coreCompiler, validated.config); - - const compilerSystemUsed: d.CompilerSystem = taskBuildSpy.mock.calls[0][1].sys; - expect(compilerSystemUsed).toBe(sys); - }); - }); - }); - - it('calls the build task', async () => { - await runTask(coreCompiler, unvalidatedConfig, 'build', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); - - expect(taskBuildSpy).toHaveBeenCalledTimes(1); - expect(taskBuildSpy).toHaveBeenCalledWith(coreCompiler, validated.config); - }); - - it('calls the docs task', async () => { - await runTask(coreCompiler, unvalidatedConfig, 'docs', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); - - expect(taskDocsSpy).toHaveBeenCalledTimes(1); - expect(taskDocsSpy).toHaveBeenCalledWith(coreCompiler, validated.config); - }); - - describe('generate task', () => { - it("calls the generate task for the argument 'generate'", async () => { - await runTask(coreCompiler, unvalidatedConfig, 'generate', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); - - expect(taskGenerateSpy).toHaveBeenCalledTimes(1); - expect(taskGenerateSpy).toHaveBeenCalledWith(validated.config); - }); - - it("calls the generate task for the argument 'g'", async () => { - await runTask(coreCompiler, unvalidatedConfig, 'g', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); - - expect(taskGenerateSpy).toHaveBeenCalledTimes(1); - expect(taskGenerateSpy).toHaveBeenCalledWith(validated.config); - }); - }); - - it('calls the help task', async () => { - await runTask(coreCompiler, unvalidatedConfig, 'help', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); - - expect(taskHelpSpy).toHaveBeenCalledTimes(1); - expect(taskHelpSpy).toHaveBeenCalledWith(validated.config.flags, validated.config.logger, sys); - }); - - it('calls the prerender task', async () => { - await runTask(coreCompiler, unvalidatedConfig, 'prerender', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); - - expect(taskPrerenderSpy).toHaveBeenCalledTimes(1); - expect(taskPrerenderSpy).toHaveBeenCalledWith(coreCompiler, validated.config); - }); - - it('calls the serve task', async () => { - await runTask(coreCompiler, unvalidatedConfig, 'serve', sys); - - expect(taskServeSpy).toHaveBeenCalledTimes(1); - expect(taskServeSpy).toHaveBeenCalledWith(coreCompiler.validateConfig(unvalidatedConfig, {}).config); - }); - - describe('telemetry task', () => { - it('calls the telemetry task when a compiler system is present', async () => { - await runTask(coreCompiler, unvalidatedConfig, 'telemetry', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); - - expect(taskTelemetrySpy).toHaveBeenCalledTimes(1); - expect(taskTelemetrySpy).toHaveBeenCalledWith(validated.config.flags, sys, validated.config.logger); - }); - }); - - it('calls the test task', async () => { - await runTask(coreCompiler, unvalidatedConfig, 'test', sys); - - expect(taskTestSpy).toHaveBeenCalledTimes(1); - expect(taskTestSpy).toHaveBeenCalledWith(coreCompiler.validateConfig(unvalidatedConfig, {}).config); - }); - - it('defaults to the help task for an unaccounted for task name', async () => { - // info is a valid task name, but isn't used in the `switch` statement of `runTask` - await runTask(coreCompiler, unvalidatedConfig, 'info', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); - - expect(taskHelpSpy).toHaveBeenCalledTimes(1); - expect(taskHelpSpy).toHaveBeenCalledWith(validated.config.flags, validated.config.logger, sys); - }); - - it('defaults to the provided task if no flags exist on the provided config', async () => { - unvalidatedConfig = mockConfig({ flags: undefined, sys }); - - await runTask(coreCompiler, unvalidatedConfig, 'help', sys); - const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); - - expect(taskHelpSpy).toHaveBeenCalledTimes(1); - expect(taskHelpSpy).toHaveBeenCalledWith(createConfigFlags({ task: 'help' }), validated.config.logger, sys); - }); - }); -}); diff --git a/src/cli/test/task-generate.spec.ts b/src/cli/test/task-generate.spec.ts deleted file mode 100644 index e5ba522de2e..00000000000 --- a/src/cli/test/task-generate.spec.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { mockCompilerSystem, mockValidatedConfig } from '@stencil/core/testing'; - -import type * as d from '../../declarations'; -import * as utils from '../../utils/validation'; -import { createConfigFlags } from '../config-flags'; -import { BoilerplateFile, getBoilerplateByExtension, taskGenerate } from '../task-generate'; - -const promptMock = jest.fn().mockResolvedValue('my-component'); - -jest.mock('prompts', () => ({ - prompt: promptMock, -})); - -let formatToPick = 'css'; - -const setup = async (plugins: any[] = []) => { - const sys = mockCompilerSystem(); - const config: d.ValidatedConfig = mockValidatedConfig({ - configPath: '/testing-path', - flags: createConfigFlags({ task: 'generate' }), - srcDir: '/src', - sys, - plugins, - }); - - // set up some mocks / spies - config.sys.exit = jest.fn(); - const errorSpy = jest.spyOn(config.logger, 'error'); - const validateTagSpy = jest.spyOn(utils, 'validateComponentTag').mockReturnValue(undefined); - - // mock prompt usage: tagName and filesToGenerate are the keys used for - // different calls, so we can cheat here and just do a single - // mockResolvedValue - let format = formatToPick; - promptMock.mockImplementation((params) => { - if (params.name === 'sassFormat') { - format = 'sass'; - return { sassFormat: 'sass' }; - } - return { - tagName: 'my-component', - filesToGenerate: [format, 'spec.tsx', 'e2e.ts'], - }; - }); - - return { config, errorSpy, validateTagSpy }; -}; - -/** - * Little test helper function which just temporarily silences - * console.log calls, so we can avoid spewing a bunch of stuff. - * @param config the user-supplied config to forward to `taskGenerate` - */ -async function silentGenerate(config: d.ValidatedConfig): Promise { - const tmp = console.log; - console.log = jest.fn(); - await taskGenerate(config); - console.log = tmp; -} - -describe('generate task', () => { - afterEach(() => { - jest.restoreAllMocks(); - jest.clearAllMocks(); - jest.resetModules(); - formatToPick = 'css'; - }); - - afterAll(() => { - jest.resetAllMocks(); - }); - - it('should exit with an error if no `configPath` is supplied', async () => { - const { config, errorSpy } = await setup(); - config.configPath = undefined; - await taskGenerate(config); - expect(config.sys.exit).toHaveBeenCalledWith(1); - expect(errorSpy).toHaveBeenCalledWith( - 'Please run this command in your root directory (i. e. the one containing stencil.config.ts).', - ); - }); - - it('should exit with an error if no `srcDir` is supplied', async () => { - const { config, errorSpy } = await setup(); - config.srcDir = undefined; - await taskGenerate(config); - expect(config.sys.exit).toHaveBeenCalledWith(1); - expect(errorSpy).toHaveBeenCalledWith("Stencil's srcDir was not specified."); - }); - - it('should exit with an error if the component name does not validate', async () => { - const { config, errorSpy, validateTagSpy } = await setup(); - validateTagSpy.mockReturnValue('error error error'); - await taskGenerate(config); - expect(config.sys.exit).toHaveBeenCalledWith(1); - expect(errorSpy).toHaveBeenCalledWith('error error error'); - }); - - it.each([true, false])('should create a directory for the generated components', async (includeTests) => { - const { config } = await setup(); - if (!includeTests) { - promptMock.mockResolvedValue({ - tagName: 'my-component', - // simulate the user picking only the css option - filesToGenerate: ['css'], - }); - } - - const createDirSpy = jest.spyOn(config.sys, 'createDir'); - await silentGenerate(config); - expect(createDirSpy).toHaveBeenCalledWith( - includeTests ? `${config.srcDir}/components/my-component/test` : `${config.srcDir}/components/my-component`, - { recursive: true }, - ); - }); - - it('should generate the files the user picked', async () => { - const { config } = await setup(); - const writeFileSpy = jest.spyOn(config.sys, 'writeFile'); - await silentGenerate(config); - const userChoices: ReadonlyArray = [ - { extension: 'tsx', path: '/src/components/my-component/my-component.tsx' }, - { extension: 'css', path: '/src/components/my-component/my-component.css' }, - { extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' }, - { extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' }, - ]; - - userChoices.forEach((file) => { - expect(writeFileSpy).toHaveBeenCalledWith( - file.path, - getBoilerplateByExtension('my-component', file.extension, true, 'css'), - ); - }); - }); - - it('should error without writing anything if a to-be-generated file is already present', async () => { - const { config, errorSpy } = await setup(); - jest.spyOn(config.sys, 'readFile').mockResolvedValue('some file contents'); - await silentGenerate(config); - expect(errorSpy).toHaveBeenCalledWith( - 'Generating code would overwrite the following files:', - '\t/src/components/my-component/my-component.tsx', - '\t/src/components/my-component/my-component.css', - '\t/src/components/my-component/test/my-component.spec.tsx', - '\t/src/components/my-component/test/my-component.e2e.ts', - ); - expect(config.sys.exit).toHaveBeenCalledWith(1); - }); - - it('should generate files for sass projects', async () => { - const { config } = await setup([{ name: 'sass' }]); - const writeFileSpy = jest.spyOn(config.sys, 'writeFile'); - await silentGenerate(config); - const userChoices: ReadonlyArray = [ - { extension: 'tsx', path: '/src/components/my-component/my-component.tsx' }, - { extension: 'sass', path: '/src/components/my-component/my-component.sass' }, - { extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' }, - { extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' }, - ]; - - userChoices.forEach((file) => { - expect(writeFileSpy).toHaveBeenCalledWith( - file.path, - getBoilerplateByExtension('my-component', file.extension, true, 'sass'), - ); - }); - }); - - it('should generate files for less projects', async () => { - formatToPick = 'less'; - const { config } = await setup([{ name: 'less' }]); - const writeFileSpy = jest.spyOn(config.sys, 'writeFile'); - await silentGenerate(config); - const userChoices: ReadonlyArray = [ - { extension: 'tsx', path: '/src/components/my-component/my-component.tsx' }, - { extension: 'less', path: '/src/components/my-component/my-component.less' }, - { extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' }, - { extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' }, - ]; - - userChoices.forEach((file) => { - expect(writeFileSpy).toHaveBeenCalledWith( - file.path, - getBoilerplateByExtension('my-component', file.extension, true, 'less'), - ); - }); - }); -}); diff --git a/src/client/client-build.ts b/src/client/client-build.ts deleted file mode 100644 index ebf6b46481c..00000000000 --- a/src/client/client-build.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BUILD } from '@app-data'; - -import type * as d from '../declarations'; - -export const Build: d.UserBuildConditionals = { - isDev: BUILD.isDev ? true : false, - isBrowser: true, - isServer: false, - isTesting: BUILD.isTesting ? true : false, -}; diff --git a/src/client/client-host-ref.ts b/src/client/client-host-ref.ts deleted file mode 100644 index 5b3e013c8d6..00000000000 --- a/src/client/client-host-ref.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { BUILD } from '@app-data'; -import { CMP_FLAGS } from '@utils/constants'; -import { reWireGetterSetter } from '@utils/es2022-rewire-class-members'; - -import type * as d from '../declarations'; - -/** - * Given a {@link d.RuntimeRef} retrieve the corresponding {@link d.HostRef} - * - * @param ref the runtime ref of interest - * @returns the Host reference (if found) or undefined - */ -export const getHostRef = (ref: d.RuntimeRef): d.HostRef | undefined => { - if (ref.__stencil__getHostRef) { - return ref.__stencil__getHostRef(); - } - - return undefined; -}; - -/** - * Register a lazy instance with the {@link hostRefs} object so it's - * corresponding {@link d.HostRef} can be retrieved later. - * - * @param lazyInstance the lazy instance of interest - * @param hostRef that instances `HostRef` object - */ -export const registerInstance = (lazyInstance: any, hostRef: d.HostRef) => { - if (!hostRef) return; - lazyInstance.__stencil__getHostRef = () => hostRef; - hostRef.$lazyInstance$ = lazyInstance; - - if (hostRef.$cmpMeta$.$flags$ & CMP_FLAGS.hasModernPropertyDecls && (BUILD.state || BUILD.prop)) { - reWireGetterSetter(lazyInstance, hostRef); - } -}; - -/** - * Register a host element for a Stencil component, setting up various metadata - * and callbacks based on {@link BUILD} flags as well as the component's runtime - * metadata. - * - * @param hostElement the host element to register - * @param cmpMeta runtime metadata for that component - * @returns a reference to the host ref WeakMap - */ -export const registerHost = (hostElement: d.HostElement, cmpMeta: d.ComponentRuntimeMeta) => { - const hostRef: d.HostRef = { - $flags$: 0, - $hostElement$: hostElement, - $cmpMeta$: cmpMeta, - $instanceValues$: new Map(), - $serializerValues$: new Map(), - }; - if (BUILD.isDev) { - hostRef.$renderCount$ = 0; - } - if (BUILD.method && BUILD.lazyLoad) { - hostRef.$onInstancePromise$ = new Promise((r) => (hostRef.$onInstanceResolve$ = r)); - } - if (BUILD.asyncLoading) { - hostRef.$onReadyPromise$ = new Promise((r) => (hostRef.$onReadyResolve$ = r)); - hostElement['s-p'] = []; - hostElement['s-rc'] = []; - } - if (BUILD.lazyLoad) { - hostRef.$fetchedCbList$ = []; - } - - const ref = hostRef; - hostElement.__stencil__getHostRef = () => ref; - - if (!BUILD.lazyLoad && cmpMeta.$flags$ & CMP_FLAGS.hasModernPropertyDecls && (BUILD.state || BUILD.prop)) { - reWireGetterSetter(hostElement, hostRef); - } - - return ref; -}; - -export const isMemberInElement = (elm: any, memberName: string) => memberName in elm; diff --git a/src/client/client-patch-browser.ts b/src/client/client-patch-browser.ts deleted file mode 100644 index e5e813faace..00000000000 --- a/src/client/client-patch-browser.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { BUILD, NAMESPACE } from '@app-data'; -import { consoleDevInfo, H, promiseResolve, win } from '@platform'; - -import type * as d from '../declarations'; - -export const patchBrowser = (): Promise => { - // NOTE!! This fn cannot use async/await! - if (BUILD.isDev && !BUILD.isTesting) { - consoleDevInfo('Running in development mode.'); - } - - if (BUILD.cloneNodeFix) { - // opted-in to polyfill cloneNode() for slot polyfilled components - patchCloneNodeFix((H as any).prototype); - } - - const scriptElm = BUILD.scriptDataOpts - ? win.document && - Array.from(win.document.querySelectorAll('script')).find( - (s) => - new RegExp(`\/${NAMESPACE}(\\.esm)?\\.js($|\\?|#)`).test(s.src) || - s.getAttribute('data-stencil-namespace') === NAMESPACE, - ) - : null; - const importMeta = import.meta.url; - const opts = BUILD.scriptDataOpts ? ((scriptElm as any) || {})['data-opts'] || {} : {}; - - if (importMeta !== '') { - opts.resourcesUrl = new URL('.', importMeta).href; - } - - return promiseResolve(opts); -}; - -const patchCloneNodeFix = (HTMLElementPrototype: any) => { - const nativeCloneNodeFn = HTMLElementPrototype.cloneNode; - - HTMLElementPrototype.cloneNode = function (this: Node, deep: boolean) { - if (this.nodeName === 'TEMPLATE') { - return nativeCloneNodeFn.call(this, deep); - } - const clonedNode = nativeCloneNodeFn.call(this, false); - const srcChildNodes = this.childNodes; - if (deep) { - for (let i = 0; i < srcChildNodes.length; i++) { - // Node.ATTRIBUTE_NODE === 2, and checking because IE11 - if (srcChildNodes[i].nodeType !== 2) { - clonedNode.appendChild(srcChildNodes[i].cloneNode(true)); - } - } - } - return clonedNode; - }; -}; diff --git a/src/client/client-style.ts b/src/client/client-style.ts deleted file mode 100644 index 75065613a9f..00000000000 --- a/src/client/client-style.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type * as d from '../declarations'; - -export const styles: d.StyleMap = /*@__PURE__*/ new Map(); -export const modeResolutionChain: d.ResolutionHandler[] = []; -export const setScopedSSR = (_opts: d.HydrateFactoryOptions) => {}; -export const needsScopedSSR = () => false; diff --git a/src/client/client-window.ts b/src/client/client-window.ts deleted file mode 100644 index 2a5258e8b92..00000000000 --- a/src/client/client-window.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { BUILD } from '@app-data'; - -import type * as d from '../declarations'; - -interface StencilWindow extends Omit { - document?: Document; -} - -export const win = (typeof window !== 'undefined' ? window : ({} as StencilWindow)) as StencilWindow; - -export const H = ((win as any).HTMLElement || (class {} as any)) as HTMLElement; - -export const plt: d.PlatformRuntime = { - $flags$: 0, - $resourcesUrl$: '', - jmp: (h) => h(), - raf: (h) => requestAnimationFrame(h), - ael: (el, eventName, listener, opts) => el.addEventListener(eventName, listener, opts), - rel: (el, eventName, listener, opts) => el.removeEventListener(eventName, listener, opts), - ce: (eventName, opts) => new CustomEvent(eventName, opts), -}; - -export const setPlatformHelpers = (helpers: { - jmp?: (c: any) => any; - raf?: (c: any) => number; - ael?: (el: any, eventName: string, listener: any, options: any) => void; - rel?: (el: any, eventName: string, listener: any, options: any) => void; - ce?: (eventName: string, opts?: any) => any; -}) => { - Object.assign(plt, helpers); -}; - -export const supportsShadow = BUILD.shadowDom; - -export const supportsListenerOptions = /*@__PURE__*/ (() => { - let supportsListenerOptions = false; - try { - win.document?.addEventListener( - 'e', - null, - Object.defineProperty({}, 'passive', { - get() { - supportsListenerOptions = true; - }, - }), - ); - } catch (e) {} - return supportsListenerOptions; -})(); - -export const promiseResolve = (v?: any) => Promise.resolve(v); - -export const supportsConstructableStylesheets = BUILD.constructableCSS - ? /*@__PURE__*/ (() => { - try { - if (!win.document.adoptedStyleSheets) { - return false; - } - new CSSStyleSheet(); - return typeof new CSSStyleSheet().replaceSync === 'function'; - } catch (e) {} - return false; - })() - : false; - -// https://github.com/salesforce/lwc/blob/5af18fdd904bc6cfcf7b76f3c539490ff11515b2/packages/%40lwc/engine-dom/src/renderer.ts#L41-L43 -export const supportsMutableAdoptedStyleSheets = supportsConstructableStylesheets - ? /*@__PURE__*/ (() => - !!win.document && Object.getOwnPropertyDescriptor(win.document.adoptedStyleSheets, 'length')!.writable)() - : false; - -export { H as HTMLElement }; diff --git a/src/client/index.ts b/src/client/index.ts deleted file mode 100644 index 372348bde66..00000000000 --- a/src/client/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './client-build'; -export * from './client-host-ref'; -export * from './client-load-module'; -export * from './client-log'; -export * from './client-style'; -export * from './client-task-queue'; -export * from './client-window'; -export { BUILD, Env, NAMESPACE } from '@app-data'; -export * from '@runtime'; diff --git a/src/client/polyfills/core-js.js b/src/client/polyfills/core-js.js deleted file mode 100755 index 7bbc909ed48..00000000000 --- a/src/client/polyfills/core-js.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * core-js 3.6.5 - * https://github.com/zloirock/core-js - * License: http://rock.mit-license.org - * © 2019 Denis Pushkarev (zloirock.ru) - */ -!function(t){"use strict";!function(t){var n={};function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}e.m=t,e.c=n,e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:r})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,n){if(1&n&&(t=e(t)),8&n)return t;if(4&n&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(e.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&n&&"string"!=typeof t)for(var o in t)e.d(r,o,function(n){return t[n]}.bind(null,o));return r},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},e.p="",e(e.s=0)}([function(t,n,e){e(1),e(55),e(62),e(68),e(70),e(71),e(72),e(73),e(75),e(76),e(78),e(87),e(88),e(89),e(98),e(99),e(101),e(102),e(103),e(105),e(106),e(107),e(108),e(110),e(111),e(112),e(113),e(114),e(115),e(116),e(117),e(118),e(127),e(130),e(131),e(133),e(135),e(136),e(137),e(138),e(139),e(141),e(143),e(146),e(148),e(150),e(151),e(153),e(154),e(155),e(156),e(157),e(159),e(160),e(162),e(163),e(164),e(165),e(166),e(167),e(168),e(169),e(170),e(172),e(173),e(183),e(184),e(185),e(189),e(191),e(192),e(193),e(194),e(195),e(196),e(198),e(201),e(202),e(203),e(204),e(208),e(209),e(212),e(213),e(214),e(215),e(216),e(217),e(218),e(219),e(221),e(222),e(223),e(226),e(227),e(228),e(229),e(230),e(231),e(232),e(233),e(234),e(235),e(236),e(237),e(238),e(240),e(241),e(243),e(248),t.exports=e(246)},function(t,n,e){var r=e(2),o=e(6),i=e(45),a=e(14),u=e(46),c=e(39),f=e(47),s=e(48),l=e(52),p=e(49),h=e(53),v=p("isConcatSpreadable"),g=h>=51||!o((function(){var t=[];return t[v]=!1,t.concat()[0]!==t})),d=l("concat"),y=function(t){if(!a(t))return!1;var n=t[v];return void 0!==n?!!n:i(t)};r({target:"Array",proto:!0,forced:!g||!d},{concat:function(t){var n,e,r,o,i,a=u(this),l=s(a,0),p=0;for(n=-1,r=arguments.length;n9007199254740991)throw TypeError("Maximum allowed index exceeded");for(e=0;e=9007199254740991)throw TypeError("Maximum allowed index exceeded");f(l,p++,i)}return l.length=p,l}})},function(t,n,e){var r=e(3),o=e(4).f,i=e(18),a=e(21),u=e(22),c=e(32),f=e(44);t.exports=function(t,n){var e,s,l,p,h,v=t.target,g=t.global,d=t.stat;if(e=g?r:d?r[v]||u(v,{}):(r[v]||{}).prototype)for(s in n){if(p=n[s],l=t.noTargetGet?(h=o(e,s))&&h.value:e[s],!f(g?s:v+(d?".":"#")+s,t.forced)&&void 0!==l){if(typeof p==typeof l)continue;c(p,l)}(t.sham||l&&l.sham)&&i(p,"sham",!0),a(e,s,p,t)}}},function(t,n){var e=function(t){return t&&t.Math==Math&&t};t.exports=e("object"==typeof globalThis&&globalThis)||e("object"==typeof window&&window)||e("object"==typeof self&&self)||e("object"==typeof global&&global)||Function("return this")()},function(t,n,e){var r=e(5),o=e(7),i=e(8),a=e(9),u=e(13),c=e(15),f=e(16),s=Object.getOwnPropertyDescriptor;n.f=r?s:function(t,n){if(t=a(t),n=u(n,!0),f)try{return s(t,n)}catch(t){}if(c(t,n))return i(!o.f.call(t,n),t[n])}},function(t,n,e){var r=e(6);t.exports=!r((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},function(t,n){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,n,e){var r={}.propertyIsEnumerable,o=Object.getOwnPropertyDescriptor,i=o&&!r.call({1:2},1);n.f=i?function(t){var n=o(this,t);return!!n&&n.enumerable}:r},function(t,n){t.exports=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}}},function(t,n,e){var r=e(10),o=e(12);t.exports=function(t){return r(o(t))}},function(t,n,e){var r=e(6),o=e(11),i="".split;t.exports=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==o(t)?i.call(t,""):Object(t)}:Object},function(t,n){var e={}.toString;t.exports=function(t){return e.call(t).slice(8,-1)}},function(t,n){t.exports=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t}},function(t,n,e){var r=e(14);t.exports=function(t,n){if(!r(t))return t;var e,o;if(n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;if("function"==typeof(e=t.valueOf)&&!r(o=e.call(t)))return o;if(!n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,n){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,n){var e={}.hasOwnProperty;t.exports=function(t,n){return e.call(t,n)}},function(t,n,e){var r=e(5),o=e(6),i=e(17);t.exports=!r&&!o((function(){return 7!=Object.defineProperty(i("div"),"a",{get:function(){return 7}}).a}))},function(t,n,e){var r=e(3),o=e(14),i=r.document,a=o(i)&&o(i.createElement);t.exports=function(t){return a?i.createElement(t):{}}},function(t,n,e){var r=e(5),o=e(19),i=e(8);t.exports=r?function(t,n,e){return o.f(t,n,i(1,e))}:function(t,n,e){return t[n]=e,t}},function(t,n,e){var r=e(5),o=e(16),i=e(20),a=e(13),u=Object.defineProperty;n.f=r?u:function(t,n,e){if(i(t),n=a(n,!0),i(e),o)try{return u(t,n,e)}catch(t){}if("get"in e||"set"in e)throw TypeError("Accessors not supported");return"value"in e&&(t[n]=e.value),t}},function(t,n,e){var r=e(14);t.exports=function(t){if(!r(t))throw TypeError(String(t)+" is not an object");return t}},function(t,n,e){var r=e(3),o=e(18),i=e(15),a=e(22),u=e(23),c=e(25),f=c.get,s=c.enforce,l=String(String).split("String");(t.exports=function(t,n,e,u){var c=!!u&&!!u.unsafe,f=!!u&&!!u.enumerable,p=!!u&&!!u.noTargetGet;"function"==typeof e&&("string"!=typeof n||i(e,"name")||o(e,"name",n),s(e).source=l.join("string"==typeof n?n:"")),t!==r?(c?!p&&t[n]&&(f=!0):delete t[n],f?t[n]=e:o(t,n,e)):f?t[n]=e:a(n,e)})(Function.prototype,"toString",(function(){return"function"==typeof this&&f(this).source||u(this)}))},function(t,n,e){var r=e(3),o=e(18);t.exports=function(t,n){try{o(r,t,n)}catch(e){r[t]=n}return n}},function(t,n,e){var r=e(24),o=Function.toString;"function"!=typeof r.inspectSource&&(r.inspectSource=function(t){return o.call(t)}),t.exports=r.inspectSource},function(t,n,e){var r=e(3),o=e(22),i=r["__core-js_shared__"]||o("__core-js_shared__",{});t.exports=i},function(t,n,e){var r,o,i,a=e(26),u=e(3),c=e(14),f=e(18),s=e(15),l=e(27),p=e(31),h=u.WeakMap;if(a){var v=new h,g=v.get,d=v.has,y=v.set;r=function(t,n){return y.call(v,t,n),n},o=function(t){return g.call(v,t)||{}},i=function(t){return d.call(v,t)}}else{var x=l("state");p[x]=!0,r=function(t,n){return f(t,x,n),n},o=function(t){return s(t,x)?t[x]:{}},i=function(t){return s(t,x)}}t.exports={set:r,get:o,has:i,enforce:function(t){return i(t)?o(t):r(t,{})},getterFor:function(t){return function(n){var e;if(!c(n)||(e=o(n)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return e}}}},function(t,n,e){var r=e(3),o=e(23),i=r.WeakMap;t.exports="function"==typeof i&&/native code/.test(o(i))},function(t,n,e){var r=e(28),o=e(30),i=r("keys");t.exports=function(t){return i[t]||(i[t]=o(t))}},function(t,n,e){var r=e(29),o=e(24);(t.exports=function(t,n){return o[t]||(o[t]=void 0!==n?n:{})})("versions",[]).push({version:"3.6.5",mode:r?"pure":"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})},function(t,n){t.exports=!1},function(t,n){var e=0,r=Math.random();t.exports=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++e+r).toString(36)}},function(t,n){t.exports={}},function(t,n,e){var r=e(15),o=e(33),i=e(4),a=e(19);t.exports=function(t,n){for(var e=o(n),u=a.f,c=i.f,f=0;fc;)r(u,e=n[c++])&&(~i(f,e)||f.push(e));return f}},function(t,n,e){var r=e(9),o=e(39),i=e(41),a=function(t){return function(n,e,a){var u,c=r(n),f=o(c.length),s=i(a,f);if(t&&e!=e){for(;f>s;)if((u=c[s++])!=u)return!0}else for(;f>s;s++)if((t||s in c)&&c[s]===e)return t||s||0;return!t&&-1}};t.exports={includes:a(!0),indexOf:a(!1)}},function(t,n,e){var r=e(40),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},function(t,n){var e=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:e)(t)}},function(t,n,e){var r=e(40),o=Math.max,i=Math.min;t.exports=function(t,n){var e=r(t);return e<0?o(e+n,0):i(e,n)}},function(t,n){t.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},function(t,n){n.f=Object.getOwnPropertySymbols},function(t,n,e){var r=e(6),o=/#|\.prototype\./,i=function(t,n){var e=u[a(t)];return e==f||e!=c&&("function"==typeof n?r(n):!!n)},a=i.normalize=function(t){return String(t).replace(o,".").toLowerCase()},u=i.data={},c=i.NATIVE="N",f=i.POLYFILL="P";t.exports=i},function(t,n,e){var r=e(11);t.exports=Array.isArray||function(t){return"Array"==r(t)}},function(t,n,e){var r=e(12);t.exports=function(t){return Object(r(t))}},function(t,n,e){var r=e(13),o=e(19),i=e(8);t.exports=function(t,n,e){var a=r(n);a in t?o.f(t,a,i(0,e)):t[a]=e}},function(t,n,e){var r=e(14),o=e(45),i=e(49)("species");t.exports=function(t,n){var e;return o(t)&&("function"!=typeof(e=t.constructor)||e!==Array&&!o(e.prototype)?r(e)&&null===(e=e[i])&&(e=void 0):e=void 0),new(void 0===e?Array:e)(0===n?0:n)}},function(t,n,e){var r=e(3),o=e(28),i=e(15),a=e(30),u=e(50),c=e(51),f=o("wks"),s=r.Symbol,l=c?s:s&&s.withoutSetter||a;t.exports=function(t){return i(f,t)||(u&&i(s,t)?f[t]=s[t]:f[t]=l("Symbol."+t)),f[t]}},function(t,n,e){var r=e(6);t.exports=!!Object.getOwnPropertySymbols&&!r((function(){return!String(Symbol())}))},function(t,n,e){var r=e(50);t.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},function(t,n,e){var r=e(6),o=e(49),i=e(53),a=o("species");t.exports=function(t){return i>=51||!r((function(){var n=[];return(n.constructor={})[a]=function(){return{foo:1}},1!==n[t](Boolean).foo}))}},function(t,n,e){var r,o,i=e(3),a=e(54),u=i.process,c=u&&u.versions,f=c&&c.v8;f?o=(r=f.split("."))[0]+r[1]:a&&(!(r=a.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=a.match(/Chrome\/(\d+)/))&&(o=r[1]),t.exports=o&&+o},function(t,n,e){var r=e(34);t.exports=r("navigator","userAgent")||""},function(t,n,e){var r=e(2),o=e(56),i=e(57);r({target:"Array",proto:!0},{copyWithin:o}),i("copyWithin")},function(t,n,e){var r=e(46),o=e(41),i=e(39),a=Math.min;t.exports=[].copyWithin||function(t,n){var e=r(this),u=i(e.length),c=o(t,u),f=o(n,u),s=arguments.length>2?arguments[2]:void 0,l=a((void 0===s?u:o(s,u))-f,u-c),p=1;for(f0;)f in e?e[c]=e[f]:delete e[c],c+=p,f+=p;return e}},function(t,n,e){var r=e(49),o=e(58),i=e(19),a=r("unscopables"),u=Array.prototype;null==u[a]&&i.f(u,a,{configurable:!0,value:o(null)}),t.exports=function(t){u[a][t]=!0}},function(t,n,e){var r,o=e(20),i=e(59),a=e(42),u=e(31),c=e(61),f=e(17),s=e(27),l=s("IE_PROTO"),p=function(){},h=function(t){return" -`)}
    -
    - -

    AFTER:

    -

    The index.html should now include two scripts using the modern ES Module script pattern. - Note that only one file will actually be requested and loaded based on the browser's native support for ES Modules. - For more info, please see Using JavaScript modules on the web. -

    -
    -  ${escapeHtml(`type="module" src="/build/${
    -    config.fsNamespace
    -  }.esm.js"${escapeHtml(`>`)}
    -  ${escapeHtml(`nomodule ${escapeHtml(
    -    `src="/build/${config.fsNamespace}.js">`,
    -  )}
    -    
    - `; - return `${generatePreamble(config)} -(function() { - function checkSupport() { - if (!document.body) { - setTimeout(checkSupport); - return; - } - function supportsDynamicImports() { - try { - new Function('import("")'); - return true; - } catch (e) {} - return false; - } - var supportsEsModules = !!('noModule' in document.createElement('script')); - - if (!supportsEsModules) { - document.body.innerHTML = '${inlineHTML(htmlLegacy)}'; - - document.getElementById('current-browser-output').textContent = window.navigator.userAgent; - document.getElementById('es-modules-test').textContent = supportsEsModules; - document.getElementById('es-dynamic-modules-test').textContent = supportsDynamicImports(); - document.getElementById('shadow-dom-test').textContent = !!(document.head.attachShadow); - document.getElementById('custom-elements-test').textContent = !!(window.customElements); - document.getElementById('css-variables-test').textContent = !!(window.CSS && window.CSS.supports && window.CSS.supports('color', 'var(--c)')); - document.getElementById('fetch-test').textContent = !!(window.fetch); - } else { - document.body.innerHTML = '${inlineHTML(htmlUpdate)}'; - } - } - - setTimeout(checkSupport); -})();`; -}; - -const inlineHTML = (html: string) => { - return html.replace(/\n/g, '\\n').replace(/\'/g, `\\'`).trim(); -}; diff --git a/src/compiler/app-core/app-polyfills.ts b/src/compiler/app-core/app-polyfills.ts deleted file mode 100644 index efa49f5d07e..00000000000 --- a/src/compiler/app-core/app-polyfills.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { join } from '@utils'; - -import type * as d from '../../declarations'; - -export const getClientPolyfill = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - polyfillFile: string, -) => { - const polyfillFilePath = join( - config.sys.getCompilerExecutingPath(), - '..', - '..', - 'internal', - 'client', - 'polyfills', - polyfillFile, - ); - return compilerCtx.fs.readFile(polyfillFilePath); -}; - -export const getAppBrowserCorePolyfills = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx) => { - // read all the polyfill content, in this particular order - const polyfills = INLINE_POLYFILLS.slice(); - - const results = await Promise.all( - polyfills.map((polyfillFile) => getClientPolyfill(config, compilerCtx, polyfillFile)), - ); - - // concat the polyfills - return results.join('\n').trim(); -}; - -// order of the polyfills matters!! test test test -// actual source of the polyfills are found in /src/client/polyfills/ -const INLINE_POLYFILLS = ['core-js.js', 'dom.js', 'es5-html-element.js', 'system.js']; diff --git a/src/compiler/build/build-hmr.ts b/src/compiler/build/build-hmr.ts deleted file mode 100644 index 50f0d6002b5..00000000000 --- a/src/compiler/build/build-hmr.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { isGlob, isOutputTargetWww, normalizePath, sortBy } from '@utils'; -import { minimatch } from 'minimatch'; -import { basename } from 'path'; - -import type * as d from '../../declarations'; -import { getScopeId } from '../style/scope-css'; - -export const generateHmr = (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - if (config.devServer?.reloadStrategy == null) { - return null; - } - - const hmr: d.HotModuleReplacement = { - reloadStrategy: config.devServer.reloadStrategy, - versionId: Date.now().toString().substring(6) + '' + Math.round(Math.random() * 89999 + 10000), - }; - - if (buildCtx.scriptsAdded.length > 0) { - hmr.scriptsAdded = buildCtx.scriptsAdded.slice(); - } - - if (buildCtx.scriptsDeleted.length > 0) { - hmr.scriptsDeleted = buildCtx.scriptsDeleted.slice(); - } - - const excludeHmr = excludeHmrFiles(config, config.devServer.excludeHmr, buildCtx.filesChanged); - if (excludeHmr.length > 0) { - hmr.excludeHmr = excludeHmr.slice(); - } - - if (buildCtx.hasHtmlChanges) { - hmr.indexHtmlUpdated = true; - } - - if (buildCtx.hasServiceWorkerChanges) { - hmr.serviceWorkerUpdated = true; - } - - const outputTargetsWww = config.outputTargets.filter(isOutputTargetWww); - - const componentsUpdated = getComponentsUpdated(compilerCtx, buildCtx); - if (componentsUpdated) { - hmr.componentsUpdated = componentsUpdated; - } - - if (Object.keys(buildCtx.stylesUpdated).length > 0) { - hmr.inlineStylesUpdated = sortBy( - buildCtx.stylesUpdated.map((s) => { - return { - styleId: getScopeId(s.styleTag, s.styleMode), - styleTag: s.styleTag, - styleText: s.styleText, - } as d.HmrStyleUpdate; - }), - (s) => s.styleId, - ); - } - - const externalStylesUpdated = getExternalStylesUpdated(buildCtx, outputTargetsWww); - if (externalStylesUpdated) { - hmr.externalStylesUpdated = externalStylesUpdated; - } - - const externalImagesUpdated = getImagesUpdated(buildCtx, outputTargetsWww); - if (externalImagesUpdated) { - hmr.imagesUpdated = externalImagesUpdated; - } - - return hmr; -}; - -const getComponentsUpdated = (compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - // find all of the components that would be affected from the file changes - if (!buildCtx.filesChanged) { - return null; - } - - const filesToLookForImporters = buildCtx.filesChanged.filter((f) => { - return f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.js') || f.endsWith('.jsx'); - }); - - if (filesToLookForImporters.length === 0) { - return null; - } - - const changedScriptFiles: string[] = []; - const checkedFiles = new Set(); - const allModuleFiles = buildCtx.moduleFiles.filter((m) => m.localImports && m.localImports.length > 0); - - while (filesToLookForImporters.length > 0) { - const scriptFile = filesToLookForImporters.shift(); - addTsFileImporters(allModuleFiles, filesToLookForImporters, checkedFiles, changedScriptFiles, scriptFile); - } - - const tags = changedScriptFiles.reduce((tags, changedTsFile) => { - const moduleFile = compilerCtx.moduleMap.get(changedTsFile); - if (moduleFile != null) { - moduleFile.cmps.forEach((cmp) => { - if (typeof cmp.tagName === 'string') { - if (!tags.includes(cmp.tagName)) { - tags.push(cmp.tagName); - } - } - }); - } - return tags; - }, [] as string[]); - - if (tags.length === 0) { - return null; - } - - return tags.sort(); -}; - -const addTsFileImporters = ( - allModuleFiles: d.Module[], - filesToLookForImporters: string[], - checkedFiles: Set, - changedScriptFiles: string[], - scriptFile: string, -) => { - if (!changedScriptFiles.includes(scriptFile)) { - // add it to our list of files to transpile - changedScriptFiles.push(scriptFile); - } - - if (checkedFiles.has(scriptFile)) { - // already checked this file - return; - } - checkedFiles.add(scriptFile); - - // get all the ts files that import this ts file - const tsFilesThatImportsThisTsFile = allModuleFiles.reduce((arr, moduleFile) => { - moduleFile.localImports.forEach((localImport) => { - let checkFile = localImport; - - if (checkFile === scriptFile) { - arr.push(moduleFile.sourceFilePath); - return; - } - - checkFile = localImport + '.tsx'; - if (checkFile === scriptFile) { - arr.push(moduleFile.sourceFilePath); - return; - } - - checkFile = localImport + '.ts'; - if (checkFile === scriptFile) { - arr.push(moduleFile.sourceFilePath); - return; - } - - checkFile = localImport + '.js'; - if (checkFile === scriptFile) { - arr.push(moduleFile.sourceFilePath); - return; - } - }); - return arr; - }, [] as string[]); - - // add all the files that import this ts file to the list of ts files we need to look through - tsFilesThatImportsThisTsFile.forEach((tsFileThatImportsThisTsFile) => { - // if we add to this array, then the while look will keep working until it's empty - filesToLookForImporters.push(tsFileThatImportsThisTsFile); - }); -}; - -const getExternalStylesUpdated = (buildCtx: d.BuildCtx, outputTargetsWww: d.OutputTargetWww[]) => { - if (!buildCtx.isRebuild || outputTargetsWww.length === 0) { - return null; - } - - const cssFiles = buildCtx.filesWritten.filter((f) => f.endsWith('.css')); - if (cssFiles.length === 0) { - return null; - } - - return cssFiles.map((cssFile) => basename(cssFile)).sort(); -}; - -const getImagesUpdated = (buildCtx: d.BuildCtx, outputTargetsWww: d.OutputTargetWww[]) => { - if (outputTargetsWww.length === 0) { - return null; - } - - const imageFiles = buildCtx.filesChanged.reduce((arr, filePath) => { - if (IMAGE_EXT.some((ext) => filePath.toLowerCase().endsWith(ext))) { - const fileName = basename(filePath); - if (!arr.includes(fileName)) { - arr.push(fileName); - } - } - return arr; - }, []); - - if (imageFiles.length === 0) { - return null; - } - - return imageFiles.sort(); -}; - -/** - * Determine a list of files (if any) which should be excluded from HMR updates. - * - * @param config a user-supplied config - * @param excludeHmr a list of glob patterns that should be used to determine - * whether to exclude a file or not (a file will be excluded if it matches one - * @param filesChanged an array of files which are changed in the HMR update - * currently under consideration - * @returns a sorted list of files to exclude - */ -const excludeHmrFiles = (config: d.Config, excludeHmr: string[], filesChanged: string[]): string[] => { - const excludeFiles: string[] = []; - - if (!excludeHmr || excludeHmr.length === 0) { - return excludeFiles; - } - - excludeHmr.forEach((excludeHmr) => { - return filesChanged - .map((fileChanged) => { - let shouldExclude = false; - - if (isGlob(excludeHmr)) { - shouldExclude = minimatch(fileChanged, excludeHmr); - } else { - shouldExclude = normalizePath(excludeHmr) === normalizePath(fileChanged); - } - - if (shouldExclude) { - config.logger.debug(`excludeHmr: ${fileChanged}`); - excludeFiles.push(basename(fileChanged)); - } - - return shouldExclude; - }) - .some((r) => r); - }); - - return excludeFiles.sort(); -}; - -const IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg']; diff --git a/src/compiler/build/build-results.ts b/src/compiler/build/build-results.ts deleted file mode 100644 index cc60d280171..00000000000 --- a/src/compiler/build/build-results.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { fromEntries, hasError, isString, normalizeDiagnostics } from '@utils'; - -import type * as d from '../../declarations'; -import { getBuildTimestamp } from './build-ctx'; -import { generateHmr } from './build-hmr'; - -export const generateBuildResults = (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - const componentGraph = buildCtx.componentGraph ? fromEntries(buildCtx.componentGraph.entries()) : undefined; - - const buildResults: d.CompilerBuildResults = { - buildId: buildCtx.buildId, - diagnostics: normalizeDiagnostics(compilerCtx, buildCtx.diagnostics), - dirsAdded: buildCtx.dirsAdded.slice().sort(), - dirsDeleted: buildCtx.dirsDeleted.slice().sort(), - duration: Date.now() - buildCtx.startTime, - filesAdded: buildCtx.filesAdded.slice().sort(), - filesChanged: buildCtx.filesChanged.slice().sort(), - filesDeleted: buildCtx.filesDeleted.slice().sort(), - filesUpdated: buildCtx.filesUpdated.slice().sort(), - hasError: hasError(buildCtx.diagnostics), - hasSuccessfulBuild: compilerCtx.hasSuccessfulBuild, - isRebuild: buildCtx.isRebuild, - namespace: config.namespace, - outputs: compilerCtx.fs.getBuildOutputs(), - rootDir: config.rootDir, - srcDir: config.srcDir, - timestamp: getBuildTimestamp(), - componentGraph, - }; - - const hmr = generateHmr(config, compilerCtx, buildCtx); - if (hmr != null) { - buildResults.hmr = hmr; - } - - if (isString(buildCtx.hydrateAppFilePath)) { - buildResults.hydrateAppFilePath = buildCtx.hydrateAppFilePath; - } - - compilerCtx.lastBuildResults = Object.assign({}, buildResults as any); - - return buildResults; -}; diff --git a/src/compiler/build/build.ts b/src/compiler/build/build.ts deleted file mode 100644 index 1643991a39c..00000000000 --- a/src/compiler/build/build.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { createDocument } from '@stencil/core/mock-doc'; -import { catchError, isString, readPackageJson } from '@utils'; -import ts from 'typescript'; - -import type * as d from '../../declarations'; -import { generateOutputTargets } from '../output-targets'; -import { emptyOutputTargets } from '../output-targets/empty-dir'; -import { generateGlobalStyles } from '../style/global-styles'; -import { runTsProgram, validateTypesAfterGeneration } from '../transpile/run-program'; -import { buildAbort, buildFinish } from './build-finish'; -import { writeBuild } from './write-build'; - -export const build = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - tsBuilder: ts.BuilderProgram, -) => { - try { - // reset process.cwd() for 3rd-party plugins - process.chdir(config.rootDir); - - // empty the directories on the first build - await emptyOutputTargets(config, compilerCtx, buildCtx); - if (buildCtx.hasError) return buildAbort(buildCtx); - - if (config.srcIndexHtml) { - const indexSrcHtml = await compilerCtx.fs.readFile(config.srcIndexHtml); - if (isString(indexSrcHtml)) { - buildCtx.indexDoc = createDocument(indexSrcHtml); - } - } - - await readPackageJson(config, compilerCtx, buildCtx); - if (buildCtx.hasError) return buildAbort(buildCtx); - - // run typescript program - const tsTimeSpan = buildCtx.createTimeSpan('transpile started'); - const emittedDts = await runTsProgram(config, compilerCtx, buildCtx, tsBuilder); - tsTimeSpan.finish('transpile finished'); - if (buildCtx.hasError) return buildAbort(buildCtx); - - // generate types and validate AFTER components.d.ts is written - const { hasTypesChanged, needsRebuild } = await validateTypesAfterGeneration( - config, - compilerCtx, - buildCtx, - tsBuilder, - emittedDts, - ); - if (buildCtx.hasError) return buildAbort(buildCtx); - - if (needsRebuild || (config.watch && hasTypesChanged)) { - // Abort and signal that a rebuild is needed: - // - needsRebuild: components.d.ts was just generated, need fresh TS program - // - watch mode with types changed: let watch trigger rebuild - return null; - } - - // preprocess and generate styles before any outputTarget starts - buildCtx.stylesPromise = generateGlobalStyles(config, compilerCtx, buildCtx); - if (buildCtx.hasError) return buildAbort(buildCtx); - - // create outputs - await generateOutputTargets(config, compilerCtx, buildCtx); - if (buildCtx.hasError) return buildAbort(buildCtx); - - // write outputs - await buildCtx.stylesPromise; - await writeBuild(config, compilerCtx, buildCtx); - } catch (e: any) { - // ¯\_(ツ)_/¯ - catchError(buildCtx.diagnostics, e); - } - - // TODO - // clear changed files - compilerCtx.changedFiles.clear(); - - // return what we've learned today - return buildFinish(buildCtx); -}; diff --git a/src/compiler/build/test/write-export-maps.spec.ts b/src/compiler/build/test/write-export-maps.spec.ts deleted file mode 100644 index 17c49e91435..00000000000 --- a/src/compiler/build/test/write-export-maps.spec.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { mockBuildCtx, mockValidatedConfig } from '@stencil/core/testing'; -import childProcess from 'child_process'; - -import * as d from '../../../declarations'; -import { stubComponentCompilerMeta } from '../../types/tests/ComponentCompilerMeta.stub'; -import { writeExportMaps } from '../write-export-maps'; - -describe('writeExportMaps', () => { - let config: d.ValidatedConfig; - let buildCtx: d.BuildCtx; - let execSyncSpy: jest.SpyInstance; - - beforeEach(() => { - config = mockValidatedConfig(); - buildCtx = mockBuildCtx(config); - - execSyncSpy = jest.spyOn(childProcess, 'execSync').mockImplementation(() => ''); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should not generate any exports if there are no output targets', () => { - writeExportMaps(config, buildCtx); - - expect(execSyncSpy).toHaveBeenCalledTimes(0); - }); - - it('should generate the default exports for the lazy build if present', () => { - config.outputTargets = [ - { - type: 'dist', - dir: '/dist', - typesDir: '/dist/types', - }, - ]; - - writeExportMaps(config, buildCtx); - - expect(execSyncSpy).toHaveBeenCalledTimes(3); - expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][import]"="./dist/index.js"`); - expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][require]"="./dist/index.cjs.js"`); - expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][types]"="./dist/types/index.d.ts"`); - }); - - it('should generate the default exports for the custom elements build if present', () => { - config.outputTargets = [ - { - type: 'dist-custom-elements', - dir: '/dist/components', - generateTypeDeclarations: true, - }, - ]; - - writeExportMaps(config, buildCtx); - - expect(execSyncSpy).toHaveBeenCalledTimes(2); - expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][import]"="./dist/components/index.js"`); - expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][types]"="./dist/components/index.d.ts"`); - }); - - it('should generate the lazy loader exports if the output target is present', () => { - config.rootDir = '/'; - config.outputTargets.push({ - type: 'dist-lazy-loader', - dir: '/dist/lazy-loader', - empty: true, - esmDir: '/dist/esm', - cjsDir: '/dist/cjs', - componentDts: '/dist/components.d.ts', - }); - - writeExportMaps(config, buildCtx); - - expect(execSyncSpy).toHaveBeenCalledTimes(3); - expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[./loader][import]"="./dist/lazy-loader/index.js"`); - expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[./loader][require]"="./dist/lazy-loader/index.cjs"`); - expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[./loader][types]"="./dist/lazy-loader/index.d.ts"`); - }); - - it('should generate the custom elements exports if the output target is present', () => { - config.rootDir = '/'; - config.outputTargets.push({ - type: 'dist-custom-elements', - dir: '/dist/components', - generateTypeDeclarations: true, - }); - - buildCtx.components = [ - stubComponentCompilerMeta({ - tagName: 'my-component', - componentClassName: 'MyComponent', - }), - ]; - - writeExportMaps(config, buildCtx); - - expect(execSyncSpy).toHaveBeenCalledTimes(4); - expect(execSyncSpy).toHaveBeenCalledWith( - `npm pkg set "exports[./my-component][import]"="./dist/components/my-component.js"`, - ); - expect(execSyncSpy).toHaveBeenCalledWith( - `npm pkg set "exports[./my-component][types]"="./dist/components/my-component.d.ts"`, - ); - }); - - it('should generate the custom elements exports for multiple components', () => { - config.rootDir = '/'; - config.outputTargets.push({ - type: 'dist-custom-elements', - dir: '/dist/components', - generateTypeDeclarations: true, - }); - - buildCtx.components = [ - stubComponentCompilerMeta({ - tagName: 'my-component', - componentClassName: 'MyComponent', - }), - stubComponentCompilerMeta({ - tagName: 'my-other-component', - componentClassName: 'MyOtherComponent', - }), - ]; - - writeExportMaps(config, buildCtx); - - expect(execSyncSpy).toHaveBeenCalledTimes(6); - expect(execSyncSpy).toHaveBeenCalledWith( - `npm pkg set "exports[./my-component][import]"="./dist/components/my-component.js"`, - ); - expect(execSyncSpy).toHaveBeenCalledWith( - `npm pkg set "exports[./my-component][types]"="./dist/components/my-component.d.ts"`, - ); - expect(execSyncSpy).toHaveBeenCalledWith( - `npm pkg set "exports[./my-other-component][import]"="./dist/components/my-other-component.js"`, - ); - expect(execSyncSpy).toHaveBeenCalledWith( - `npm pkg set "exports[./my-other-component][types]"="./dist/components/my-other-component.d.ts"`, - ); - }); -}); diff --git a/src/compiler/build/watch-build.ts b/src/compiler/build/watch-build.ts deleted file mode 100644 index 3e5d9ee3b27..00000000000 --- a/src/compiler/build/watch-build.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { isString, resolve } from '@utils'; -import { dirname } from 'path'; -import type ts from 'typescript'; - -import type * as d from '../../declarations'; -import { compilerRequest } from '../bundle/dev-module'; -import { - filesChanged, - hasHtmlChanges, - hasScriptChanges, - hasStyleChanges, - isWatchIgnorePath, - scriptsAdded, - scriptsDeleted, -} from '../fs-watch/fs-watch-rebuild'; -import { hasServiceWorkerChanges } from '../service-worker/generate-sw'; -import { createTsWatchProgram } from '../transpile/create-watch-program'; -import { build } from './build'; -import { BuildContext } from './build-ctx'; - -/** - * This method contains context and functionality for a TS watch build. This is called via - * the compiler when running a build in watch mode (i.e. `stencil build --watch`). - * - * In essence, this method tracks all files that change while the program is running to trigger - * a rebuild of a Stencil project using a {@link ts.EmitAndSemanticDiagnosticsBuilderProgram}. - * - * @param config The validated config for the Stencil project - * @param compilerCtx The compiler context for the project - * @returns An object containing helper methods for the dev-server's watch program - */ -export const createWatchBuild = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, -): Promise => { - let isRebuild = false; - let tsWatchProgram: { - program: ts.WatchOfConfigFile; - rebuild: () => void; - }; - let closeResolver: Function; - const watchWaiter = new Promise((resolve) => (closeResolver = resolve)); - - const dirsAdded = new Set(); - const dirsDeleted = new Set(); - const filesAdded = new Set(); - const filesUpdated = new Set(); - const filesDeleted = new Set(); - - /** - * A callback function that is invoked to trigger a rebuild of a Stencil project. This will - * update the build context with the associated file changes (these are used downstream to trigger - * HMR) and then calls the `build()` function to execute the Stencil build. - * - * @param tsBuilder A {@link ts.BuilderProgram} to be passed to the `build()` function. - */ - const onBuild = async (tsBuilder: ts.BuilderProgram) => { - const buildCtx = new BuildContext(config, compilerCtx); - buildCtx.isRebuild = isRebuild; - buildCtx.requiresFullBuild = !isRebuild; - buildCtx.dirsAdded = Array.from(dirsAdded.keys()).sort(); - buildCtx.dirsDeleted = Array.from(dirsDeleted.keys()).sort(); - buildCtx.filesAdded = Array.from(filesAdded.keys()).sort(); - buildCtx.filesUpdated = Array.from(filesUpdated.keys()).sort(); - buildCtx.filesDeleted = Array.from(filesDeleted.keys()).sort(); - buildCtx.filesChanged = filesChanged(buildCtx); - buildCtx.scriptsAdded = scriptsAdded(buildCtx); - buildCtx.scriptsDeleted = scriptsDeleted(buildCtx); - buildCtx.hasScriptChanges = hasScriptChanges(buildCtx); - buildCtx.hasStyleChanges = hasStyleChanges(buildCtx); - buildCtx.hasHtmlChanges = hasHtmlChanges(config, buildCtx); - buildCtx.hasServiceWorkerChanges = hasServiceWorkerChanges(config, buildCtx); - - if (config.flags.debug) { - config.logger.debug(`WATCH_BUILD::watchBuild::onBuild filesAdded: ${formatFilesForDebug(buildCtx.filesAdded)}`); - config.logger.debug( - `WATCH_BUILD::watchBuild::onBuild filesDeleted: ${formatFilesForDebug(buildCtx.filesDeleted)}`, - ); - config.logger.debug( - `WATCH_BUILD::watchBuild::onBuild filesUpdated: ${formatFilesForDebug(buildCtx.filesUpdated)}`, - ); - config.logger.debug( - `WATCH_BUILD::watchBuild::onBuild filesWritten: ${formatFilesForDebug(buildCtx.filesWritten)}`, - ); - } - - // Make sure all files in the module map are still in the fs - // Otherwise, we can run into build errors because the compiler can think - // there are two component files with the same tag name - Array.from(compilerCtx.moduleMap.keys()).forEach((key) => { - if (filesUpdated.has(key) || filesDeleted.has(key)) { - // Check if the file exists in the fs - const fileExists = compilerCtx.fs.accessSync(key); - if (!fileExists) { - compilerCtx.moduleMap.delete(key); - } - } - }); - - // Make sure all added/updated files are watched - // We need to check both added/updates since the TS watch program behaves kinda weird - // and doesn't always handle file renames the same way - new Set([...filesUpdated, ...filesAdded]).forEach((filePath) => { - compilerCtx.addWatchFile(filePath); - }); - - dirsAdded.clear(); - dirsDeleted.clear(); - filesAdded.clear(); - filesUpdated.clear(); - filesDeleted.clear(); - - emitFsChange(compilerCtx, buildCtx); - - buildCtx.start(); - - // Rebuild the project - const result = await build(config, compilerCtx, buildCtx, tsBuilder); - - if (result && !result.hasError) { - isRebuild = true; - } - }; - - /** - * Utility method for formatting a debug message that must either list a number of files, or the word 'none' if the - * provided list is empty - * - * @param files a list of files, the list may be empty - * @returns the provided list if it is not empty. otherwise, return the word 'none' - */ - const formatFilesForDebug = (files: ReadonlyArray): string => { - /** - * In the created message, it's important that there's no whitespace prior to the file name. - * Stencil's logger will split messages by whitespace according to the width of the terminal window. - * Since file names can be fully qualified paths (and therefore quite long), putting whitespace between a '-' and - * the path can lead to formatted messages where the '-' is on its own line - */ - return files.length > 0 ? files.map((filename: string) => `-${filename}`).join('\n') : 'none'; - }; - - /** - * Utility method to start/construct the watch program. This will mark - * all relevant files to be watched and then call a method to build the TS - * program responsible for building the project. - * - * @returns A promise of the result of creating the watch program. - */ - const start = async () => { - /** - * Stencil watches the following directories for changes: - */ - await Promise.all([ - /** - * the `srcDir` directory, e.g. component files - */ - watchFiles(compilerCtx, config.srcDir), - /** - * the root directory, e.g. `stencil.config.ts` - */ - watchFiles(compilerCtx, config.rootDir, { - recursive: false, - }), - /** - * the external directories, defined in `watchExternalDirs`, e.g. `node_modules` - */ - ...(config.watchExternalDirs || []).map((dir) => watchFiles(compilerCtx, dir)), - ]); - - tsWatchProgram = await createTsWatchProgram(config, onBuild); - return watchWaiter; - }; - - /** - * A map of absolute directory paths and their associated {@link d.CompilerFileWatcher} (which contains - * the ability to teardown the watcher for the specific directory) - */ - const watchingDirs = new Map(); - /** - * A map of absolute file paths and their associated {@link d.CompilerFileWatcher} (which contains - * the ability to teardown the watcher for the specific file) - */ - const watchingFiles = new Map(); - - /** - * Callback method that will execute whenever TS alerts us that a file change - * has occurred. This will update the appropriate set with the file path based on the - * type of change, and then will kick off a rebuild of the project. - * - * @param filePath The absolute path to the file in the Stencil project - * @param eventKind The type of file change that occurred (update, add, delete) - */ - const onFsChange: d.CompilerFileWatcherCallback = (filePath, eventKind) => { - if (tsWatchProgram && !isWatchIgnorePath(config, filePath)) { - updateCompilerCtxCache(config, compilerCtx, filePath, eventKind); - - switch (eventKind) { - case 'dirAdd': - dirsAdded.add(filePath); - break; - case 'dirDelete': - dirsDeleted.add(filePath); - break; - case 'fileAdd': - filesAdded.add(filePath); - break; - case 'fileUpdate': - filesUpdated.add(filePath); - break; - case 'fileDelete': - filesDeleted.add(filePath); - break; - } - - config.logger.debug( - `WATCH_BUILD::fs_event_change - type=${eventKind}, path=${filePath}, time=${new Date().getTime()}`, - ); - - // Trigger a rebuild of the project - tsWatchProgram.rebuild(); - } - }; - - /** - * Callback method that will execute when TS alerts us that a directory modification has occurred. - * This will just call the `onFsChange()` callback method with the same arguments. - * - * @param filePath The absolute path to the file in the Stencil project - * @param eventKind The type of file change that occurred (update, add, delete) - */ - const onDirChange: d.CompilerFileWatcherCallback = (filePath, eventKind) => { - if (eventKind != null) { - onFsChange(filePath, eventKind); - } - }; - - /** - * Utility method to teardown the TS watch program and close/clear all watched files. - * - * @returns An object with the `exitCode` status of the teardown. - */ - const close = async () => { - watchingDirs.forEach((w) => w.close()); - watchingFiles.forEach((w) => w.close()); - watchingDirs.clear(); - watchingFiles.clear(); - - if (tsWatchProgram) { - tsWatchProgram.program.close(); - tsWatchProgram = null; - } - - const watcherCloseResults: d.WatcherCloseResults = { - exitCode: 0, - }; - closeResolver(watcherCloseResults); - return watcherCloseResults; - }; - - const request = async (data: d.CompilerRequest) => compilerRequest(config, compilerCtx, data); - - // Add a definition to the `compilerCtx` for `addWatchFile` - // This method will add the specified file path to the watched files collection and instruct - // the `CompilerSystem` what to do when a file change occurs (the `onFsChange()` callback) - compilerCtx.addWatchFile = (filePath) => { - if (isString(filePath) && !watchingFiles.has(filePath) && !isWatchIgnorePath(config, filePath)) { - watchingFiles.set(filePath, config.sys.watchFile(filePath, onFsChange)); - } - }; - - // Add a definition to the `compilerCtx` for `addWatchDir` - // This method will add the specified file path to the watched directories collection and instruct - // the `CompilerSystem` what to do when a directory change occurs (the `onDirChange()` callback) - compilerCtx.addWatchDir = (dirPath, recursive) => { - if (isString(dirPath) && !watchingDirs.has(dirPath) && !isWatchIgnorePath(config, dirPath)) { - watchingDirs.set(dirPath, config.sys.watchDirectory(dirPath, onDirChange, recursive)); - } - }; - - // When the compiler system destroys, we need to also destroy this watch program - config.sys.addDestroy(close); - - return { - start, - close, - on: compilerCtx.events.on, - request, - }; -}; - -/** - * A list of directories that are excluded from being watched for changes. - */ -const EXCLUDE_DIRS = ['.cache', '.git', '.github', '.stencil', '.vscode', 'node_modules']; - -/** - * A list of file extensions that are excluded from being watched for changes. - */ -const EXCLUDE_EXTENSIONS = [ - '.md', - '.markdown', - '.txt', - '.spec.ts', - '.spec.tsx', - '.e2e.ts', - '.e2e.tsx', - '.gitignore', - '.editorconfig', -]; - -/** - * Marks all root files of a Stencil project to be watched for changes. Whenever - * one of these files is determined as changed (according to TS), a rebuild of the project will execute. - * - * @param compilerCtx The compiler context for the Stencil project - * @param dir The directory to watch for changes - * @param options The options to watch files in the directory - * @param options.recursive Whether to watch files recursively - * @param options.excludeDirNames A list of directories to exclude from being watched - * @param options.excludeExtensions A list of file extensions to exclude from being watched for changes - */ -const watchFiles = async ( - compilerCtx: d.CompilerCtx, - dir: string, - options?: { - recursive?: boolean; - excludeDirNames?: string[]; - excludeExtensions?: string[]; - }, -) => { - const recursive = options?.recursive ?? true; - const excludeDirNames = options?.excludeDirNames ?? EXCLUDE_DIRS; - const excludeExtensions = options?.excludeExtensions ?? EXCLUDE_EXTENSIONS; - - /** - * non-src files that cause a rebuild - * mainly for root level config files, and getting an event when they change - */ - const rootFiles = await compilerCtx.fs.readdir(dir, { - recursive, - excludeDirNames, - excludeExtensions, - }); - - /** - * If the directory is watched recursively, we need to watch the directory itself. - */ - if (recursive) { - compilerCtx.addWatchDir(dir, true); - } - - /** - * Iterate over each file in the collection (filter out directories) and add - * a watcher for each - */ - rootFiles.filter(({ isFile }) => isFile).forEach(({ absPath }) => compilerCtx.addWatchFile(absPath)); -}; - -const emitFsChange = (compilerCtx: d.CompilerCtx, buildCtx: BuildContext) => { - if ( - buildCtx.dirsAdded.length > 0 || - buildCtx.dirsDeleted.length > 0 || - buildCtx.filesUpdated.length > 0 || - buildCtx.filesAdded.length > 0 || - buildCtx.filesDeleted.length > 0 - ) { - compilerCtx.events.emit('fsChange', { - dirsAdded: buildCtx.dirsAdded.slice(), - dirsDeleted: buildCtx.dirsDeleted.slice(), - filesUpdated: buildCtx.filesUpdated.slice(), - filesAdded: buildCtx.filesAdded.slice(), - filesDeleted: buildCtx.filesDeleted.slice(), - }); - } -}; - -const updateCompilerCtxCache = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - path: string, - kind: d.CompilerFileWatcherEvent, -) => { - compilerCtx.fs.clearFileCache(path); - compilerCtx.changedFiles.add(path); - - if (kind === 'fileDelete') { - compilerCtx.moduleMap.delete(path); - } else if (kind === 'dirDelete') { - const fsRootDir = resolve('/'); - compilerCtx.moduleMap.forEach((_, moduleFilePath) => { - let moduleAncestorDir = dirname(moduleFilePath); - - for (let i = 0; i < 50; i++) { - if (moduleAncestorDir === config.rootDir || moduleAncestorDir === fsRootDir) { - break; - } - - if (moduleAncestorDir === path) { - compilerCtx.fs.clearFileCache(moduleFilePath); - compilerCtx.moduleMap.delete(moduleFilePath); - compilerCtx.changedFiles.add(moduleFilePath); - break; - } - - moduleAncestorDir = dirname(moduleAncestorDir); - } - }); - } -}; diff --git a/src/compiler/build/write-export-maps.ts b/src/compiler/build/write-export-maps.ts deleted file mode 100644 index ab8588a3844..00000000000 --- a/src/compiler/build/write-export-maps.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - isEligiblePrimaryPackageOutputTarget, - isOutputTargetDistCustomElements, - isOutputTargetDistLazyLoader, -} from '@utils'; -import { relative } from '@utils'; -import { execSync } from 'child_process'; - -import * as d from '../../declarations'; -import { PRIMARY_PACKAGE_TARGET_CONFIGS } from '../types/validate-primary-package-output-target'; - -/** - * Create export map entry point definitions for the `package.json` file using the npm CLI. - * This will generate a root entry point for the package, as well as entry points for each component and - * the lazy loader (if applicable). - * - * @param config The validated Stencil config - * @param buildCtx The build context containing the components to generate export maps for - */ -export const writeExportMaps = (config: d.ValidatedConfig, buildCtx: d.BuildCtx) => { - const eligiblePrimaryTargets = config.outputTargets.filter(isEligiblePrimaryPackageOutputTarget); - if (eligiblePrimaryTargets.length > 0) { - const primaryTarget = - eligiblePrimaryTargets.find((o) => o.isPrimaryPackageOutputTarget) ?? eligiblePrimaryTargets[0]; - const outputTargetConfig = PRIMARY_PACKAGE_TARGET_CONFIGS[primaryTarget.type]; - - if (outputTargetConfig.getModulePath) { - const importPath = outputTargetConfig.getModulePath(config.rootDir, primaryTarget.dir!); - - if (importPath) { - execSync(`npm pkg set "exports[.][import]"="${importPath}"`); - } - } - - if (outputTargetConfig.getMainPath) { - const requirePath = outputTargetConfig.getMainPath(config.rootDir, primaryTarget.dir!); - - if (requirePath) { - execSync(`npm pkg set "exports[.][require]"="${requirePath}"`); - } - } - - if (outputTargetConfig.getTypesPath) { - const typesPath = outputTargetConfig.getTypesPath(config.rootDir, primaryTarget); - - if (typesPath) { - execSync(`npm pkg set "exports[.][types]"="${typesPath}"`); - } - } - } - - const distLazyLoader = config.outputTargets.find(isOutputTargetDistLazyLoader); - if (distLazyLoader != null) { - // Calculate relative path from project root to lazy-loader output directory - let outDir = relative(config.rootDir, distLazyLoader.dir); - if (!outDir.startsWith('.')) { - outDir = './' + outDir; - } - - execSync(`npm pkg set "exports[./loader][import]"="${outDir}/index.js"`); - execSync(`npm pkg set "exports[./loader][require]"="${outDir}/index.cjs"`); - execSync(`npm pkg set "exports[./loader][types]"="${outDir}/index.d.ts"`); - } - - const distCustomElements = config.outputTargets.find(isOutputTargetDistCustomElements); - if (distCustomElements != null) { - // Calculate relative path from project root to custom elements output directory - let outDir = relative(config.rootDir, distCustomElements.dir!); - if (!outDir.startsWith('.')) { - outDir = './' + outDir; - } - - buildCtx.components.forEach((cmp) => { - execSync(`npm pkg set "exports[./${cmp.tagName}][import]"="${outDir}/${cmp.tagName}.js"`); - - if (distCustomElements.generateTypeDeclarations) { - execSync(`npm pkg set "exports[./${cmp.tagName}][types]"="${outDir}/${cmp.tagName}.d.ts"`); - } - }); - } -}; diff --git a/src/compiler/bundle/app-data-plugin.ts b/src/compiler/bundle/app-data-plugin.ts deleted file mode 100644 index 5ec4dd1779f..00000000000 --- a/src/compiler/bundle/app-data-plugin.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { createJsVarName, isString, loadTypeScriptDiagnostics, normalizePath } from '@utils'; -import MagicString from 'magic-string'; -import { basename } from 'path'; -import type { LoadResult, Plugin, ResolveIdResult, TransformResult } from 'rollup'; -import ts from 'typescript'; - -import type * as d from '../../declarations'; -import type { BundlePlatform } from './bundle-interface'; -import { removeCollectionImports } from '../transformers/remove-collection-imports'; -import { APP_DATA_CONDITIONAL, STENCIL_APP_DATA_ID, STENCIL_APP_GLOBALS_ID } from './entry-alias-ids'; - -/** - * A Rollup plugin which bundles application data. - * - * @param config the Stencil configuration for a particular project - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param buildConditionals the set build conditionals for the build - * @param platform the platform that is being built - * @returns a Rollup plugin which carries out the necessary work - */ -export const appDataPlugin = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - buildConditionals: d.BuildConditionals, - platform: BundlePlatform, -): Plugin => { - if (!platform) { - return { - name: 'appDataPlugin', - }; - } - const globalScripts = getGlobalScriptData(config, compilerCtx); - - return { - name: 'appDataPlugin', - - resolveId(id: string, importer: string | undefined): ResolveIdResult { - if (id === STENCIL_APP_DATA_ID || id === STENCIL_APP_GLOBALS_ID) { - if (platform === 'worker') { - this.error('@stencil/core packages cannot be imported from a worker.'); - } - - if (platform === 'hydrate' || STENCIL_APP_GLOBALS_ID) { - // hydrate will always bundle app-data and runtime - // and the load() fn will build a custom globals import - return id; - } else if (platform === 'client' && importer && importer.endsWith(APP_DATA_CONDITIONAL)) { - // since the importer ends with ?app-data=conditional we know that - // we need to build custom app-data based off of component metadata - // return the same "id" so that the "load()" method knows to - // build custom app-data - return id; - } - // for a client build that does not have ?app-data=conditional at the end then we - // do not want to create custom app-data, but should use the default - } - return null; - }, - - async load(id: string): Promise { - if (id === STENCIL_APP_GLOBALS_ID) { - const s = new MagicString(``); - appendGlobalScripts(globalScripts, s); - await appendGlobalStyles(buildCtx, s, platform); - return s.toString(); - } - if (id === STENCIL_APP_DATA_ID) { - // build custom app-data based off of component metadata - const s = new MagicString(``); - appendNamespace(config, s); - appendBuildConditionals(config, buildConditionals, s); - appendEnv(config, s); - return s.toString(); - } - if (id !== config.globalScript) { - return null; - } - - const module = compilerCtx.moduleMap.get(config.globalScript); - if (!module) { - return null; - } else if (!module.sourceMapFileText) { - return { - code: module.staticSourceFileText, - map: null, - }; - } - - const sourceMap: d.SourceMap = JSON.parse(module.sourceMapFileText); - sourceMap.sources = sourceMap.sources.map((src) => basename(src)); - return { code: module.staticSourceFileText, map: sourceMap }; - }, - - transform(code: string, id: string): TransformResult { - id = normalizePath(id); - if (globalScripts.some((s) => s.path === id)) { - const program = this.parse(code, {}); - const needsDefault = !(program as any).body.some((s: any) => s.type === 'ExportDefaultDeclaration'); - - if (needsDefault) { - const diagnostic: d.Diagnostic = { - level: 'warn', - type: 'build', - header: 'Missing default export in globalScript', - messageText: `globalScript should export a default function.\nSee: https://stenciljs.com/docs/config#globalscript`, - relFilePath: id, - lines: [], - }; - buildCtx.diagnostics.push(diagnostic); - } - - const defaultExport = needsDefault ? '\nexport const globalFn = () => {};\nexport default globalFn;' : ''; - code = code + defaultExport; - - const compilerOptions: ts.CompilerOptions = { ...config.tsCompilerOptions }; - compilerOptions.module = ts.ModuleKind.ESNext; - - const results = ts.transpileModule(code, { - compilerOptions, - fileName: id, - transformers: { - after: [removeCollectionImports(compilerCtx)], - }, - }); - buildCtx.diagnostics.push(...loadTypeScriptDiagnostics(results.diagnostics)); - - if (config.sourceMap) { - // generate the sourcemap for global script - const codeMs = new MagicString(code); - const codeMap = codeMs.generateMap({ - source: id, - // this is the name of the sourcemap, not to be confused with the `file` field in a generated sourcemap - file: id + '.map', - includeContent: true, - hires: true, - }); - - return { - code: results.outputText, - map: { - ...codeMap, - // MagicString changed their types in this PR: https://github.com/Rich-Harris/magic-string/pull/235 - // so that their `sourcesContent` is of type `(string | null)[]`. But, it will only return `[null]` if - // `includeContent` is set to `false`. Since we explicitly set `includeContent: true`, we can override - // the type to satisfy Rollup's type expectation - sourcesContent: codeMap.sourcesContent as string[], - }, - }; - } - - return { code: results.outputText }; - } - return null; - }, - }; -}; - -export const getGlobalScriptData = (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx) => { - const globalScripts: GlobalScript[] = []; - - if (isString(config.globalScript)) { - const mod = compilerCtx.moduleMap.get(config.globalScript); - const globalScript = compilerCtx.version === 2 ? config.globalScript : mod && mod.jsFilePath; - - if (globalScript) { - globalScripts.push({ - defaultName: createJsVarName(config.namespace + 'GlobalScript'), - path: normalizePath(globalScript), - }); - } - } - - compilerCtx.collections.forEach((collection) => { - if (collection.global != null && isString(collection.global.sourceFilePath)) { - let defaultName = createJsVarName(collection.collectionName + 'GlobalScript'); - if (globalScripts.some((s) => s.defaultName === defaultName)) { - defaultName += globalScripts.length; - } - globalScripts.push({ - defaultName, - path: normalizePath(collection.global.sourceFilePath), - }); - } - }); - - return globalScripts; -}; - -const appendGlobalScripts = (globalScripts: GlobalScript[], s: MagicString) => { - if (globalScripts.length === 1) { - s.prepend(`import * as appGlobalScriptNs from '${globalScripts[0].path}';\n`); - s.prepend(`const appGlobalScript = appGlobalScriptNs.default || (() => {});\n`); - s.append(`export const globalScripts = appGlobalScript;\n`); - } else if (globalScripts.length > 1) { - globalScripts.forEach((globalScript) => { - s.prepend(`import * as ${globalScript.defaultName}Ns from '${globalScript.path}';\n`); - s.prepend(`const ${globalScript.defaultName} = ${globalScript.defaultName}Ns.default || (() => {});\n`); - }); - - s.append(`export const globalScripts = () => {\n`); - s.append(` return Promise.all([\n`); - globalScripts.forEach((globalScript) => { - s.append(` ${globalScript.defaultName}(),\n`); - }); - s.append(` ]);\n`); - s.append(`};\n`); - } else { - s.append(`export const globalScripts = () => {};\n`); - } -}; - -/** - * Appends the global styles to the MagicString. - * - * @param buildCtx the build context - * @param s the MagicString to append the global styles onto - * @param platform the platform that is being built - */ -const appendGlobalStyles = async (buildCtx: d.BuildCtx, s: MagicString, platform: BundlePlatform) => { - const { addGlobalStyleToComponents } = buildCtx.config.extras; - const shouldIncludeGlobalStyles = - addGlobalStyleToComponents === true || (addGlobalStyleToComponents === 'client' && platform === 'client'); - const globalStyles = buildCtx.config.globalStyle && shouldIncludeGlobalStyles ? await buildCtx.stylesPromise : ''; - s.append(`export const globalStyles = ${JSON.stringify(globalStyles)};\n`); -}; - -/** - * Generates the `BUILD` constant that is used at compile-time in a Stencil project - * - * **This function mutates the provided {@link MagicString} argument** - * - * @param config the configuration associated with the Stencil project - * @param buildConditionals the build conditionals to serialize into a JS object - * @param s a `MagicString` to append the generated constant onto - */ -export const appendBuildConditionals = ( - config: d.ValidatedConfig, - buildConditionals: d.BuildConditionals, - s: MagicString, -): void => { - const buildData = Object.keys(buildConditionals) - .sort() - .map((key) => key + ': ' + JSON.stringify((buildConditionals as any)[key])) - .join(', '); - - s.append(`export const BUILD = /* ${config.fsNamespace} */ { ${buildData} };\n`); -}; - -const appendEnv = (config: d.ValidatedConfig, s: MagicString) => { - s.append(`export const Env = /* ${config.fsNamespace} */ ${JSON.stringify(config.env)};\n`); -}; - -const appendNamespace = (config: d.ValidatedConfig, s: MagicString) => { - s.append(`export const NAMESPACE = '${config.fsNamespace}';\n`); -}; - -interface GlobalScript { - defaultName: string; - path: string; -} diff --git a/src/compiler/bundle/bundle-interface.ts b/src/compiler/bundle/bundle-interface.ts deleted file mode 100644 index adeb0b97dad..00000000000 --- a/src/compiler/bundle/bundle-interface.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { PreserveEntrySignaturesOption } from 'rollup'; -import type { SourceFile, TransformerFactory } from 'typescript'; - -import type { BuildConditionals } from '../../declarations'; - -/** - * Options for bundled output passed on Rollup - * - * This covers the ID for the bundle, the platform it runs on, input modules, - * and more - */ -export interface BundleOptions { - id: string; - conditionals?: BuildConditionals; - /** - * When `true`, all `@stencil/core/*` packages will be treated as external - * and omitted from the generated bundle. - */ - externalRuntime?: boolean; - platform: BundlePlatform; - /** - * A collection of TypeScript transformation factories to apply during the "before" stage of the TypeScript - * compilation pipeline (before built-in .js transformations) - */ - customBeforeTransformers?: TransformerFactory[]; - /** - * This is equivalent to the Rollup `input` configuration option. It's - * an object mapping names to entry points which tells Rollup to bundle - * each thing up as a separate output chunk. - * - * @see {@link https://rollupjs.org/guide/en/#input} - */ - inputs: { [entryKey: string]: string }; - /** - * A map of strings which are passed to the Stencil-specific loader plugin - * which we use to resolve the imports of Stencil project files when building - * with Rollup. - * - * @see {@link loader-plugin:loaderPlugin} - */ - loader?: { [id: string]: string }; - /** - * Duplicate of Rollup's `inlineDynamicImports` output option. - * - * Creates dynamic imports (i.e. `import()` calls) as a part of the same - * chunk being bundled. Rather than being created as separate chunks. - * - * @see {@link https://rollupjs.org/guide/en/#outputinlinedynamicimports} - */ - inlineDynamicImports?: boolean; - inlineWorkers?: boolean; - /** - * Duplicate of Rollup's `preserveEntrySignatures` option. - * - * "Controls if Rollup tries to ensure that entry chunks have the same - * exports as the underlying entry module." - * - * @see {@link https://rollupjs.org/guide/en/#preserveentrysignatures} - */ - preserveEntrySignatures?: PreserveEntrySignaturesOption; -} - -export type BundlePlatform = 'client' | 'hydrate' | 'worker'; diff --git a/src/compiler/bundle/bundle-output.ts b/src/compiler/bundle/bundle-output.ts deleted file mode 100644 index 7686975f0c8..00000000000 --- a/src/compiler/bundle/bundle-output.ts +++ /dev/null @@ -1,211 +0,0 @@ -import rollupCommonjsPlugin from '@rollup/plugin-commonjs'; -import rollupJsonPlugin from '@rollup/plugin-json'; -import rollupNodeResolvePlugin from '@rollup/plugin-node-resolve'; -import rollupReplacePlugin from '@rollup/plugin-replace'; -import { createOnWarnFn, isString, loadRollupDiagnostics } from '@utils'; -import { type ObjectHook, PluginContext, rollup, RollupOptions, TreeshakingOptions } from 'rollup'; - -import type * as d from '../../declarations'; -import { lazyComponentPlugin } from '../output-targets/dist-lazy/lazy-component-plugin'; -import { appDataPlugin } from './app-data-plugin'; -import type { BundleOptions } from './bundle-interface'; -import { coreResolvePlugin } from './core-resolve-plugin'; -import { devNodeModuleResolveId } from './dev-node-module-resolve'; -import { extFormatPlugin } from './ext-format-plugin'; -import { extTransformsPlugin } from './ext-transforms-plugin'; -import { fileLoadPlugin } from './file-load-plugin'; -import { loaderPlugin } from './loader-plugin'; -import { pluginHelper } from './plugin-helper'; -import { serverPlugin } from './server-plugin'; -import { resolveIdWithTypeScript, typescriptPlugin } from './typescript-plugin'; -import { userIndexPlugin } from './user-index-plugin'; -import { workerPlugin } from './worker-plugin'; - -export const bundleOutput = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - bundleOpts: BundleOptions, -) => { - try { - const rollupOptions = getRollupOptions(config, compilerCtx, buildCtx, bundleOpts); - const rollupBuild = await rollup(rollupOptions); - - compilerCtx.rollupCache.set(bundleOpts.id, rollupBuild.cache); - return rollupBuild; - } catch (e: any) { - if (!buildCtx.hasError) { - // TODO(STENCIL-353): Implement a type guard that balances using our own copy of Rollup types (which are - // breakable) and type safety (so that the error variable may be something other than `any`) - loadRollupDiagnostics(config, compilerCtx, buildCtx, e); - } - } - return undefined; -}; - -/** - * Build the rollup options that will be used to transpile, minify, and otherwise transform a Stencil project - * @param config the Stencil configuration for the project - * @param compilerCtx the current compiler context - * @param buildCtx a context object containing information about the current build - * @param bundleOpts Rollup bundling options to apply to the base configuration setup by this function - * @returns the rollup options to be used - */ -export const getRollupOptions = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - bundleOpts: BundleOptions, -): RollupOptions => { - const nodeResolvePlugin = rollupNodeResolvePlugin({ - mainFields: ['collection:main', 'jsnext:main', 'es2017', 'es2015', 'module', 'main'], - browser: bundleOpts.platform !== 'hydrate', - rootDir: config.rootDir, - exportConditions: ['default', 'module', 'import', 'require'], - extensions: ['.tsx', '.ts', '.mts', '.cts', '.js', '.mjs', '.cjs', '.json', '.d.ts', '.d.mts', '.d.cts'], - ...config.nodeResolve, - }); - - // @ts-expect-error - this is required now. - nodeResolvePlugin.resolve = async function () { - // Investigate if we can use this to leverage Stencil's in-memory fs - }; - - // @ts-expect-error - this is required now. - nodeResolvePlugin.warn = (log) => { - const onWarn = createOnWarnFn(buildCtx.diagnostics); - if (typeof log === 'string') { - onWarn({ message: log }); - } else if (typeof log === 'function') { - const result = log(); - if (typeof result === 'string') { - onWarn({ message: result }); - } else { - onWarn(result); - } - } else { - onWarn(log); - } - }; - - assertIsObjectHook(nodeResolvePlugin.resolveId); - // remove default 'post' order - nodeResolvePlugin.resolveId.order = null; - const orgNodeResolveId = nodeResolvePlugin.resolveId.handler; - - const orgNodeResolveId2 = (nodeResolvePlugin.resolveId.handler = async function (importee: string, importer: string) { - const [realImportee, query] = importee.split('?'); - const resolved = await orgNodeResolveId.call( - nodeResolvePlugin as unknown as PluginContext, - realImportee, - importer, - { - attributes: {}, - isEntry: true, - }, - ); - if (resolved) { - if (isString(resolved)) { - return query ? resolved + '?' + query : resolved; - } - return { - ...resolved, - id: query ? resolved.id + '?' + query : resolved.id, - }; - } - return resolved; - }); - if (config.devServer?.experimentalDevModules) { - nodeResolvePlugin.resolveId = async function (importee: string, importer: string) { - const resolvedId = await orgNodeResolveId2.call( - nodeResolvePlugin as unknown as PluginContext, - importee, - importer, - ); - return devNodeModuleResolveId(config, compilerCtx.fs, resolvedId, importee); - }; - } - - const beforePlugins = config.rollupPlugins.before || []; - const afterPlugins = config.rollupPlugins.after || []; - - const rollupOptions: RollupOptions = { - input: bundleOpts.inputs, - output: { - inlineDynamicImports: bundleOpts.inlineDynamicImports ?? false, - }, - - plugins: [ - coreResolvePlugin( - config, - compilerCtx, - bundleOpts.platform, - !!bundleOpts.externalRuntime, - bundleOpts.conditionals?.lazyLoad ?? false, - ), - appDataPlugin(config, compilerCtx, buildCtx, bundleOpts.conditionals, bundleOpts.platform), - lazyComponentPlugin(buildCtx), - loaderPlugin(bundleOpts.loader), - userIndexPlugin(config, compilerCtx), - typescriptPlugin(compilerCtx, bundleOpts, config), - extFormatPlugin(config), - extTransformsPlugin(config, compilerCtx, buildCtx), - workerPlugin(config, compilerCtx, buildCtx, bundleOpts.platform, !!bundleOpts.inlineWorkers), - serverPlugin(config, bundleOpts.platform), - ...beforePlugins, - nodeResolvePlugin, - resolveIdWithTypeScript(config, compilerCtx), - rollupCommonjsPlugin({ - include: /node_modules/, - sourceMap: config.sourceMap, - transformMixedEsModules: false, - ...config.commonjs, - }), - ...afterPlugins, - pluginHelper(config, buildCtx, bundleOpts.platform), - rollupJsonPlugin({ - preferConst: true, - }), - rollupReplacePlugin({ - 'process.env.NODE_ENV': config.devMode ? '"development"' : '"production"', - preventAssignment: true, - }), - fileLoadPlugin(compilerCtx.fs), - ], - - treeshake: getTreeshakeOption(config, bundleOpts), - preserveEntrySignatures: bundleOpts.preserveEntrySignatures ?? 'strict', - - onwarn: createOnWarnFn(buildCtx.diagnostics), - - cache: compilerCtx.rollupCache.get(bundleOpts.id), - - external: config.rollupConfig.inputOptions.external, - - maxParallelFileOps: config.rollupConfig.inputOptions.maxParallelFileOps, - }; - - return rollupOptions; -}; - -const getTreeshakeOption = (config: d.ValidatedConfig, bundleOpts: BundleOptions): TreeshakingOptions | boolean => { - if (bundleOpts.platform === 'hydrate') { - return { - propertyReadSideEffects: false, - tryCatchDeoptimization: false, - }; - } - - const treeshake = - !config.devMode && config.rollupConfig.inputOptions.treeshake !== false - ? { - propertyReadSideEffects: false, - tryCatchDeoptimization: false, - } - : false; - return treeshake; -}; - -function assertIsObjectHook(hook: ObjectHook): asserts hook is { handler: T; order?: 'pre' | 'post' | null } { - if (typeof hook !== 'object') throw new Error(`expected the rollup plugin hook ${hook} to be an object`); -} diff --git a/src/compiler/bundle/core-resolve-plugin.ts b/src/compiler/bundle/core-resolve-plugin.ts deleted file mode 100644 index c8128b7596a..00000000000 --- a/src/compiler/bundle/core-resolve-plugin.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { isRemoteUrl, join, normalizeFsPath, normalizePath } from '@utils'; -import { dirname } from 'path'; -import type { Plugin } from 'rollup'; - -import type * as d from '../../declarations'; -import type { BundlePlatform } from './bundle-interface'; -import { HYDRATED_CSS } from '../../runtime/runtime-constants'; -import { fetchModuleAsync } from '../sys/fetch/fetch-module-async'; -import { getStencilModuleUrl, packageVersions } from '../sys/fetch/fetch-utils'; -import { - APP_DATA_CONDITIONAL, - STENCIL_CORE_ID, - STENCIL_INTERNAL_CLIENT_ID, - STENCIL_INTERNAL_CLIENT_PATCH_BROWSER_ID, - STENCIL_INTERNAL_HYDRATE_ID, - STENCIL_INTERNAL_ID, - STENCIL_JSX_DEV_RUNTIME_ID, - STENCIL_JSX_RUNTIME_ID, -} from './entry-alias-ids'; - -export const coreResolvePlugin = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - platform: BundlePlatform, - externalRuntime: boolean, - lazyLoad: boolean, -): Plugin => { - const compilerExe = config.sys.getCompilerExecutingPath(); - const internalClient = getStencilInternalModule(config, compilerExe, 'client/index.js'); - const internalClientPatchBrowser = getStencilInternalModule(config, compilerExe, 'client/patch-browser.js'); - const internalHydrate = getStencilInternalModule(config, compilerExe, 'hydrate/index.js'); - - return { - name: 'coreResolvePlugin', - - resolveId(id) { - if (id === STENCIL_CORE_ID || id === STENCIL_INTERNAL_ID) { - if (platform === 'client') { - if (externalRuntime) { - return { - id: STENCIL_INTERNAL_CLIENT_ID, - external: true, - }; - } - if (lazyLoad) { - // with a lazy / dist build, add `?app-data=conditional` as an identifier to ensure we don't - // use the default app-data, but build a custom one based on component meta - return internalClient + APP_DATA_CONDITIONAL; - } - // for a non-lazy / dist-custom-elements build, use the default, complete core. - // This ensures all features are available for any importer library - return internalClient; - } - if (platform === 'hydrate') { - return internalHydrate; - } - } - if (id === STENCIL_INTERNAL_CLIENT_ID) { - if (externalRuntime) { - // not bundling the client runtime and the user's component together this - // must be the custom elements build, where @stencil/core/internal/client - // is an import, rather than bundling - return { - id: STENCIL_INTERNAL_CLIENT_ID, - external: true, - }; - } - // importing @stencil/core/internal/client directly, so it shouldn't get - // the custom app-data conditionals - return internalClient; - } - if (id === STENCIL_INTERNAL_CLIENT_PATCH_BROWSER_ID) { - if (externalRuntime) { - return { - id: STENCIL_INTERNAL_CLIENT_PATCH_BROWSER_ID, - external: true, - }; - } - return internalClientPatchBrowser; - } - if (id === STENCIL_INTERNAL_HYDRATE_ID) { - return internalHydrate; - } - // Handle jsx-runtime and jsx-dev-runtime imports - // These must resolve to the same internal client path as @stencil/core - // to prevent Rollup from bundling duplicate runtime code with different - // minified property names, which causes VNode property mismatches during hydration - if (id === STENCIL_JSX_RUNTIME_ID || id === STENCIL_JSX_DEV_RUNTIME_ID) { - if (platform === 'client') { - if (externalRuntime) { - return { - id: STENCIL_INTERNAL_CLIENT_ID, - external: true, - }; - } - if (lazyLoad) { - // with a lazy / dist build, add `?app-data=conditional` as an identifier to ensure we don't - // use the default app-data, but build a custom one based on component meta - return internalClient + APP_DATA_CONDITIONAL; - } - // for a non-lazy / dist-custom-elements build, use the default, complete core. - return internalClient; - } - if (platform === 'hydrate') { - return internalHydrate; - } - } - return null; - }, - - async load(filePath) { - if (filePath && !filePath.startsWith('\0')) { - filePath = normalizeFsPath(filePath); - - if (filePath === internalClient || filePath === internalHydrate) { - if (platform === 'worker') { - return ` -export const Build = { - isDev: ${config.devMode}, - isBrowser: true, - isServer: false, - isTesting: false, -};`; - } - let code = await compilerCtx.fs.readFile(filePath); - - if (typeof code !== 'string' && isRemoteUrl(compilerExe)) { - const url = getStencilModuleUrl(compilerExe, filePath); - code = await fetchModuleAsync(config.sys, compilerCtx.fs, packageVersions, url, filePath); - } - - if (typeof code === 'string') { - const hydratedFlag = config.hydratedFlag; - if (hydratedFlag) { - const hydratedFlagHead = getHydratedFlagHead(hydratedFlag); - if (HYDRATED_CSS !== hydratedFlagHead) { - code = code.replace(HYDRATED_CSS, hydratedFlagHead); - if (hydratedFlag.name !== 'hydrated') { - code = code.replace(`.classList.add("hydrated")`, `.classList.add("${hydratedFlag.name}")`); - code = code.replace(`.classList.add('hydrated')`, `.classList.add('${hydratedFlag.name}')`); - code = code.replace(`.setAttribute("hydrated",`, `.setAttribute("${hydratedFlag.name}",`); - code = code.replace(`.setAttribute('hydrated',`, `.setAttribute('${hydratedFlag.name}',`); - } - } - } else { - code = code.replace(HYDRATED_CSS, '{}'); - } - } - - return code; - } - } - return null; - }, - }; -}; - -export const getStencilInternalModule = (config: d.ValidatedConfig, compilerExe: string, internalModule: string) => { - if (isRemoteUrl(compilerExe)) { - return normalizePath( - config.sys.getLocalModulePath({ - rootDir: config.rootDir, - moduleId: '@stencil/core', - path: 'internal/' + internalModule, - }), - ); - } - - const compilerExeDir = dirname(compilerExe); - return normalizePath(join(compilerExeDir, '..', 'internal', internalModule)); -}; - -export const getHydratedFlagHead = (h: d.HydratedFlag) => { - // {visibility:hidden}.hydrated{visibility:inherit} - - let initial: string; - let hydrated: string; - - if (!String(h.initialValue) || h.initialValue === '' || h.initialValue == null) { - initial = ''; - } else { - initial = `{${h.property}:${h.initialValue}}`; - } - - const selector = h.selector === 'attribute' ? `[${h.name}]` : `.${h.name}`; - - if (!String(h.hydratedValue) || h.hydratedValue === '' || h.hydratedValue == null) { - hydrated = ''; - } else { - hydrated = `${selector}{${h.property}:${h.hydratedValue}}`; - } - - return initial + hydrated; -}; diff --git a/src/compiler/bundle/entry-alias-ids.ts b/src/compiler/bundle/entry-alias-ids.ts deleted file mode 100644 index 69474dfbb8f..00000000000 --- a/src/compiler/bundle/entry-alias-ids.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const STENCIL_CORE_ID = '@stencil/core'; -export const STENCIL_INTERNAL_ID = '@stencil/core/internal'; -export const STENCIL_APP_DATA_ID = '@stencil/core/internal/app-data'; -export const STENCIL_APP_GLOBALS_ID = '@stencil/core/internal/app-globals'; -export const STENCIL_HYDRATE_FACTORY_ID = '@stencil/core/hydrate-factory'; -export const STENCIL_INTERNAL_CLIENT_ID = '@stencil/core/internal/client'; -export const STENCIL_INTERNAL_CLIENT_PATCH_BROWSER_ID = '@stencil/core/internal/client/patch-browser'; -export const STENCIL_INTERNAL_HYDRATE_ID = '@stencil/core/internal/hydrate'; -export const STENCIL_MOCK_DOC_ID = '@stencil/core/mock-doc'; -export const STENCIL_JSX_RUNTIME_ID = '@stencil/core/jsx-runtime'; -export const STENCIL_JSX_DEV_RUNTIME_ID = '@stencil/core/jsx-dev-runtime'; -export const APP_DATA_CONDITIONAL = '?app-data=conditional'; -export const LAZY_BROWSER_ENTRY_ID = '@lazy-browser-entrypoint' + APP_DATA_CONDITIONAL; -export const LAZY_EXTERNAL_ENTRY_ID = '@lazy-external-entrypoint' + APP_DATA_CONDITIONAL; -export const USER_INDEX_ENTRY_ID = '@user-index-entrypoint'; diff --git a/src/compiler/bundle/ext-transforms-plugin.ts b/src/compiler/bundle/ext-transforms-plugin.ts deleted file mode 100644 index f48ff60133f..00000000000 --- a/src/compiler/bundle/ext-transforms-plugin.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { hasError, isOutputTargetDistCollection, join, mergeIntoWith, normalizeFsPath, relative } from '@utils'; -import type { Plugin } from 'rollup'; - -import type * as d from '../../declarations'; -import { runPluginTransformsEsmImports } from '../plugin/plugin'; -import { getScopeId } from '../style/scope-css'; -import { parseImportPath } from '../transformers/stencil-import-path'; - -/** - * This keeps a map of all the component styles we've seen already so we can create - * a correct state of all styles when we're doing a rebuild. This map helps by - * storing the state of all styles as follows, e.g.: - * - * ``` - * { - * 'cmp-a-$': { - * '/path/to/project/cmp-a.scss': 'button{color:red}', - * '/path/to/project/cmp-a.md.scss': 'button{color:blue}' - * } - * ``` - * - * Whenever one of the files change, we can propagate a correct concatenated - * version of all styles to the browser by setting `buildCtx.stylesUpdated`. - */ -type ComponentStyleMap = Map; -const allCmpStyles = new Map(); - -/** - * A Rollup plugin which bundles up some transformation of CSS imports as well - * as writing some files to disk for the `DIST_COLLECTION` output target. - * - * @param config a user-supplied configuration - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @returns a Rollup plugin which carries out the necessary work - */ -export const extTransformsPlugin = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -): Plugin => { - return { - name: 'extTransformsPlugin', - - /** - * A custom function targeting the `transform` build hook in Rollup. See here for details: - * https://rollupjs.org/guide/en/#transform - * - * Here we are ignoring the first argument (which contains the module's source code) and - * only looking at the `id` argument. We use that `id` to get information about the module - * in question from disk ourselves so that we can then do some transformations on it. - * - * @param _ an unused parameter (normally the code for a given module) - * @param id the id of a module - * @returns metadata for Rollup or null if no transformation should be done - */ - async transform(_, id) { - if (/\0/.test(id)) { - return null; - } - - /** - * Make sure compiler context has a registered worker. The interface suggests that it - * potentially can be undefined, therefore check for it here. - */ - if (!compilerCtx.worker) { - return null; - } - - // The `id` here was possibly previously updated using - // `serializeImportPath` to annotate the filepath with various metadata - // serialized to query-params. If that was done for this particular `id` - // then the `data` prop will not be null. - const { data } = parseImportPath(id); - - if (data != null) { - let cmpStyles: ComponentStyleMap | undefined = undefined; - let cmp: d.ComponentCompilerMeta | undefined = undefined; - const filePath = normalizeFsPath(id); - const code = await compilerCtx.fs.readFile(filePath); - if (typeof code !== 'string') { - return null; - } - - /** - * add file to watch list if it is outside of the `srcDir` config path - */ - if (config.watch && (id.startsWith('/') || id.startsWith('.')) && !id.startsWith(config.srcDir)) { - compilerCtx.addWatchFile(id.split('?')[0]); - } - - const pluginTransforms = await runPluginTransformsEsmImports(config, compilerCtx, buildCtx, code, filePath); - - if (data.tag) { - cmp = buildCtx.components.find((c) => c.tagName === data.tag); - const moduleFile = cmp && !cmp.isCollectionDependency && compilerCtx.moduleMap.get(cmp.sourceFilePath); - - if (moduleFile) { - const collectionDirs = config.outputTargets.filter(isOutputTargetDistCollection); - const relPath = relative(config.srcDir, pluginTransforms.id); - - // If we found a `moduleFile` in the module map above then we - // should write the transformed CSS file (found in the return value - // of `runPluginTransformsEsmImports`, above) to disk. - await Promise.all( - collectionDirs.map(async (outputTarget) => { - const collectionPath = join(outputTarget.collectionDir, relPath); - await compilerCtx.fs.writeFile(collectionPath, pluginTransforms.code); - }), - ); - } - - /** - * initiate map for component styles - */ - const scopeId = getScopeId(data.tag, data.mode); - if (!allCmpStyles.has(scopeId)) { - allCmpStyles.set(scopeId, new Map()); - } - cmpStyles = allCmpStyles.get(scopeId); - } - - const cssTransformResults = await compilerCtx.worker.transformCssToEsm({ - file: pluginTransforms.id, - input: pluginTransforms.code, - tag: data.tag, - tags: buildCtx.components.map((c) => c.tagName), - addTagTransformers: !!buildCtx.config.extras.additionalTagTransformers, - encapsulation: data.encapsulation, - mode: data.mode, - sourceMap: config.sourceMap, - minify: config.minifyCss, - autoprefixer: config.autoprefixCss, - docs: config.buildDocs, - }); - - /** - * persist component styles for transformed stylesheet - */ - if (cmpStyles) { - cmpStyles.set(filePath, cssTransformResults.styleText); - } - - // Set style docs - if (cmp) { - cmp.styleDocs ||= []; - mergeIntoWith(cmp.styleDocs, cssTransformResults.styleDocs, (docs) => `${docs.name},${docs.mode}`); - } - - // Track dependencies - for (const dep of pluginTransforms.dependencies) { - this.addWatchFile(dep); - compilerCtx.addWatchFile(dep); - } - - buildCtx.diagnostics.push(...pluginTransforms.diagnostics); - buildCtx.diagnostics.push(...cssTransformResults.diagnostics); - const didError = hasError(cssTransformResults.diagnostics) || hasError(pluginTransforms.diagnostics); - if (didError) { - this.error('Plugin CSS transform error'); - } - - const hasUpdatedStyle = buildCtx.stylesUpdated.some((s) => { - return s.styleTag === data.tag && s.styleMode === data.mode && s.styleText === cssTransformResults.styleText; - }); - - /** - * if the style has updated, compose all styles for the component - */ - if (!hasUpdatedStyle && data.tag && data.mode) { - const externalStyles = cmp?.styles?.[0]?.externalStyles; - - /** - * if component has external styles, use a list to keep the order to which - * styles are applied. - */ - const styleText = cmpStyles - ? externalStyles - ? /** - * attempt to find the original `filePath` key through `originalComponentPath` - * and `absolutePath` as path can differ based on how Stencil is installed - * e.g. through `npm link` or `npm install` - */ - externalStyles - .map((es) => cmpStyles.get(es.originalComponentPath) || cmpStyles.get(es.absolutePath)) - .join('\n') - : /** - * if `externalStyles` is not defined, then created the style text in the - * order of which the styles were compiled. - */ - [...cmpStyles.values()].join('\n') - : /** - * if `cmpStyles` is not defined, then use the style text from the transform - * as it is not connected to a component. - */ - cssTransformResults.styleText; - - buildCtx.stylesUpdated.push({ - styleTag: data.tag, - styleMode: data.mode, - styleText, - }); - } - - return { - code: cssTransformResults.output, - map: cssTransformResults.map, - moduleSideEffects: false, - }; - } - - return null; - }, - }; -}; diff --git a/src/compiler/bundle/file-load-plugin.ts b/src/compiler/bundle/file-load-plugin.ts deleted file mode 100644 index df79ee44f1c..00000000000 --- a/src/compiler/bundle/file-load-plugin.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isDtsFile, normalizeFsPath } from '@utils'; -import type { Plugin } from 'rollup'; - -import { InMemoryFileSystem } from '../sys/in-memory-fs'; - -export const fileLoadPlugin = (fs: InMemoryFileSystem): Plugin => { - return { - name: 'fileLoadPlugin', - - load(id) { - const fsFilePath = normalizeFsPath(id); - if (isDtsFile(fsFilePath)) { - return ''; - } - return fs.readFile(fsFilePath); - }, - }; -}; diff --git a/src/compiler/bundle/loader-plugin.ts b/src/compiler/bundle/loader-plugin.ts deleted file mode 100644 index 091cc702ebe..00000000000 --- a/src/compiler/bundle/loader-plugin.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { LoadResult, Plugin, ResolveIdResult } from 'rollup'; - -/** - * Rollup plugin that aids in resolving the entry points (1 or more files) for a Stencil project. For example, a project - * using the `dist-custom-elements` output target may have a single 'entry point' for each file containing a component. - * Each of those files will be independently resolved and loaded by this plugin for further processing by Rollup later - * in the bundling process. - * - * @param entries the Stencil project files to process. It should be noted that the keys in this object may not - * necessarily be an absolute or relative path to a file, but may be a Rollup Virtual Module (which begin with \0). - * @returns the rollup plugin that loads and process a Stencil project's entry points - */ -export const loaderPlugin = (entries: { [id: string]: string } = {}): Plugin => { - return { - name: 'stencilLoaderPlugin', - /** - * A rollup build hook for resolving the imports of individual Stencil project files. This hook only resolves - * modules that are contained in the plugin's `entries` argument. [Source](https://rollupjs.org/guide/en/#resolveid) - * @param id the importee to resolve - * @returns a string that resolves an import to some id, null otherwise - */ - resolveId(id: string): ResolveIdResult { - if (id in entries) { - return { - id, - }; - } - return null; - }, - /** - * A rollup build hook for loading individual Stencil project files [Source](https://rollupjs.org/guide/en/#load) - * @param id the path of the module to load. It should be noted that the keys in this object may not necessarily - * be an absolute or relative path to a file, but may be a Rollup Virtual Module. - * @returns the module matched, null otherwise - */ - load(id: string): LoadResult { - if (id in entries) { - return entries[id]; - } - return null; - }, - }; -}; diff --git a/src/compiler/bundle/plugin-helper.ts b/src/compiler/bundle/plugin-helper.ts deleted file mode 100644 index 76db0a366c9..00000000000 --- a/src/compiler/bundle/plugin-helper.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { buildError, relative } from '@utils'; - -import type * as d from '../../declarations'; -import type { BundlePlatform } from './bundle-interface'; - -export const pluginHelper = (config: d.ValidatedConfig, builtCtx: d.BuildCtx, platform: BundlePlatform) => { - return { - name: 'pluginHelper', - resolveId(importee: string, importer: string): null { - if (/\0/.test(importee)) { - // ignore IDs with null character, these belong to other plugins - return null; - } - - if (importee.endsWith('/')) { - importee = importee.slice(0, -1); - } - - if (builtIns.has(importee)) { - let fromMsg = ''; - if (importer) { - fromMsg = ` from ${relative(config.rootDir, importer)}`; - } - const diagnostic = buildError(builtCtx.diagnostics); - diagnostic.header = `Node Polyfills Required`; - diagnostic.messageText = `For the import "${importee}" to be bundled${fromMsg}, ensure the "rollup-plugin-node-polyfills" plugin is installed and added to the stencil config plugins (${platform}). Please see the bundling docs for more information. - Further information: https://stenciljs.com/docs/module-bundling`; - } - return null; - }, - }; -}; - -const builtIns = new Set([ - 'child_process', - 'cluster', - 'dgram', - 'dns', - 'module', - 'net', - 'readline', - 'repl', - 'tls', - - 'assert', - 'console', - 'constants', - 'domain', - 'events', - 'path', - 'punycode', - 'querystring', - '_stream_duplex', - '_stream_passthrough', - '_stream_readable', - '_stream_writable', - '_stream_transform', - 'string_decoder', - 'sys', - 'tty', - - 'crypto', - 'fs', - - 'Buffer', - 'buffer', - 'global', - 'http', - 'https', - 'os', - 'process', - 'stream', - 'timers', - 'url', - 'util', - 'vm', - 'zlib', -]); diff --git a/src/compiler/bundle/server-plugin.ts b/src/compiler/bundle/server-plugin.ts deleted file mode 100644 index 9662e86567b..00000000000 --- a/src/compiler/bundle/server-plugin.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { isOutputTargetHydrate, isString, normalizeFsPath } from '@utils'; -import { isAbsolute } from 'path'; -import type { Plugin } from 'rollup'; - -import type * as d from '../../declarations'; -import type { BundlePlatform } from './bundle-interface'; - -export const serverPlugin = (config: d.ValidatedConfig, platform: BundlePlatform): Plugin => { - const isHydrateBundle = platform === 'hydrate'; - const serverVarid = `@removed-server-code`; - - const isServerOnlyModule = (id: string) => { - if (isString(id)) { - id = normalizeFsPath(id); - return id.includes('.server/') || id.endsWith('.server'); - } - return false; - }; - - const externals = isHydrateBundle - ? config.outputTargets.filter(isOutputTargetHydrate).flatMap((o) => o.external) - : []; - - return { - name: 'serverPlugin', - - resolveId(id, importer) { - if (id === serverVarid) { - return id; - } - if (isHydrateBundle) { - if (externals.includes(id)) { - // don't attempt to bundle node builtins for the hydrate bundle - return { - id, - external: true, - }; - } - if (isServerOnlyModule(importer) && !id.startsWith('.') && !isAbsolute(id)) { - // do not bundle if the importer is a server-only module - // and the module it is importing is a node module - return { - id, - external: true, - }; - } - } else { - if (isServerOnlyModule(id)) { - // any path that has .server in it shouldn't actually - // be bundled in the web build, only the hydrate build - return serverVarid; - } - } - return null; - }, - - load(id) { - if (id === serverVarid) { - return { - code: 'export default {};', - syntheticNamedExports: true, - }; - } - return null; - }, - }; -}; diff --git a/src/compiler/bundle/test/ext-transforms-plugin.spec.ts b/src/compiler/bundle/test/ext-transforms-plugin.spec.ts deleted file mode 100644 index 984ee2d7a80..00000000000 --- a/src/compiler/bundle/test/ext-transforms-plugin.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { mockBuildCtx, mockCompilerCtx, mockModule, mockValidatedConfig } from '@stencil/core/testing'; -import { normalizePath } from '@utils'; - -import * as importPathLib from '../../transformers/stencil-import-path'; -import { stubComponentCompilerMeta } from '../../types/tests/ComponentCompilerMeta.stub'; -import { BundleOptions } from '../bundle-interface'; -import { extTransformsPlugin } from '../ext-transforms-plugin'; - -describe('extTransformsPlugin', () => { - function setup(bundleOptsOverrides: Partial = {}) { - const config = mockValidatedConfig({ - plugins: [], - outputTargets: [ - { - type: 'dist-collection', - dir: 'dist/', - collectionDir: 'dist/collectionDir', - }, - ], - srcDir: '/some/stubbed/path', - }); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - - const compilerComponentMeta = stubComponentCompilerMeta({ - tagName: 'my-component', - componentClassName: 'MyComponent', - }); - - buildCtx.components = [compilerComponentMeta]; - - compilerCtx.moduleMap.set( - compilerComponentMeta.sourceFilePath, - mockModule({ - cmps: [compilerComponentMeta], - }), - ); - - const bundleOpts: BundleOptions = { - id: 'test-bundle', - platform: 'client', - inputs: {}, - ...bundleOptsOverrides, - }; - - const cssText = ':host { text: pink; }'; - - // mock out the read for our CSS - jest.spyOn(compilerCtx.fs, 'readFile').mockResolvedValue(cssText); - - // mock out compilerCtx.worker.transformCssToEsm because 1) we want to - // test what arguments are passed to it and 2) calling it un-mocked causes - // the infamous autoprefixer-spew-issue :( - const transformCssToEsmSpy = jest.spyOn(compilerCtx.worker, 'transformCssToEsm').mockResolvedValue({ - styleText: cssText, - output: cssText, - map: null, - diagnostics: [], - imports: [], - defaultVarName: 'foo', - styleDocs: [], - }); - - const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile'); - return { - plugin: extTransformsPlugin(config, compilerCtx, buildCtx), - config, - compilerCtx, - buildCtx, - bundleOpts, - writeFileSpy, - transformCssToEsmSpy, - cssText, - }; - } - - describe('transform function', () => { - it('should set name', () => { - expect(setup().plugin.name).toBe('extTransformsPlugin'); - }); - - it('should return early if no data can be gleaned from the id', async () => { - const { plugin } = setup(); - // @ts-ignore we're testing something which shouldn't normally happen, - // but might if an argument of the wrong type were passed as `id` - const parseSpy = jest.spyOn(importPathLib, 'parseImportPath').mockReturnValue({ data: null }); - // @ts-ignore the Rollup plugins expect to be called in a Rollup context - expect(await plugin.transform('asdf', 'foo.css')).toBe(null); - parseSpy.mockRestore(); - }); - - it('should write CSS files if associated with a tag', async () => { - const { plugin, writeFileSpy } = setup(); - - // @ts-ignore the Rollup plugins expect to be called in a Rollup context - await plugin.transform('asdf', '/some/stubbed/path/foo.css?tag=my-component'); - - const [path, css] = writeFileSpy.mock.calls[0]; - - expect(normalizePath(path)).toBe('./dist/collectionDir/foo.css'); - - expect(css).toBe(':host { text: pink; }'); - }); - }); -}); diff --git a/src/compiler/bundle/typescript-plugin.ts b/src/compiler/bundle/typescript-plugin.ts deleted file mode 100644 index c3fa2ae4a96..00000000000 --- a/src/compiler/bundle/typescript-plugin.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { isDtsFile, isString, normalizeFsPath } from '@utils'; -import { basename, isAbsolute } from 'path'; -import type { LoadResult, Plugin, TransformResult } from 'rollup'; -import ts from 'typescript'; - -import type * as d from '../../declarations'; -import { tsResolveModuleName } from '../sys/typescript/typescript-resolve-module'; -import { getModule } from '../transpile/transpiled-module'; -import type { BundleOptions } from './bundle-interface'; - -/** - * Rollup plugin that aids in resolving the TypeScript files and performing the transpilation step. - * @param compilerCtx the current compiler context - * @param bundleOpts Rollup bundling options to apply during TypeScript compilation - * @param config the Stencil configuration for the project - * @returns the rollup plugin for handling TypeScript files. - */ -export const typescriptPlugin = ( - compilerCtx: d.CompilerCtx, - bundleOpts: BundleOptions, - config: d.ValidatedConfig, -): Plugin => { - return { - name: `${bundleOpts.id}TypescriptPlugin`, - - /** - * A rollup build hook for loading TypeScript files and their associated source maps (if they exist). - * [Source](https://rollupjs.org/guide/en/#load) - * @param id the path of the file to load - * @returns the module matched (with its sourcemap if it exists), null otherwise - */ - load(id: string): LoadResult { - if (isAbsolute(id)) { - const fsFilePath = normalizeFsPath(id); - const module = getModule(compilerCtx, fsFilePath); - - if (module) { - if (!module.sourceMapFileText) { - return { code: module.staticSourceFileText, map: null }; - } - - const sourceMap: d.SourceMap = JSON.parse(module.sourceMapFileText); - sourceMap.sources = sourceMap.sources.map((src) => basename(src)); - return { code: module.staticSourceFileText, map: sourceMap }; - } - } - return null; - }, - /** - * Performs TypeScript compilation/transpilation, including applying any transformations against the Abstract Syntax - * Tree (AST) specific to stencil - * @param _code the code to modify, unused - * @param id module's identifier - * @returns the transpiled code, with its associated sourcemap. null otherwise - */ - transform(_code: string, id: string): TransformResult { - if (isAbsolute(id)) { - const fsFilePath = normalizeFsPath(id); - const mod = getModule(compilerCtx, fsFilePath); - if (mod?.cmps) { - const tsResult = ts.transpileModule(mod.staticSourceFileText, { - compilerOptions: config.tsCompilerOptions, - fileName: mod.sourceFilePath, - transformers: { - before: bundleOpts.customBeforeTransformers ?? [], - }, - }); - const sourceMap: d.SourceMap = tsResult.sourceMapText ? JSON.parse(tsResult.sourceMapText) : null; - return { code: tsResult.outputText, map: sourceMap }; - } - } - return null; - }, - }; -}; - -export const resolveIdWithTypeScript = (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx): Plugin => { - return { - name: `resolveIdWithTypeScript`, - - async resolveId(importee, importer) { - if (/\0/.test(importee) || !isString(importer)) { - return null; - } - - const tsResolved = tsResolveModuleName(config, compilerCtx, importee, importer); - if (tsResolved && tsResolved.resolvedModule) { - // this is probably a .d.ts file for whatever reason in how TS resolves this - // use this resolved file as the "importer" - const tsResolvedPath = tsResolved.resolvedModule.resolvedFileName; - if (isString(tsResolvedPath) && !isDtsFile(tsResolvedPath)) { - return tsResolvedPath; - } - } - - return null; - }, - }; -}; diff --git a/src/compiler/bundle/user-index-plugin.ts b/src/compiler/bundle/user-index-plugin.ts deleted file mode 100644 index d5c8f52483d..00000000000 --- a/src/compiler/bundle/user-index-plugin.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { join } from '@utils'; -import type { Plugin } from 'rollup'; - -import type * as d from '../../declarations'; -import { USER_INDEX_ENTRY_ID } from './entry-alias-ids'; - -export const userIndexPlugin = (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx): Plugin => { - return { - name: 'userIndexPlugin', - - async resolveId(importee) { - if (importee === USER_INDEX_ENTRY_ID) { - const usersIndexJsPath = join(config.srcDir, 'index.ts'); - const hasUserIndex = await compilerCtx.fs.access(usersIndexJsPath); - if (hasUserIndex) { - return usersIndexJsPath; - } - return importee; - } - return null; - }, - - async load(id) { - if (id === USER_INDEX_ENTRY_ID) { - return `//! Autogenerated index`; - } - return null; - }, - }; -}; diff --git a/src/compiler/bundle/worker-plugin.ts b/src/compiler/bundle/worker-plugin.ts deleted file mode 100644 index 7b2a42fc4e2..00000000000 --- a/src/compiler/bundle/worker-plugin.ts +++ /dev/null @@ -1,474 +0,0 @@ -import { generatePreamble, hasError, normalizeFsPath } from '@utils'; -import type { Plugin, PluginContext, TransformResult } from 'rollup'; - -import type * as d from '../../declarations'; -import type { BundlePlatform } from './bundle-interface'; -import { optimizeModule } from '../optimize/optimize-module'; -import { bundleOutput } from './bundle-output'; -import { STENCIL_INTERNAL_ID } from './entry-alias-ids'; - -export const workerPlugin = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - platform: BundlePlatform, - inlineWorkers: boolean, -): Plugin => { - if (platform === 'worker' || platform === 'hydrate') { - return { - name: 'workerPlugin', - transform(_, id) { - if (id.endsWith('?worker') || id.endsWith('?worker-inline')) { - return { - code: getMockedWorkerMain(), - map: { mappings: '' }, - }; - } - return null; - }, - }; - } - - const workersMap = new Map(); - - return { - name: 'workerPlugin', - - buildStart() { - workersMap.clear(); - }, - - resolveId(id) { - if (id === WORKER_HELPER_ID) { - return { - id, - moduleSideEffects: false, - }; - } - return null; - }, - - load(id) { - if (id === WORKER_HELPER_ID) { - return WORKER_HELPERS; - } - return null; - }, - - async transform(_, id): Promise { - if (/\0/.test(id)) { - return null; - } - - // Canonical worker path - if (id.endsWith('?worker')) { - const workerEntryPath = normalizeFsPath(id); - const workerName = getWorkerName(workerEntryPath); - const { code, dependencies, workerMsgId } = await getWorker( - config, - compilerCtx, - buildCtx, - this, - workersMap, - workerEntryPath, - ); - const referenceId = this.emitFile({ - type: 'asset', - source: code, - name: workerName + '.js', - }); - dependencies.forEach((id) => this.addWatchFile(id)); - return { - code: getWorkerMain(referenceId, workerName, workerMsgId), - map: { mappings: '' }, - moduleSideEffects: false, - }; - } else if (id.endsWith('?worker-inline')) { - const workerEntryPath = normalizeFsPath(id); - const workerName = getWorkerName(workerEntryPath); - const { code, dependencies, workerMsgId } = await getWorker( - config, - compilerCtx, - buildCtx, - this, - workersMap, - workerEntryPath, - ); - const referenceId = this.emitFile({ - type: 'asset', - source: code, - name: workerName + '.js', - }); - dependencies.forEach((id) => this.addWatchFile(id)); - return { - code: getInlineWorker(referenceId, workerName, workerMsgId), - map: { mappings: '' }, - moduleSideEffects: false, - }; - } - - // Proxy worker path - const workerEntryPath = getWorkerEntryPath(id); - if (workerEntryPath != null) { - const worker = await getWorker(config, compilerCtx, buildCtx, this, workersMap, workerEntryPath); - if (worker) { - if (inlineWorkers) { - return { - code: getInlineWorkerProxy(workerEntryPath, worker.workerMsgId, worker.exports), - map: { mappings: '' }, - moduleSideEffects: false, - }; - } else { - return { - code: getWorkerProxy(workerEntryPath, worker.exports), - map: { mappings: '' }, - moduleSideEffects: false, - }; - } - } - } - return null; - }, - }; -}; - -const getWorkerEntryPath = (id: string) => { - if (WORKER_SUFFIX.some((p) => id.endsWith(p))) { - return normalizeFsPath(id); - } - return null; -}; - -interface WorkerMeta { - code: string; - workerMsgId: string; - exports: string[]; - dependencies: string[]; -} - -const getWorker = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - ctx: PluginContext, - workersMap: Map, - workerEntryPath: string, -): Promise => { - let worker = workersMap.get(workerEntryPath); - if (!worker) { - worker = await buildWorker(config, compilerCtx, buildCtx, ctx, workerEntryPath); - workersMap.set(workerEntryPath, worker); - } - return worker; -}; - -const getWorkerName = (id: string) => { - const parts = id.split('/').filter((i) => !i.includes('index')); - id = parts[parts.length - 1]; - return id.replace('.tsx', '').replace('.ts', ''); -}; - -const buildWorker = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - ctx: PluginContext, - workerEntryPath: string, -) => { - const workerName = getWorkerName(workerEntryPath); - const workerMsgId = `stencil.${workerName}`; - const build = await bundleOutput(config, compilerCtx, buildCtx, { - platform: 'worker', - id: workerName, - inputs: { - [workerName]: workerEntryPath, - }, - inlineDynamicImports: true, - }); - - if (build) { - // Generate commonjs output so we can intercept exports at runtime - const output = await build.generate({ - format: 'commonjs', - banner: `${generatePreamble(config)}\n(()=>{\n`, - footer: '})();', - intro: getWorkerIntro(workerMsgId, config.devMode), - esModule: false, - externalLiveBindings: false, - }); - const entryPoint = output.output[0]; - if (entryPoint.imports.length > 0) { - ctx.error('Workers should not have any external imports: ' + JSON.stringify(entryPoint.imports)); - } - - // Optimize code - let code = entryPoint.code; - const results = await optimizeModule(config, compilerCtx, { - input: code, - sourceTarget: config.buildEs5 ? 'es5' : 'es2017', - isCore: false, - minify: config.minifyJs, - inlineHelpers: true, - }); - buildCtx.diagnostics.push(...results.diagnostics); - if (!hasError(results.diagnostics)) { - code = results.output; - } - - return { - code, - exports: entryPoint.exports, - workerMsgId, - dependencies: Object.keys(entryPoint.modules).filter((id) => !/\0/.test(id) && id !== workerEntryPath), - }; - } - return null; -}; - -const WORKER_SUFFIX = ['.worker.ts', '.worker.tsx', '.worker/index.ts', '.worker/index.tsx']; - -const WORKER_HELPER_ID = '@worker-helper'; - -const GET_TRANSFERABLES = ` -const isInstanceOf = (value, className) => { - const C = globalThis[className]; - return C != null && value instanceof C; -} -const getTransferables = (value) => { - if (value != null) { - if ( - isInstanceOf(value, "ArrayBuffer") || - isInstanceOf(value, "MessagePort") || - isInstanceOf(value, "ImageBitmap") || - isInstanceOf(value, "OffscreenCanvas") - ) { - return [value]; - } - if (typeof value === "object") { - if (value.constructor === Object) { - value = Object.values(value); - } - if (Array.isArray(value)) { - return value.flatMap(getTransferables); - } - return getTransferables(value.buffer); - } - } - return []; -};`; -const getWorkerIntro = (workerMsgId: string, isDev: boolean) => ` -${GET_TRANSFERABLES} -const exports = {}; -const workerMsgId = '${workerMsgId}'; -const workerMsgCallbackId = workerMsgId + '.cb'; -addEventListener('message', async ({data}) => { - if (data && data[0] === workerMsgId) { - let id = data[1]; - let method = data[2]; - let args = data[3]; - let i = 0; - let argsLen = args.length; - let value; - let err; - - try { - for (; i < argsLen; i++) { - if (Array.isArray(args[i]) && args[i][0] === workerMsgCallbackId) { - const callbackId = args[i][1]; - args[i] = (...cbArgs) => { - postMessage( - [workerMsgCallbackId, callbackId, cbArgs] - ); - }; - } - } - ${ - isDev - ? ` - value = exports[method](...args); - if (!value || !value.then) { - throw new Error('The exported method "' + method + '" does not return a Promise, make sure it is an "async" function'); - } - value = await value; - ` - : ` - value = await exports[method](...args);` - } - - } catch (e) { - value = null; - if (e instanceof Error) { - err = { - isError: true, - value: { - message: e.message, - name: e.name, - stack: e.stack, - } - }; - } else { - err = { - isError: false, - value: e - }; - } - value = undefined; - } - - const transferables = getTransferables(value); - ${isDev ? `if (transferables.length > 0) console.debug('Transfering', transferables);` : ''} - - postMessage( - [workerMsgId, id, value, err], - transferables - ); - } -}); -`; - -export const WORKER_HELPERS = ` -import { consoleError } from '${STENCIL_INTERNAL_ID}'; - -${GET_TRANSFERABLES} - -let pendingIds = 0; -let callbackIds = 0; -const pending = new Map(); -const callbacks = new Map(); - -export const createWorker = (workerPath, workerName, workerMsgId) => { - const worker = new Worker(workerPath, {name:workerName}); - - worker.addEventListener('message', ({data}) => { - if (data) { - const workerMsg = data[0]; - const id = data[1]; - const value = data[2]; - - if (workerMsg === workerMsgId) { - const err = data[3]; - const [resolve, reject, callbackIds] = pending.get(id); - pending.delete(id); - - if (err) { - const errObj = (err.isError) - ? Object.assign(new Error(err.value.message), err.value) - : err.value; - - consoleError(errObj); - reject(errObj); - } else { - if (callbackIds) { - callbackIds.forEach(id => callbacks.delete(id)); - } - resolve(value); - } - } else if (workerMsg === workerMsgId + '.cb') { - try { - callbacks.get(id)(...value); - } catch (e) { - consoleError(e); - } - } - } - }); - - return worker; -}; - -export const createWorkerProxy = (worker, workerMsgId, exportedMethod) => ( - (...args) => new Promise((resolve, reject) => { - let pendingId = pendingIds++; - let i = 0; - let argLen = args.length; - let mainData = [resolve, reject]; - pending.set(pendingId, mainData); - - for (; i < argLen; i++) { - if (typeof args[i] === 'function') { - const callbackId = callbackIds++; - callbacks.set(callbackId, args[i]); - args[i] = [workerMsgId + '.cb', callbackId]; - (mainData[2] = mainData[2] || []).push(callbackId); - } - } - const postMessage = (w) => ( - w.postMessage( - [workerMsgId, pendingId, exportedMethod, args], - getTransferables(args) - ) - ); - if (worker.then) { - worker.then(postMessage); - } else { - postMessage(worker); - } - }) -); -`; - -const getWorkerMain = (referenceId: string, workerName: string, workerMsgId: string) => { - return ` -import { createWorker } from '${WORKER_HELPER_ID}'; -export const workerName = '${workerName}'; -export const workerMsgId = '${workerMsgId}'; -export const workerPath = import.meta.ROLLUP_FILE_URL_${referenceId}; -export const worker = /*@__PURE__*/createWorker(workerPath, workerName, workerMsgId); -`; -}; - -const getInlineWorker = (referenceId: string, workerName: string, workerMsgId: string) => { - return ` -import { createWorker } from '${WORKER_HELPER_ID}'; -export const workerName = '${workerName}'; -export const workerMsgId = '${workerMsgId}'; -export const workerPath = import.meta.ROLLUP_FILE_URL_${referenceId}; -export let worker; -try { - // first try directly starting the worker with the URL - worker = /*@__PURE__*/createWorker(workerPath, workerName, workerMsgId); -} catch(e) { - // probably a cross-origin issue, try using a Blob instead - const blob = new Blob(['importScripts("' + workerPath + '")'], { type: 'text/javascript' }); - const url = URL.createObjectURL(blob); - worker = /*@__PURE__*/createWorker(url, workerName, workerMsgId); - URL.revokeObjectURL(url); -} -`; -}; - -const getMockedWorkerMain = () => { - // for the hydrate build the workers won't actually work - // however, we still need to make the {worker} export - // kick-in otherwise bundling chokes - return ` -export const workerName = 'mocked-worker'; -export const workerMsgId = workerName; -export const workerPath = workerName; -export const worker = { name: workerName }; -`; -}; - -const getWorkerProxy = (workerEntryPath: string, exportedMethods: string[]) => { - return ` -import { createWorkerProxy } from '${WORKER_HELPER_ID}'; -import { worker, workerName, workerMsgId } from '${workerEntryPath}?worker'; -${exportedMethods - .map((exportedMethod) => { - return `export const ${exportedMethod} = /*@__PURE__*/createWorkerProxy(worker, workerMsgId, '${exportedMethod}');`; - }) - .join('\n')} -`; -}; - -const getInlineWorkerProxy = (workerEntryPath: string, workerMsgId: string, exportedMethods: string[]) => { - return ` -import { createWorkerProxy } from '${WORKER_HELPER_ID}'; -const workerPromise = import('${workerEntryPath}?worker-inline').then(m => m.worker); -${exportedMethods - .map((exportedMethod) => { - return `export const ${exportedMethod} = /*@__PURE__*/createWorkerProxy(workerPromise, '${workerMsgId}', '${exportedMethod}');`; - }) - .join('\n')} -`; -}; diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts deleted file mode 100644 index 228c4c13bfc..00000000000 --- a/src/compiler/compiler.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { isFunction } from '@utils'; -import ts from 'typescript'; - -import type { Compiler, Config, Diagnostic, ValidatedConfig } from '../declarations'; -import { CompilerContext } from './build/compiler-ctx'; -import { createFullBuild } from './build/full-build'; -import { createWatchBuild } from './build/watch-build'; -import { Cache } from './cache'; -import { getConfig } from './sys/config'; -import { createInMemoryFs } from './sys/in-memory-fs'; -import { resolveModuleIdAsync } from './sys/resolve/resolve-module-async'; -import { patchTypescript } from './sys/typescript/typescript-sys'; -import { createSysWorker } from './sys/worker/sys-worker'; - -/** - * Generate a Stencil compiler instance - * @param userConfig a user-provided Stencil configuration to apply to the compiler instance - * @returns a new instance of a Stencil compiler - * @public - */ -export const createCompiler = async (userConfig: Config): Promise => { - // actual compiler code - const config: ValidatedConfig = getConfig(userConfig); - const diagnostics: Diagnostic[] = []; - const sys = config.sys; - const compilerCtx = new CompilerContext(); - - if (isFunction(config.sys.setupCompiler)) { - config.sys.setupCompiler({ ts }); - } - - compilerCtx.fs = createInMemoryFs(sys); - compilerCtx.cache = new Cache(config, createInMemoryFs(sys)); - await compilerCtx.cache.initCacheDir(); - - sys.resolveModuleId = (opts) => resolveModuleIdAsync(sys, compilerCtx.fs, opts); - compilerCtx.worker = createSysWorker(config); - - if (sys.events) { - // Pipe events from sys.events to compilerCtx - sys.events.on(compilerCtx.events.emit); - } - patchTypescript(config, compilerCtx.fs); - - const build = () => createFullBuild(config, compilerCtx); - - const createWatcher = () => createWatchBuild(config, compilerCtx); - - const destroy = async () => { - compilerCtx.reset(); - compilerCtx.events.unsubscribeAll(); - await sys.destroy(); - }; - - const compiler: Compiler = { - build, - createWatcher, - destroy, - sys, - }; - - config.logger.printDiagnostics(diagnostics); - - return compiler; -}; diff --git a/src/compiler/config/config-utils.ts b/src/compiler/config/config-utils.ts deleted file mode 100644 index 6c1f78a9407..00000000000 --- a/src/compiler/config/config-utils.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { isBoolean, join } from '@utils'; -import { isAbsolute } from 'path'; - -import type { ConfigFlags } from '../../cli/config-flags'; -import type * as d from '../../declarations'; - -export const getAbsolutePath = (config: d.ValidatedConfig, dir: string) => { - if (!isAbsolute(dir)) { - dir = join(config.rootDir, dir); - } - return dir; -}; - -/** - * This function does two things: - * - * 1. If you pass a `flagName`, it will hoist that `flagName` out of the - * `ConfigFlags` object and onto the 'root' level (if you will) of the - * `config` under the `configName` (`keyof d.Config`) that you pass. - * 2. If you _don't_ pass a `flagName` it will just set the value you supply - * on the config. - * - * @param config the config that we want to update - * @param configName the key we're setting on the config - * @param flagName either the name of a ConfigFlag prop we want to hoist up or null - * @param defaultValue the default value we should set! - */ -export const setBooleanConfig = ( - config: d.UnvalidatedConfig, - configName: (K & keyof ConfigFlags) | K, - flagName: keyof ConfigFlags | null, - defaultValue: d.Config[K], -) => { - if (flagName) { - const flagValue = config.flags?.[flagName]; - if (isBoolean(flagValue)) { - config[configName] = flagValue; - } - } - - const userConfigName = getUserConfigName(config, configName); - - if (typeof config[userConfigName] === 'function') { - config[userConfigName] = !!config[userConfigName](); - } - - if (isBoolean(config[userConfigName])) { - config[configName] = config[userConfigName]; - } else { - config[configName] = defaultValue; - } -}; - -/** - * Find any possibly mis-capitalized configuration names on the config, logging - * and warning if one is found. - * - * @param config the user-supplied config that we're dealing with - * @param correctConfigName the configuration name that we're checking for right now - * @returns a string container a mis-capitalized config name found on the - * config object, if any. - */ -const getUserConfigName = (config: d.UnvalidatedConfig, correctConfigName: keyof d.Config): string => { - const userConfigNames = Object.keys(config); - - for (const userConfigName of userConfigNames) { - if (userConfigName.toLowerCase() === correctConfigName.toLowerCase()) { - if (userConfigName !== correctConfigName) { - config.logger?.warn(`config "${userConfigName}" should be "${correctConfigName}"`); - return userConfigName; - } - break; - } - } - - return correctConfigName; -}; diff --git a/src/compiler/config/constants.ts b/src/compiler/config/constants.ts deleted file mode 100644 index 03e6f9c57f2..00000000000 --- a/src/compiler/config/constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type * as d from '../../declarations'; - -type DefaultTargetComponentConfig = d.Config['docs']['markdown']['targetComponent']; - -export const DEFAULT_DEV_MODE = false; -export const DEFAULT_HASHED_FILENAME_LENGTH = 8; -export const MIN_HASHED_FILENAME_LENGTH = 4; -export const MAX_HASHED_FILENAME_LENGTH = 32; -export const DEFAULT_NAMESPACE = 'App'; -export const DEFAULT_TARGET_COMPONENT_STYLES: DefaultTargetComponentConfig = { - background: '#f9f', - textColor: '#333', -}; diff --git a/src/compiler/config/outputs/index.ts b/src/compiler/config/outputs/index.ts deleted file mode 100644 index 206cc45bc38..00000000000 --- a/src/compiler/config/outputs/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { buildError, isValidConfigOutputTarget, VALID_CONFIG_OUTPUT_TARGETS } from '@utils'; - -import type * as d from '../../../declarations'; -import { validateCollection } from './validate-collection'; -import { validateCustomElement } from './validate-custom-element'; -import { validateCustomOutput } from './validate-custom-output'; -import { validateDist } from './validate-dist'; -import { validateDocs } from './validate-docs'; -import { validateHydrateScript } from './validate-hydrate-script'; -import { validateLazy } from './validate-lazy'; -import { validateStats } from './validate-stats'; -import { validateWww } from './validate-www'; - -export const validateOutputTargets = (config: d.ValidatedConfig, diagnostics: d.Diagnostic[]) => { - const userOutputs = (config.outputTargets || []).slice(); - - userOutputs.forEach((outputTarget) => { - if (!isValidConfigOutputTarget(outputTarget.type)) { - const err = buildError(diagnostics); - err.messageText = `Invalid outputTarget type "${ - outputTarget.type - }". Valid outputTarget types include: ${VALID_CONFIG_OUTPUT_TARGETS.map((t) => `"${t}"`).join(', ')}`; - } - }); - - config.outputTargets = [ - ...validateCollection(config, userOutputs), - ...validateCustomElement(config, userOutputs), - ...validateCustomOutput(config, diagnostics, userOutputs), - ...validateLazy(config, userOutputs), - ...validateWww(config, diagnostics, userOutputs), - ...validateDist(config, userOutputs), - ...validateDocs(config, diagnostics, userOutputs), - ...validateStats(config, userOutputs), - ]; - - // hydrate also gets info from the www output - config.outputTargets = [ - ...config.outputTargets, - ...validateHydrateScript(config, [...userOutputs, ...config.outputTargets]), - ]; -}; diff --git a/src/compiler/config/outputs/validate-collection.ts b/src/compiler/config/outputs/validate-collection.ts deleted file mode 100644 index 842f0193861..00000000000 --- a/src/compiler/config/outputs/validate-collection.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { isBoolean, isOutputTargetDistCollection } from '@utils'; - -import type * as d from '../../../declarations'; -import { getAbsolutePath } from '../config-utils'; - -/** - * Validate and return DIST_COLLECTION output targets, ensuring that the `dir` - * property is set on them. - * - * @param config a validated configuration object - * @param userOutputs an array of output targets - * @returns an array of validated DIST_COLLECTION output targets - */ -export const validateCollection = ( - config: d.ValidatedConfig, - userOutputs: d.OutputTarget[], -): d.OutputTargetDistCollection[] => { - return userOutputs.filter(isOutputTargetDistCollection).map((outputTarget) => { - return { - ...outputTarget, - transformAliasedImportPaths: isBoolean(outputTarget.transformAliasedImportPaths) - ? outputTarget.transformAliasedImportPaths - : true, - dir: getAbsolutePath(config, outputTarget.dir ?? 'dist/collection'), - }; - }); -}; diff --git a/src/compiler/config/outputs/validate-custom-element.ts b/src/compiler/config/outputs/validate-custom-element.ts deleted file mode 100644 index 8a7dcc9040d..00000000000 --- a/src/compiler/config/outputs/validate-custom-element.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { COPY, DIST_TYPES, isBoolean, isOutputTargetDistCustomElements, join } from '@utils'; - -import type { - OutputTarget, - OutputTargetCopy, - OutputTargetDistCustomElements, - OutputTargetDistTypes, - ValidatedConfig, -} from '../../../declarations'; -import { CustomElementsExportBehaviorOptions } from '../../../declarations'; -import { getAbsolutePath } from '../config-utils'; -import { validateCopy } from '../validate-copy'; - -/** - * Validate one or more `dist-custom-elements` output targets. Validation of an output target may involve back-filling - * fields that are omitted with sensible defaults and/or creating additional supporting output targets that were not - * explicitly defined by the user - * @param config the Stencil configuration associated with the project being compiled - * @param userOutputs the output target(s) specified by the user - * @returns the validated output target(s) - */ -export const validateCustomElement = ( - config: ValidatedConfig, - userOutputs: ReadonlyArray, -): ReadonlyArray => { - const defaultDir = 'dist'; - - return userOutputs.filter(isOutputTargetDistCustomElements).reduce( - (outputs, o) => { - const outputTarget = { - ...o, - dir: getAbsolutePath(config, o.dir || join(defaultDir, 'components')), - }; - if (!isBoolean(outputTarget.empty)) { - outputTarget.empty = true; - } - if (!isBoolean(outputTarget.externalRuntime)) { - outputTarget.externalRuntime = true; - } - if (!isBoolean(outputTarget.generateTypeDeclarations)) { - outputTarget.generateTypeDeclarations = true; - } - // Export behavior must be defined on the validated target config and must - // be one of the export behavior valid values - if ( - outputTarget.customElementsExportBehavior == null || - !CustomElementsExportBehaviorOptions.includes(outputTarget.customElementsExportBehavior) - ) { - outputTarget.customElementsExportBehavior = 'default'; - } - - // Normalize autoLoader option - if (outputTarget.autoLoader === true) { - outputTarget.autoLoader = { - fileName: 'loader', - autoStart: true, - }; - } else if (outputTarget.autoLoader && typeof outputTarget.autoLoader === 'object') { - outputTarget.autoLoader = { - fileName: outputTarget.autoLoader.fileName || 'loader', - autoStart: outputTarget.autoLoader.autoStart !== false, - }; - } - - // unlike other output targets, Stencil does not allow users to define the output location of types at this time - if (outputTarget.generateTypeDeclarations) { - const typesDirectory = getAbsolutePath(config, join(defaultDir, 'types')); - outputs.push({ - type: DIST_TYPES, - dir: outputTarget.dir, - typesDir: typesDirectory, - }); - } - - outputTarget.copy = validateCopy(outputTarget.copy, []); - - if (outputTarget.copy.length > 0) { - outputs.push({ - type: COPY, - dir: config.rootDir, - copy: [...outputTarget.copy], - }); - } - outputs.push(outputTarget); - - return outputs; - }, - [] as (OutputTargetDistCustomElements | OutputTargetCopy | OutputTargetDistTypes)[], - ); -}; diff --git a/src/compiler/config/outputs/validate-custom-output.ts b/src/compiler/config/outputs/validate-custom-output.ts deleted file mode 100644 index 9e722ff4ffe..00000000000 --- a/src/compiler/config/outputs/validate-custom-output.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { catchError, COPY, isOutputTargetCustom } from '@utils'; - -import type * as d from '../../../declarations'; - -export const validateCustomOutput = ( - config: d.ValidatedConfig, - diagnostics: d.Diagnostic[], - userOutputs: d.OutputTarget[], -) => { - return userOutputs.filter(isOutputTargetCustom).map((o) => { - if (o.validate) { - const localDiagnostics: d.Diagnostic[] = []; - try { - o.validate(config, diagnostics); - } catch (e: any) { - catchError(localDiagnostics, e); - } - if (o.copy && o.copy.length > 0) { - config.outputTargets.push({ - type: COPY, - dir: config.rootDir, - copy: [...o.copy], - }); - } - diagnostics.push(...localDiagnostics); - } - return o; - }); -}; diff --git a/src/compiler/config/outputs/validate-dist.ts b/src/compiler/config/outputs/validate-dist.ts deleted file mode 100644 index e597740fbf2..00000000000 --- a/src/compiler/config/outputs/validate-dist.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { - COPY, - DIST_COLLECTION, - DIST_GLOBAL_STYLES, - DIST_LAZY, - DIST_LAZY_LOADER, - DIST_TYPES, - getComponentsDtsTypesFilePath, - isBoolean, - isOutputTargetDist, - isString, - join, - resolve, -} from '@utils'; -import { isAbsolute } from 'path'; - -import type * as d from '../../../declarations'; -import { getAbsolutePath } from '../config-utils'; -import { validateCopy } from '../validate-copy'; - -/** - * Validate that the "dist" output targets are valid and ready to go. - * - * This function will also add in additional output targets to its output, based on the input supplied. - * - * @param config the compiler config, what else? - * @param userOutputs a user-supplied list of output targets. - * @returns a list of OutputTargets which have been validated for us. - */ -export const validateDist = (config: d.ValidatedConfig, userOutputs: d.OutputTarget[]): d.OutputTarget[] => { - const distOutputTargets = userOutputs.filter(isOutputTargetDist); - - const outputs: d.OutputTarget[] = []; - - for (const outputTarget of distOutputTargets) { - const distOutputTarget = validateOutputTargetDist(config, outputTarget); - outputs.push(distOutputTarget); - - const namespace = config.fsNamespace || 'app'; - const lazyDir = join(distOutputTarget.buildDir, namespace); - - // Lazy build for CDN in dist - outputs.push({ - type: DIST_LAZY, - esmDir: lazyDir, - systemDir: config.buildEs5 ? lazyDir : undefined, - systemLoaderFile: config.buildEs5 ? join(lazyDir, namespace + '.js') : undefined, - legacyLoaderFile: join(distOutputTarget.buildDir, namespace + '.js'), - polyfills: outputTarget.polyfills !== undefined ? !!distOutputTarget.polyfills : true, - isBrowserBuild: true, - empty: distOutputTarget.empty, - }); - outputs.push({ - type: COPY, - dir: lazyDir, - copyAssets: 'dist', - copy: (distOutputTarget.copy ?? []).concat(), - }); - outputs.push({ - type: DIST_GLOBAL_STYLES, - file: join(lazyDir, `${config.fsNamespace}.css`), - }); - - outputs.push({ - type: DIST_TYPES, - dir: distOutputTarget.dir, - typesDir: distOutputTarget.typesDir, - }); - - if (config.buildDist) { - if (distOutputTarget.collectionDir) { - outputs.push({ - type: DIST_COLLECTION, - dir: distOutputTarget.dir, - collectionDir: distOutputTarget.collectionDir, - empty: distOutputTarget.empty, - transformAliasedImportPaths: distOutputTarget.transformAliasedImportPathsInCollection, - }); - outputs.push({ - type: COPY, - dir: distOutputTarget.collectionDir, - copyAssets: 'collection', - copy: [...distOutputTarget.copy, { src: '**/*.svg' }, { src: '**/*.js' }], - }); - } - - const esmDir = join(distOutputTarget.dir, 'esm'); - const esmEs5Dir = config.buildEs5 ? join(distOutputTarget.dir, 'esm-es5') : undefined; - const cjsDir = join(distOutputTarget.dir, 'cjs'); - - // Create lazy output-target - outputs.push({ - type: DIST_LAZY, - esmDir, - esmEs5Dir, - cjsDir, - - cjsIndexFile: join(distOutputTarget.dir, 'index.cjs.js'), - esmIndexFile: join(distOutputTarget.dir, 'index.js'), - polyfills: true, - empty: distOutputTarget.empty, - }); - - // Create output target that will generate the /loader entry-point - outputs.push({ - type: DIST_LAZY_LOADER, - dir: distOutputTarget.esmLoaderPath, - - esmDir, - esmEs5Dir, - cjsDir, - componentDts: getComponentsDtsTypesFilePath(distOutputTarget), - empty: distOutputTarget.empty, - }); - } - } - - return outputs; -}; - -/** - * Validate that an OutputTargetDist object has what it needs to do it's job. - * To enforce this, we have this function return - * `Required`, giving us a compile-time check that all - * properties are defined (with either user-supplied or default values). - * - * @param config the current config - * @param o the OutputTargetDist object we want to validate - * @returns `Required`, i.e. `d.OutputTargetDist` with all - * optional properties rendered un-optional. - */ -const validateOutputTargetDist = (config: d.ValidatedConfig, o: d.OutputTargetDist): Required => { - // we need to create an object with a bunch of default values here so that - // the typescript compiler can infer their types correctly - const outputTarget = { - ...o, - dir: getAbsolutePath(config, o.dir || DEFAULT_DIR), - buildDir: isString(o.buildDir) ? o.buildDir : DEFAULT_BUILD_DIR, - collectionDir: o.collectionDir !== undefined ? o.collectionDir : DEFAULT_COLLECTION_DIR, - typesDir: o.typesDir || DEFAULT_TYPES_DIR, - esmLoaderPath: o.esmLoaderPath || DEFAULT_ESM_LOADER_DIR, - copy: validateCopy(o.copy ?? [], []), - polyfills: isBoolean(o.polyfills) ? o.polyfills : false, - empty: isBoolean(o.empty) ? o.empty : true, - transformAliasedImportPathsInCollection: isBoolean(o.transformAliasedImportPathsInCollection) - ? o.transformAliasedImportPathsInCollection - : true, - isPrimaryPackageOutputTarget: o.isPrimaryPackageOutputTarget ?? false, - } satisfies Required; - - if (!isAbsolute(outputTarget.buildDir)) { - outputTarget.buildDir = join(outputTarget.dir, outputTarget.buildDir); - } - - if (outputTarget.collectionDir && !isAbsolute(outputTarget.collectionDir)) { - outputTarget.collectionDir = join(outputTarget.dir, outputTarget.collectionDir); - } - - if (!isAbsolute(outputTarget.esmLoaderPath)) { - outputTarget.esmLoaderPath = resolve(outputTarget.dir, outputTarget.esmLoaderPath); - } - - if (!isAbsolute(outputTarget.typesDir)) { - outputTarget.typesDir = join(outputTarget.dir, outputTarget.typesDir); - } - - return outputTarget; -}; - -const DEFAULT_DIR = 'dist'; -const DEFAULT_BUILD_DIR = ''; -const DEFAULT_COLLECTION_DIR = 'collection'; -const DEFAULT_TYPES_DIR = 'types'; -const DEFAULT_ESM_LOADER_DIR = 'loader'; diff --git a/src/compiler/config/outputs/validate-docs.ts b/src/compiler/config/outputs/validate-docs.ts deleted file mode 100644 index d27b2e70ca0..00000000000 --- a/src/compiler/config/outputs/validate-docs.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { - buildError, - DOCS_JSON, - DOCS_README, - isFunction, - isOutputTargetDocsCustom, - isOutputTargetDocsCustomElementsManifest, - isOutputTargetDocsJson, - isOutputTargetDocsReadme, - isOutputTargetDocsVscode, - isString, - join, -} from '@utils'; -import { isAbsolute } from 'path'; - -import type * as d from '../../../declarations'; -import { NOTE } from '../../docs/constants'; - -export const validateDocs = (config: d.ValidatedConfig, diagnostics: d.Diagnostic[], userOutputs: d.OutputTarget[]) => { - const docsOutputs: d.OutputTarget[] = []; - - // json docs flag - if (isString(config.flags.docsJson)) { - docsOutputs.push( - validateJsonDocsOutputTarget(config, diagnostics, { - type: DOCS_JSON, - file: config.flags.docsJson, - }), - ); - } - - // json docs - const jsonDocsOutputs = userOutputs.filter(isOutputTargetDocsJson); - jsonDocsOutputs.forEach((jsonDocsOutput) => { - docsOutputs.push(validateJsonDocsOutputTarget(config, diagnostics, jsonDocsOutput)); - }); - - // readme docs flag - if (config.flags.docs || config.flags.task === 'docs') { - if (!userOutputs.some(isOutputTargetDocsReadme)) { - // didn't provide a docs config, so let's add one - docsOutputs.push(validateReadmeOutputTarget(config, { type: DOCS_README })); - } - } - - // readme docs - const readmeDocsOutputs = userOutputs.filter(isOutputTargetDocsReadme); - readmeDocsOutputs.forEach((readmeDocsOutput) => { - docsOutputs.push(validateReadmeOutputTarget(config, readmeDocsOutput)); - }); - - // custom docs - const customDocsOutputs = userOutputs.filter(isOutputTargetDocsCustom); - customDocsOutputs.forEach((jsonDocsOutput) => { - docsOutputs.push(validateCustomDocsOutputTarget(diagnostics, jsonDocsOutput)); - }); - - // vscode docs - const vscodeDocsOutputs = userOutputs.filter(isOutputTargetDocsVscode); - vscodeDocsOutputs.forEach((vscodeDocsOutput) => { - docsOutputs.push(validateVScodeDocsOutputTarget(diagnostics, vscodeDocsOutput)); - }); - - // custom elements manifest docs - const customElementsManifestOutputs = userOutputs.filter(isOutputTargetDocsCustomElementsManifest); - customElementsManifestOutputs.forEach((cemOutput) => { - docsOutputs.push(validateCustomElementsManifestOutputTarget(config, cemOutput)); - }); - - return docsOutputs; -}; - -const validateReadmeOutputTarget = (config: d.ValidatedConfig, outputTarget: d.OutputTargetDocsReadme) => { - if (!isString(outputTarget.dir)) { - outputTarget.dir = config.srcDir; - } - - if (!isAbsolute(outputTarget.dir)) { - outputTarget.dir = join(config.rootDir, outputTarget.dir); - } - - if (outputTarget.footer == null) { - outputTarget.footer = NOTE; - } - outputTarget.strict = !!outputTarget.strict; - return outputTarget; -}; - -const validateJsonDocsOutputTarget = ( - config: d.ValidatedConfig, - diagnostics: d.Diagnostic[], - outputTarget: d.OutputTargetDocsJson, -) => { - if (!isString(outputTarget.file)) { - const err = buildError(diagnostics); - err.messageText = `docs-json outputTarget missing the "file" option`; - } - - outputTarget.file = join(config.rootDir, outputTarget.file); - if (isString(outputTarget.typesFile)) { - outputTarget.typesFile = join(config.rootDir, outputTarget.typesFile); - } else if (outputTarget.typesFile !== null && outputTarget.file.endsWith('.json')) { - outputTarget.typesFile = outputTarget.file.replace(/\.json$/, '.d.ts'); - } - outputTarget.strict = !!outputTarget.strict; - return outputTarget; -}; - -const validateCustomDocsOutputTarget = (diagnostics: d.Diagnostic[], outputTarget: d.OutputTargetDocsCustom) => { - if (!isFunction(outputTarget.generator)) { - const err = buildError(diagnostics); - err.messageText = `docs-custom outputTarget missing the "generator" function`; - } - - outputTarget.strict = !!outputTarget.strict; - return outputTarget; -}; - -const validateVScodeDocsOutputTarget = (diagnostics: d.Diagnostic[], outputTarget: d.OutputTargetDocsVscode) => { - if (!isString(outputTarget.file)) { - const err = buildError(diagnostics); - err.messageText = `docs-vscode outputTarget missing the "file" path`; - } - return outputTarget; -}; - -const validateCustomElementsManifestOutputTarget = ( - config: d.ValidatedConfig, - outputTarget: d.OutputTargetDocsCustomElementsManifest, -) => { - if (!isString(outputTarget.file)) { - outputTarget.file = 'custom-elements.json'; - } - outputTarget.file = join(config.rootDir, outputTarget.file); - outputTarget.strict = !!outputTarget.strict; - return outputTarget; -}; diff --git a/src/compiler/config/outputs/validate-hydrate-script.ts b/src/compiler/config/outputs/validate-hydrate-script.ts deleted file mode 100644 index 778fa3e31d9..00000000000 --- a/src/compiler/config/outputs/validate-hydrate-script.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - DIST_HYDRATE_SCRIPT, - isBoolean, - isOutputTargetDist, - isOutputTargetHydrate, - isOutputTargetWww, - isString, - join, -} from '@utils'; -import { isAbsolute } from 'path'; - -import type * as d from '../../../declarations'; - -export const validateHydrateScript = (config: d.ValidatedConfig, userOutputs: d.OutputTarget[]) => { - const output: d.OutputTargetHydrate[] = []; - - const hasHydrateOutputTarget = userOutputs.some(isOutputTargetHydrate); - - if (!hasHydrateOutputTarget) { - // we don't already have a hydrate output target - // let's still see if we require one because of other output targets - - const hasWwwOutput = userOutputs.filter(isOutputTargetWww).some((o) => isString(o.indexHtml)); - const shouldBuildHydrate = config.flags.prerender || config.flags.ssr; - - if (hasWwwOutput && shouldBuildHydrate) { - // we're prerendering a www output target, so we'll need a hydrate app - let hydrateDir: string; - const distOutput = userOutputs.find(isOutputTargetDist); - if (distOutput != null && isString(distOutput.dir)) { - hydrateDir = join(distOutput.dir, 'hydrate'); - } else { - hydrateDir = 'dist/hydrate'; - } - - const hydrateForWwwOutputTarget: d.OutputTargetHydrate = { - type: DIST_HYDRATE_SCRIPT, - dir: hydrateDir, - }; - userOutputs.push(hydrateForWwwOutputTarget); - } - } - - const hydrateOutputTargets = userOutputs.filter(isOutputTargetHydrate); - - hydrateOutputTargets.forEach((outputTarget) => { - if (!isString(outputTarget.dir)) { - // no directory given, see if we've got a dist to go off of - outputTarget.dir = 'hydrate'; - } - - if (!isAbsolute(outputTarget.dir)) { - outputTarget.dir = join(config.rootDir, outputTarget.dir); - } - - if (!isBoolean(outputTarget.empty)) { - outputTarget.empty = true; - } - - if (!isBoolean(outputTarget.minify)) { - outputTarget.minify = false; - } - - if (!isBoolean(outputTarget.generatePackageJson)) { - outputTarget.generatePackageJson = true; - } - - outputTarget.external = outputTarget.external || []; - - outputTarget.external.push('fs'); - outputTarget.external.push('path'); - outputTarget.external.push('crypto'); - - output.push(outputTarget); - }); - - return output; -}; diff --git a/src/compiler/config/outputs/validate-lazy.ts b/src/compiler/config/outputs/validate-lazy.ts deleted file mode 100644 index 01cf70d7859..00000000000 --- a/src/compiler/config/outputs/validate-lazy.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DIST_LAZY, isBoolean, isOutputTargetDistLazy, join } from '@utils'; - -import type * as d from '../../../declarations'; -import { getAbsolutePath } from '../config-utils'; - -export const validateLazy = (config: d.ValidatedConfig, userOutputs: d.OutputTarget[]) => { - return userOutputs.filter(isOutputTargetDistLazy).map((o) => { - const dir = getAbsolutePath(config, o.dir || join('dist', config.fsNamespace)); - const lazyOutput: d.OutputTargetDistLazy = { - type: DIST_LAZY, - esmDir: dir, - systemDir: config.buildEs5 ? dir : undefined, - systemLoaderFile: config.buildEs5 ? join(dir, `${config.fsNamespace}.js`) : undefined, - polyfills: !!o.polyfills, - isBrowserBuild: true, - empty: isBoolean(o.empty) ? o.empty : true, - }; - return lazyOutput; - }); -}; diff --git a/src/compiler/config/outputs/validate-www.ts b/src/compiler/config/outputs/validate-www.ts deleted file mode 100644 index ce9ffe664b1..00000000000 --- a/src/compiler/config/outputs/validate-www.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - buildError, - COPY, - DIST_GLOBAL_STYLES, - DIST_LAZY, - isBoolean, - isOutputTargetDist, - isOutputTargetWww, - isString, - join, - WWW, -} from '@utils'; -import { isAbsolute } from 'path'; - -import type * as d from '../../../declarations'; -import { getAbsolutePath } from '../config-utils'; -import { validateCopy } from '../validate-copy'; -import { validatePrerender } from '../validate-prerender'; -import { validateServiceWorker } from '../validate-service-worker'; - -export const validateWww = (config: d.ValidatedConfig, diagnostics: d.Diagnostic[], userOutputs: d.OutputTarget[]) => { - const hasOutputTargets = userOutputs.length > 0; - const hasE2eTests = !!config.flags.e2e; - const userWwwOutputs = userOutputs.filter(isOutputTargetWww); - - if ( - !hasOutputTargets || - (hasE2eTests && !userOutputs.some(isOutputTargetWww) && !userOutputs.some(isOutputTargetDist)) - ) { - userWwwOutputs.push({ type: WWW }); - } - - if (config.flags.prerender && userWwwOutputs.length === 0) { - const err = buildError(diagnostics); - err.messageText = `You need at least one "www" output target configured in your stencil.config.ts, when the "--prerender" flag is used`; - } - - return userWwwOutputs.reduce( - ( - outputs: (d.OutputTargetWww | d.OutputTargetDistLazy | d.OutputTargetCopy | d.OutputTargetDistGlobalStyles)[], - o, - ) => { - const outputTarget = validateWwwOutputTarget(config, o, diagnostics); - outputs.push(outputTarget); - - // Add dist-lazy output target - const buildDir = outputTarget.buildDir; - outputs.push({ - type: DIST_LAZY, - dir: buildDir, - esmDir: buildDir, - systemDir: config.buildEs5 ? buildDir : undefined, - systemLoaderFile: config.buildEs5 ? join(buildDir, `${config.fsNamespace}.js`) : undefined, - polyfills: outputTarget.polyfills, - isBrowserBuild: true, - }); - - // Copy for dist - outputs.push({ - type: COPY, - dir: buildDir, - copyAssets: 'dist', - }); - - // Copy for www - outputs.push({ - type: COPY, - dir: outputTarget.appDir, - copy: validateCopy(outputTarget.copy, [ - { src: 'assets', warn: false }, - { src: 'manifest.json', warn: false }, - ]), - }); - - // Generate global style with original name - outputs.push({ - type: DIST_GLOBAL_STYLES, - file: join(buildDir, `${config.fsNamespace}.css`), - }); - - return outputs; - }, - [], - ); -}; - -const validateWwwOutputTarget = ( - config: d.ValidatedConfig, - outputTarget: d.OutputTargetWww, - diagnostics: d.Diagnostic[], -) => { - if (!isString(outputTarget.baseUrl)) { - outputTarget.baseUrl = '/'; - } - - if (!outputTarget.baseUrl.endsWith('/')) { - // Make sure the baseUrl always finish with "/" - outputTarget.baseUrl += '/'; - } - - outputTarget.dir = getAbsolutePath(config, outputTarget.dir || 'www'); - - // Fix "dir" to account - const pathname = new URL(outputTarget.baseUrl, 'http://localhost/').pathname; - outputTarget.appDir = join(outputTarget.dir, pathname); - if (outputTarget.appDir.endsWith('/') || outputTarget.appDir.endsWith('\\')) { - outputTarget.appDir = outputTarget.appDir.substring(0, outputTarget.appDir.length - 1); - } - - if (!isString(outputTarget.buildDir)) { - outputTarget.buildDir = 'build'; - } - - if (!isAbsolute(outputTarget.buildDir)) { - outputTarget.buildDir = join(outputTarget.appDir, outputTarget.buildDir); - } - - if (!isString(outputTarget.indexHtml)) { - outputTarget.indexHtml = 'index.html'; - } - - if (!isAbsolute(outputTarget.indexHtml)) { - outputTarget.indexHtml = join(outputTarget.appDir, outputTarget.indexHtml); - } - - if (!isBoolean(outputTarget.empty)) { - outputTarget.empty = true; - } - - validatePrerender(config, diagnostics, outputTarget); - validateServiceWorker(config, outputTarget); - - if (outputTarget.polyfills === undefined) { - outputTarget.polyfills = true; - } - outputTarget.polyfills = !!outputTarget.polyfills; - - return outputTarget; -}; diff --git a/src/compiler/config/test/fixtures/stencil.config.ts b/src/compiler/config/test/fixtures/stencil.config.ts deleted file mode 100644 index 4919bf2c0ee..00000000000 --- a/src/compiler/config/test/fixtures/stencil.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Config } from '../../../../declarations'; - -export const config: Config = { - hashedFileNameLength: 13, -}; diff --git a/src/compiler/config/test/fixtures/stencil.config2.ts b/src/compiler/config/test/fixtures/stencil.config2.ts deleted file mode 100644 index 2fd4f087052..00000000000 --- a/src/compiler/config/test/fixtures/stencil.config2.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Config } from '../../../../declarations'; - -export const config: Config = { - hashedFileNameLength: 27, - flags: { - dev: true, - }, - extras: { - enableImportInjection: true, - }, -}; diff --git a/src/compiler/config/test/load-config.spec.ts b/src/compiler/config/test/load-config.spec.ts deleted file mode 100644 index 9fd5bb6c2be..00000000000 --- a/src/compiler/config/test/load-config.spec.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { mockCompilerSystem } from '@stencil/core/testing'; -import path from 'path'; -import ts from 'typescript'; - -import { ConfigFlags } from '../../../cli/config-flags'; -import type * as d from '../../../declarations'; -import { normalizePath } from '../../../utils'; -import { loadConfig } from '../load-config'; - -describe('load config', () => { - const configPath = require.resolve('./fixtures/stencil.config.ts'); - const configPath2 = require.resolve('./fixtures/stencil.config2.ts'); - - let sys: d.CompilerSystem; - - beforeEach(() => { - sys = mockCompilerSystem(); - - jest.spyOn(ts, 'getParsedCommandLineOfConfigFile').mockReturnValue({ - options: { - target: ts.ScriptTarget.ES2017, - module: ts.ModuleKind.ESNext, - }, - fileNames: [], - errors: [], - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it("merges a user's configuration with a stencil.config file on disk", async () => { - const loadedConfig = await loadConfig({ - configPath: configPath2, - sys, - config: { - hashedFileNameLength: 9, - rootDir: '/foo/bar', - }, - initTsConfig: true, - }); - - expect(loadedConfig.diagnostics).toHaveLength(0); - - const actualConfig = loadedConfig.config; - // this field is defined on the `init` argument, and should override the value found in the config on disk - expect(actualConfig).toBeDefined(); - expect(actualConfig.hashedFileNameLength).toEqual(9); - // these fields are defined in the config file on disk, and should be present - expect(actualConfig.flags).toEqual({ dev: true }); - expect(actualConfig.extras).toBeDefined(); - expect(actualConfig.extras!.enableImportInjection).toBe(true); - // respects custom root dir - expect(actualConfig.rootDir).toBe('/foo/bar'); - }); - - it('uses the provided config path when no initial config provided', async () => { - const loadedConfig = await loadConfig({ - configPath, - sys, - initTsConfig: true, - }); - - expect(loadedConfig.diagnostics).toHaveLength(0); - - const actualConfig = loadedConfig.config; - expect(actualConfig).toBeDefined(); - // set the config path based on the one provided in the init object - expect(actualConfig.configPath).toBe(normalizePath(configPath)); - // this field is defined in the config file on disk, and should be present - expect(actualConfig.hashedFileNameLength).toBe(13); - // this field should default to an empty object literal, since it wasn't present in the config file - expect(actualConfig.flags).toEqual({}); - }); - - describe('empty initialization argument', () => { - it('provides sensible default values with no config', async () => { - const loadedConfig = await loadConfig({ initTsConfig: true, sys }); - - const actualConfig = loadedConfig.config; - expect(actualConfig).toBeDefined(); - expect(actualConfig.sys).toBeDefined(); - expect(actualConfig.logger).toBeDefined(); - expect(actualConfig.configPath).toBe(null); - }); - - it('creates a tsconfig file when "initTsConfig" set', async () => { - const tsconfigPath = path.resolve(path.dirname(configPath), 'tsconfig.json'); - expect(sys.accessSync(tsconfigPath)).toBe(false); - const loadedConfig = await loadConfig({ initTsConfig: true, configPath, sys }); - expect(sys.accessSync(tsconfigPath)).toBe(true); - expect(loadedConfig.diagnostics).toHaveLength(0); - }); - - it('errors that a tsconfig file could not be created when "initTsConfig" isn\'t present', async () => { - const loadedConfig = await loadConfig({ configPath, sys }); - expect(loadedConfig.diagnostics).toHaveLength(1); - expect(loadedConfig.diagnostics[0]).toEqual({ - absFilePath: undefined, - header: 'Missing tsconfig.json', - level: 'error', - lines: [], - messageText: `Unable to load TypeScript config file. Please create a "tsconfig.json" file within the "${normalizePath( - path.dirname(configPath), - )}" directory.`, - relFilePath: undefined, - type: 'build', - }); - }); - }); - - describe('no initialization argument', () => { - it('errors that a tsconfig file cannot be found', async () => { - const loadConfigResults = await loadConfig({ sys }); - expect(loadConfigResults.diagnostics).toHaveLength(1); - expect(loadConfigResults.diagnostics[0]).toEqual({ - absFilePath: undefined, - header: 'Missing tsconfig.json', - level: 'error', - lines: [], - messageText: expect.stringMatching( - `Unable to load TypeScript config file. Please create a "tsconfig.json" file within the`, - ), - relFilePath: undefined, - type: 'build', - }); - }); - }); -}); diff --git a/src/compiler/config/test/tsconfig.json b/src/compiler/config/test/tsconfig.json deleted file mode 100644 index 0ff0be83173..00000000000 --- a/src/compiler/config/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/config/test/validate-config.spec.ts b/src/compiler/config/test/validate-config.spec.ts deleted file mode 100644 index d09ac53dd09..00000000000 --- a/src/compiler/config/test/validate-config.spec.ts +++ /dev/null @@ -1,629 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockCompilerSystem, mockLoadConfigInit, mockLogger } from '@stencil/core/testing'; -import { DOCS_CUSTOM, DOCS_JSON, DOCS_README, DOCS_VSCODE } from '@utils'; - -import { createConfigFlags } from '../../../cli/config-flags'; -import { isWatchIgnorePath } from '../../fs-watch/fs-watch-rebuild'; -import { validateConfig } from '../validate-config'; - -describe('validation', () => { - let userConfig: d.UnvalidatedConfig; - let bootstrapConfig: d.LoadConfigInit; - const logger = mockLogger(); - const sys = mockCompilerSystem(); - - beforeEach(() => { - userConfig = { - sys: sys, - logger: logger, - rootDir: '/User/some/path/', - namespace: 'Testing', - }; - bootstrapConfig = mockLoadConfigInit(); - }); - - describe('caching', () => { - it('should cache the validated config between calls if the same config is passed back in', () => { - const { config } = validateConfig(userConfig, {}); - const { config: secondRound } = validateConfig(config, {}); - // we should have object identity - expect(config === secondRound).toBe(true); - // objects should be deepEqual as well - expect(config).toEqual(secondRound); - }); - - it('should bust the cache if a different config is supplied than the cached one', () => { - // validate once, caching that result - const { config } = validateConfig(userConfig, {}); - // pass a new initial configuration - const { config: secondRound } = validateConfig({ ...userConfig }, {}); - // shouldn't have object equality with the earlier one - expect(config === secondRound).toBe(false); - }); - }); - - describe('flags', () => { - it('adds a default "flags" object if none is provided', () => { - userConfig.flags = undefined; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.flags).toEqual({}); - }); - - it('serializes a provided "flags" object', () => { - userConfig.flags = createConfigFlags({ dev: false }); - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.flags).toEqual(createConfigFlags({ dev: false })); - }); - - describe('devMode', () => { - it('defaults "devMode" to false when "flag.prod" is truthy', () => { - userConfig.flags = createConfigFlags({ prod: true }); - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devMode).toBe(false); - }); - - it('defaults "devMode" to true when "flag.dev" is truthy', () => { - userConfig.flags = createConfigFlags({ dev: true }); - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devMode).toBe(true); - }); - - it('defaults "devMode" to false when "flag.prod" & "flag.dev" are truthy', () => { - userConfig.flags = createConfigFlags({ dev: true, prod: true }); - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devMode).toBe(false); - }); - - it('sets "devMode" to false if the user provided flag isn\'t a boolean', () => { - // the branch under test explicitly requires a value whose type is not allowed by the type system - const devMode = 'not-a-bool' as unknown as boolean; - userConfig = { devMode }; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devMode).toBe(false); - }); - }); - }); - - describe('allowInlineScripts', () => { - it('set allowInlineScripts true', () => { - userConfig.allowInlineScripts = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.allowInlineScripts).toBe(true); - }); - - it('set allowInlineScripts false', () => { - userConfig.allowInlineScripts = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.allowInlineScripts).toBe(false); - }); - - it('default allowInlineScripts true', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.allowInlineScripts).toBe(true); - }); - }); - - describe('transformAliasedImportPaths', () => { - it.each([true, false])('set transformAliasedImportPaths %p', (bool) => { - userConfig.transformAliasedImportPaths = bool; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.transformAliasedImportPaths).toBe(bool); - }); - - it('defaults `transformAliasedImportPaths` to true', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.transformAliasedImportPaths).toBe(true); - }); - }); - - describe('suppressReservedPublicNameWarnings', () => { - it.each([true, false])('sets suppressReservedPublicNameWarnings to %p when provided', (bool) => { - userConfig.suppressReservedPublicNameWarnings = bool; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.suppressReservedPublicNameWarnings).toBe(bool); - }); - - it('defaults suppressReservedPublicNameWarnings to false', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.suppressReservedPublicNameWarnings).toBe(false); - }); - }); - - describe('enableCache', () => { - it('set enableCache true', () => { - userConfig.enableCache = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.enableCache).toBe(true); - }); - - it('set enableCache false', () => { - userConfig.enableCache = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.enableCache).toBe(false); - }); - - it('default enableCache true', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.enableCache).toBe(true); - }); - }); - - describe('buildAppCore', () => { - it('set buildAppCore true', () => { - userConfig.buildAppCore = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildAppCore).toBe(true); - }); - - it('set buildAppCore false', () => { - userConfig.buildAppCore = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildAppCore).toBe(false); - }); - - it('default buildAppCore true', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildAppCore).toBe(true); - }); - }); - - describe('es5 build', () => { - it('set buildEs5 false', () => { - userConfig.buildEs5 = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildEs5).toBe(false); - }); - - it('set buildEs5 true', () => { - userConfig.buildEs5 = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildEs5).toBe(true); - }); - - it('set buildEs5 true, dev mode', () => { - userConfig.devMode = true; - userConfig.buildEs5 = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildEs5).toBe(true); - }); - - it('prod mode, set modern and es5', () => { - userConfig.devMode = false; - userConfig.buildEs5 = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildEs5).toBe(true); - }); - - it('build es5 when set to "prod" and in prod', () => { - userConfig.devMode = false; - userConfig.buildEs5 = 'prod'; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildEs5).toBe(true); - }); - - it('do not build es5 when set to "prod" and in dev', () => { - userConfig.devMode = true; - userConfig.buildEs5 = 'prod'; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildEs5).toBe(false); - }); - - it('prod mode default to only modern and not es5', () => { - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildEs5).toBe(false); - }); - }); - - describe('hashed filenames', () => { - it('should error when hashedFileNameLength too large', () => { - userConfig.hashedFileNameLength = 33; - const validated = validateConfig(userConfig, bootstrapConfig); - expect(validated.diagnostics).toHaveLength(1); - }); - - it('should error when hashedFileNameLength too small', () => { - userConfig.hashedFileNameLength = 3; - const validated = validateConfig(userConfig, bootstrapConfig); - expect(validated.diagnostics).toHaveLength(1); - }); - - it('should set from hashedFileNameLength', () => { - userConfig.hashedFileNameLength = 28; - const validated = validateConfig(userConfig, bootstrapConfig); - expect(validated.config.hashedFileNameLength).toBe(28); - }); - - it('should set hashedFileNameLength', () => { - userConfig.hashedFileNameLength = 6; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.hashedFileNameLength).toBe(6); - }); - - it('should default hashedFileNameLength', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.hashedFileNameLength).toBe(8); - }); - - it('should default hashFileNames to false in watch mode despite prod mode', () => { - userConfig.watch = true; - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.hashFileNames).toBe(true); - }); - - it('should default hashFileNames to true in prod mode', () => { - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.hashFileNames).toBe(true); - }); - - it('should default hashFileNames to false in dev mode', () => { - userConfig.devMode = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.hashFileNames).toBe(false); - }); - - it.each([true, false])('should set hashFileNames when hashFileNames===%b', (hashFileNames) => { - userConfig.hashFileNames = hashFileNames; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.hashFileNames).toBe(hashFileNames); - }); - - it('should set hashFileNames from function', () => { - (userConfig as any).hashFileNames = () => { - return true; - }; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.hashFileNames).toBe(true); - }); - }); - - describe('minifyJs', () => { - it('should set minifyJs to true', () => { - userConfig.devMode = true; - userConfig.minifyJs = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.minifyJs).toBe(true); - }); - - it('should default minifyJs to true in prod mode', () => { - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.minifyJs).toBe(true); - }); - - it('should default minifyJs to false in dev mode', () => { - userConfig.devMode = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.minifyJs).toBe(false); - }); - }); - - describe('minifyCss', () => { - it('should set minifyCss to true', () => { - userConfig.devMode = true; - userConfig.minifyCss = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.minifyCss).toBe(true); - }); - - it('should default minifyCss to true in prod mode', () => { - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.minifyCss).toBe(true); - }); - - it('should default minifyCss to false in dev mode', () => { - userConfig.devMode = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.minifyCss).toBe(false); - }); - }); - - it('should default watch to false', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.watch).toBe(false); - }); - - it('should set devMode to false', () => { - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devMode).toBe(false); - }); - - it('should set devMode to true', () => { - userConfig.devMode = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devMode).toBe(true); - }); - - it('should default devMode to false', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devMode).toBe(false); - }); - - it.each([DOCS_JSON, DOCS_CUSTOM, DOCS_README, DOCS_VSCODE])( - 'should not add "%s" output target by default', - (targetType) => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.outputTargets.some((o) => o.type === targetType)).toBe(false); - }, - ); - - it('should set devInspector false', () => { - userConfig.devInspector = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devInspector).toBe(false); - }); - - it('should set devInspector true', () => { - userConfig.devInspector = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devInspector).toBe(true); - }); - - it('should default devInspector false when devMode is false', () => { - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devInspector).toBe(false); - }); - - it('should default devInspector true when devMode is true', () => { - userConfig.devMode = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.devInspector).toBe(true); - }); - - it('should default dist false and www true', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.outputTargets.some((o) => o.type === 'dist')).toBe(false); - expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); - }); - - it('should error for invalid outputTarget type', () => { - userConfig.outputTargets = [ - { - type: 'whatever', - } as any, - ]; - const validated = validateConfig(userConfig, bootstrapConfig); - expect(validated.diagnostics).toHaveLength(1); - }); - - it('should default outputTargets with www', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); - }); - - it('should set extras defaults', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.appendChildSlotFix).toBe(false); - expect(config.extras.cloneNodeFix).toBe(false); - expect(config.extras.lifecycleDOMEvents).toBe(false); - expect(config.extras.scriptDataOpts).toBe(false); - expect(config.extras.slotChildNodesFix).toBe(false); - expect(config.extras.initializeNextTick).toBe(false); - expect(config.extras.tagNameTransform).toBe(false); - expect(config.extras.additionalTagTransformers).toBe(false); - expect(config.extras.scopedSlotTextContentFix).toBe(false); - expect(config.extras.addGlobalStyleToComponents).toBe('client'); - expect(config.extras.additionalTagTransformers).toBe(false); - }); - - describe('extras.additionalTagTransformers', () => { - it('set extras.additionalTagTransformers false', () => { - userConfig.extras = { additionalTagTransformers: false }; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.additionalTagTransformers).toBe(false); - }); - - it('set extras.additionalTagTransformers true', () => { - userConfig.extras = { additionalTagTransformers: true }; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.additionalTagTransformers).toBe(true); - }); - - it('set extras.additionalTagTransformers true, dev mode', () => { - userConfig.devMode = true; - userConfig.extras = { additionalTagTransformers: true }; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.additionalTagTransformers).toBe(true); - }); - - it('prod mode, set extras.additionalTagTransformers', () => { - userConfig.devMode = false; - userConfig.extras = { additionalTagTransformers: true }; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.additionalTagTransformers).toBe(true); - }); - - it('build extras.additionalTagTransformers when set to "prod" and in prod', () => { - userConfig.devMode = false; - userConfig.extras = { additionalTagTransformers: 'prod' }; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.additionalTagTransformers).toBe(true); - }); - - it('do not build extras.additionalTagTransformers when set to "prod" and in dev', () => { - userConfig.devMode = true; - userConfig.extras = { additionalTagTransformers: 'prod' }; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.additionalTagTransformers).toBe(false); - }); - - it('prod mode default to only modern and not extras.additionalTagTransformers', () => { - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.additionalTagTransformers).toBe(false); - }); - }); - - it('should set slot config based on `experimentalSlotFixes`', () => { - userConfig.extras = {}; - userConfig.extras.experimentalSlotFixes = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.appendChildSlotFix).toBe(true); - expect(config.extras.cloneNodeFix).toBe(true); - expect(config.extras.slotChildNodesFix).toBe(true); - expect(config.extras.scopedSlotTextContentFix).toBe(true); - }); - - it('should override slot fix config based on `experimentalSlotFixes`', () => { - // This test is to verify the flags get overwritten correctly even if an - // invalid config is ingested. Hence, the `any` cast - userConfig.extras = { - appendChildSlotFix: false, - slotChildNodesFix: false, - cloneNodeFix: false, - scopedSlotTextContentFix: false, - experimentalSlotFixes: true, - } as any; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.appendChildSlotFix).toBe(true); - expect(config.extras.cloneNodeFix).toBe(true); - expect(config.extras.slotChildNodesFix).toBe(true); - expect(config.extras.scopedSlotTextContentFix).toBe(true); - }); - - it('should set extras experimentalScopedSlotChanges `true` if set in user config', () => { - userConfig.extras = { - experimentalScopedSlotChanges: true, - }; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.extras.experimentalScopedSlotChanges).toBe(true); - }); - - it('should set taskQueue "async" by default', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.taskQueue).toBe('async'); - }); - - it('should set taskQueue', () => { - userConfig.taskQueue = 'congestionAsync'; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.taskQueue).toBe('congestionAsync'); - }); - - it('empty watchIgnoredRegex, all valid', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.watchIgnoredRegex).toEqual([]); - expect(isWatchIgnorePath(config, '/some/image.gif')).toBe(false); - expect(isWatchIgnorePath(config, '/some/typescript.ts')).toBe(false); - }); - - it('should change a single watchIgnoredRegex to an array', () => { - userConfig.watchIgnoredRegex = /\.(gif|jpe?g|png)$/i; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.watchIgnoredRegex).toHaveLength(1); - expect((config.watchIgnoredRegex as any[])[0]).toEqual(/\.(gif|jpe?g|png)$/i); - expect(isWatchIgnorePath(config, '/some/image.gif')).toBe(true); - expect(isWatchIgnorePath(config, '/some/typescript.ts')).toBe(false); - }); - - it('should clean up valid watchIgnoredRegex', () => { - userConfig.watchIgnoredRegex = [/\.(gif|jpe?g)$/i, null, 'me-regex' as any, /\.(png)$/i]; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.watchIgnoredRegex).toHaveLength(2); - expect((config.watchIgnoredRegex as any[])[0]).toEqual(/\.(gif|jpe?g)$/i); - expect((config.watchIgnoredRegex as any[])[1]).toEqual(/\.(png)$/i); - expect(isWatchIgnorePath(config, '/some/image.gif')).toBe(true); - expect(isWatchIgnorePath(config, '/some/image.jpg')).toBe(true); - expect(isWatchIgnorePath(config, '/some/image.png')).toBe(true); - expect(isWatchIgnorePath(config, '/some/typescript.ts')).toBe(false); - }); - - describe('sourceMap', () => { - it('sets the field to true when the set to true in the config', () => { - userConfig.sourceMap = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.sourceMap).toBe(true); - }); - - it('sets the field to false when set to false in the config', () => { - userConfig.sourceMap = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.sourceMap).toBe(false); - }); - - it('defaults to "dev" behavior when not set (true in dev mode)', () => { - userConfig.devMode = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.sourceMap).toBe(true); - }); - - it('defaults to "dev" behavior when not set (false in prod mode)', () => { - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.sourceMap).toBe(false); - }); - - it('sets the field to true when set to "dev" and devMode is true', () => { - userConfig.sourceMap = 'dev'; - userConfig.devMode = true; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.sourceMap).toBe(true); - }); - - it('sets the field to false when set to "dev" and devMode is false', () => { - userConfig.sourceMap = 'dev'; - userConfig.devMode = false; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.sourceMap).toBe(false); - }); - - it('sets the field to true when set to "dev" and --dev flag is passed', () => { - userConfig.sourceMap = 'dev'; - userConfig.flags = createConfigFlags({ dev: true }); - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.sourceMap).toBe(true); - }); - - it('sets the field to false when set to "dev" and --prod flag is passed', () => { - userConfig.sourceMap = 'dev'; - userConfig.flags = createConfigFlags({ prod: true }); - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.sourceMap).toBe(false); - }); - }); - - describe('buildDist', () => { - it.each([true, false])('should set the field based on the config flag (%p)', (flag) => { - userConfig.flags = createConfigFlags({ esm: flag }); - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildDist).toBe(flag); - }); - - it.each([true, false])('should fallback to !devMode', (devMode) => { - userConfig.devMode = devMode; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildDist).toBe(!devMode); - }); - - it.each([true, false])('should fallback to buildEs5 in devMode', (buildEs5) => { - userConfig.devMode = true; - userConfig.buildEs5 = buildEs5; - const { config } = validateConfig(userConfig, bootstrapConfig); - expect(config.buildDist).toBe(config.buildEs5); - }); - }); - - describe('validatePrimaryPackageOutputTarget', () => { - it('should default to false', () => { - const { config } = validateConfig(userConfig, bootstrapConfig); - - expect(config.validatePrimaryPackageOutputTarget).toBe(false); - }); - - it.each([true, false])( - 'should set validatePrimaryPackageOutputTarget to %p', - (validatePrimaryPackageOutputTarget) => { - userConfig.validatePrimaryPackageOutputTarget = validatePrimaryPackageOutputTarget; - - const { config } = validateConfig(userConfig, bootstrapConfig); - - expect(config.validatePrimaryPackageOutputTarget).toBe(validatePrimaryPackageOutputTarget); - }, - ); - }); -}); diff --git a/src/compiler/config/test/validate-copy.spec.ts b/src/compiler/config/test/validate-copy.spec.ts deleted file mode 100644 index 4d69a64d663..00000000000 --- a/src/compiler/config/test/validate-copy.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type * as d from '../../../declarations'; -import { validateCopy } from '../validate-copy'; - -describe('validate-copy', () => { - describe('validateCopy', () => { - it.each([false, null, undefined, []])('returns an empty array when the copy task is `%s`', (copyValue) => { - expect(validateCopy(copyValue, [])).toEqual([]); - }); - - it('pushes default tasks not found in the original copy list', () => { - const defaultCopyTasks: d.CopyTask[] = [ - { src: 'defaultSrc' }, - { src: 'anotherDefaultSrc', dest: 'anotherDefaultDest' }, - ]; - - expect(validateCopy([], defaultCopyTasks)).toEqual(defaultCopyTasks); - }); - - it('combines provided and default tasks', () => { - const tasksToValidate: d.CopyTask[] = [{ src: 'someSrc', dest: 'someDest', keepDirStructure: true, warn: false }]; - const defaultCopyTasks: d.CopyTask[] = [ - { src: 'defaultSrc' }, - { src: 'anotherDefaultSrc', dest: 'anotherDefaultDest' }, - ]; - - expect(validateCopy(tasksToValidate, defaultCopyTasks)).toEqual([...tasksToValidate, ...defaultCopyTasks]); - }); - - it('prefers provided tasks over default tasks', () => { - const tasksToValidate: d.CopyTask[] = [ - { src: 'aDuplicateSrc', dest: 'someDest', keepDirStructure: true, warn: false }, - ]; - const defaultCopyTasks: d.CopyTask[] = [ - { src: 'aDuplicateSrc' }, - { src: 'anotherDefaultSrc', dest: 'anotherDefaultDest' }, - ]; - - // the first task from the default task list is not used - expect(validateCopy(tasksToValidate, defaultCopyTasks)).toEqual([ - { src: 'aDuplicateSrc', dest: 'someDest', keepDirStructure: true, warn: false }, - { src: 'anotherDefaultSrc', dest: 'anotherDefaultDest' }, - ]); - }); - - it('de-duplicates copy tasks', () => { - const copyTask: d.CopyTask = { src: 'aDuplicateSrc', dest: 'someDest', keepDirStructure: true, warn: false }; - const tasksToValidate: d.CopyTask[] = [{ ...copyTask }, { ...copyTask }]; - - expect(validateCopy(tasksToValidate, [])).toEqual([{ ...copyTask }]); - }); - }); -}); diff --git a/src/compiler/config/test/validate-custom.spec.ts b/src/compiler/config/test/validate-custom.spec.ts deleted file mode 100644 index 6a1e7c79769..00000000000 --- a/src/compiler/config/test/validate-custom.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; -import { buildWarn } from '@utils'; - -import { validateConfig } from '../validate-config'; - -describe('validateCustom', () => { - let userConfig: d.Config; - - beforeEach(() => { - userConfig = mockConfig(); - }); - - it('should log warning', () => { - userConfig.outputTargets = [ - { - type: 'custom', - name: 'test', - validate: (_, diagnostics) => { - const warn = buildWarn(diagnostics); - warn.messageText = 'test warning'; - }, - generator: async () => { - return; - }, - }, - ]; - const { diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); - // TODO(STENCIL-1107): Decrement the right-hand side value from 2 to 1 - expect(diagnostics.length).toBe(2); - // TODO(STENCIL-1107): Keep this assertion - expect(diagnostics[0]).toEqual({ - header: 'Build Warn', - level: 'warn', - lines: [], - messageText: 'test warning', - type: 'build', - }); - // TODO(STENCIL-1107): Remove this assertion - expect(diagnostics[1]).toEqual({ - header: 'Build Warn', - level: 'warn', - lines: [], - messageText: - 'nodeResolve.customResolveOptions is a deprecated option in a Stencil Configuration file. If you need this option, please open a new issue in the Stencil repository (https://github.com/stenciljs/core/issues/new/choose)', - type: 'build', - }); - }); -}); diff --git a/src/compiler/config/test/validate-namespace.spec.ts b/src/compiler/config/test/validate-namespace.spec.ts deleted file mode 100644 index 142c0dff205..00000000000 --- a/src/compiler/config/test/validate-namespace.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type * as d from '@stencil/core/declarations'; - -import { validateNamespace } from '../validate-namespace'; - -// TODO(STENCIL-968): Update tests to check diagnostic messages -describe('validateNamespace', () => { - const diagnostics: d.Diagnostic[] = []; - - beforeEach(() => { - diagnostics.length = 0; - }); - - it('should not allow special characters in namespace', () => { - validateNamespace('My/Namespace', undefined, diagnostics); - expect(diagnostics).toHaveLength(1); - - diagnostics.length = 0; - validateNamespace('My%20Namespace', undefined, diagnostics); - expect(diagnostics).toHaveLength(1); - - diagnostics.length = 0; - validateNamespace('My:Namespace', undefined, diagnostics); - expect(diagnostics).toHaveLength(1); - }); - - it('should not allow spaces in namespace', () => { - validateNamespace('My Namespace', undefined, diagnostics); - expect(diagnostics).toHaveLength(1); - }); - - it('should not allow dash for last character of namespace', () => { - validateNamespace('MyNamespace-', undefined, diagnostics); - expect(diagnostics).toHaveLength(1); - }); - - it('should not allow dash for first character of namespace', () => { - validateNamespace('-MyNamespace', undefined, diagnostics); - expect(diagnostics).toHaveLength(1); - }); - - it('should not allow number for first character of namespace', () => { - validateNamespace('88MyNamespace', undefined, diagnostics); - expect(diagnostics).toHaveLength(1); - }); - - it('should enforce namespace being at least 3 characters', () => { - validateNamespace('ab', undefined, diagnostics); - expect(diagnostics).toHaveLength(1); - }); - - it('should allow $ in the namespace', () => { - const { namespace, fsNamespace } = validateNamespace('$MyNamespace', undefined, diagnostics); - expect(namespace).toBe('$MyNamespace'); - expect(fsNamespace).toBe('$mynamespace'); - }); - - it('should allow underscore in the namespace', () => { - const { namespace, fsNamespace } = validateNamespace('My_Namespace', undefined, diagnostics); - expect(namespace).toBe('My_Namespace'); - expect(fsNamespace).toBe('my_namespace'); - }); - - it('should allow dash in the namespace', () => { - const { namespace, fsNamespace } = validateNamespace('My-Namespace', undefined, diagnostics); - expect(namespace).toBe('MyNamespace'); - expect(fsNamespace).toBe('my-namespace'); - }); - - it('should set user namespace', () => { - const { namespace, fsNamespace } = validateNamespace('MyNamespace', undefined, diagnostics); - expect(namespace).toBe('MyNamespace'); - expect(fsNamespace).toBe('mynamespace'); - }); - - it('should set default namespace', () => { - const { namespace, fsNamespace } = validateNamespace(undefined, undefined, diagnostics); - expect(namespace).toBe('App'); - expect(fsNamespace).toBe('app'); - }); -}); diff --git a/src/compiler/config/test/validate-output-dist-collection.spec.ts b/src/compiler/config/test/validate-output-dist-collection.spec.ts deleted file mode 100644 index d2f7082bddc..00000000000 --- a/src/compiler/config/test/validate-output-dist-collection.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; -import { join, resolve } from '@utils'; - -import { validateConfig } from '../validate-config'; - -describe('validateDistCollectionOutputTarget', () => { - let config: d.Config; - - const rootDir = resolve('/'); - const defaultDir = join(rootDir, 'dist', 'collection'); - - beforeEach(() => { - config = mockConfig(); - }); - - it('sets correct default values', () => { - const target: d.OutputTargetDistCollection = { - type: 'dist-collection', - empty: false, - dir: null, - collectionDir: null, - }; - config.outputTargets = [target]; - - const { config: validatedConfig } = validateConfig(config, mockLoadConfigInit()); - - expect(validatedConfig.outputTargets).toEqual([ - { - type: 'dist-collection', - empty: false, - dir: defaultDir, - collectionDir: null, - transformAliasedImportPaths: true, - }, - ]); - }); - - it('sets specified directory', () => { - const target: d.OutputTargetDistCollection = { - type: 'dist-collection', - empty: false, - dir: '/my-dist', - collectionDir: null, - }; - config.outputTargets = [target]; - - const { config: validatedConfig } = validateConfig(config, mockLoadConfigInit()); - - expect(validatedConfig.outputTargets).toEqual([ - { - type: 'dist-collection', - empty: false, - dir: '/my-dist', - collectionDir: null, - transformAliasedImportPaths: true, - }, - ]); - }); - - describe('transformAliasedImportPaths', () => { - it.each([false, true])( - "sets option '%s' when explicitly '%s' in config", - (transformAliasedImportPaths: boolean) => { - const target: d.OutputTargetDistCollection = { - type: 'dist-collection', - empty: false, - dir: null, - collectionDir: null, - transformAliasedImportPaths, - }; - config.outputTargets = [target]; - - const { config: validatedConfig } = validateConfig(config, mockLoadConfigInit()); - - expect(validatedConfig.outputTargets).toEqual([ - { - type: 'dist-collection', - empty: false, - dir: defaultDir, - collectionDir: null, - transformAliasedImportPaths, - }, - ]); - }, - ); - }); -}); diff --git a/src/compiler/config/test/validate-output-dist-custom-element.spec.ts b/src/compiler/config/test/validate-output-dist-custom-element.spec.ts deleted file mode 100644 index d72dddecfdf..00000000000 --- a/src/compiler/config/test/validate-output-dist-custom-element.spec.ts +++ /dev/null @@ -1,480 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; -import { COPY, DIST_CUSTOM_ELEMENTS, DIST_TYPES, join } from '@utils'; -import path from 'path'; - -import { validateConfig } from '../validate-config'; - -describe('validate-output-dist-custom-element', () => { - describe('validateCustomElement', () => { - // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) - const rootDir = path.resolve('/'); - const defaultDistDir = join(rootDir, 'dist', 'components'); - const distCustomElementsDir = 'my-dist-custom-elements'; - let userConfig: d.Config; - - beforeEach(() => { - userConfig = mockConfig(); - }); - - it('generates a default dist-custom-elements output target', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_TYPES, - dir: defaultDistDir, - typesDir: join(rootDir, 'dist', 'types'), - }, - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: true, - externalRuntime: true, - generateTypeDeclarations: true, - customElementsExportBehavior: 'default', - }, - ]); - }); - - it('uses a provided export behavior over the default value', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - customElementsExportBehavior: 'single-export-module', - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_TYPES, - dir: defaultDistDir, - typesDir: join(rootDir, 'dist', 'types'), - }, - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: true, - externalRuntime: true, - generateTypeDeclarations: true, - customElementsExportBehavior: 'single-export-module', - }, - ]); - }); - - it('uses the default export behavior if the specified value is invalid', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - customElementsExportBehavior: 'not-a-valid-option' as d.CustomElementsExportBehavior, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_TYPES, - dir: defaultDistDir, - typesDir: join(rootDir, 'dist', 'types'), - }, - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: true, - externalRuntime: true, - generateTypeDeclarations: true, - customElementsExportBehavior: 'default', - }, - ]); - }); - - it('uses a provided dir field over a default directory', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - dir: distCustomElementsDir, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: join(rootDir, distCustomElementsDir), - empty: true, - externalRuntime: true, - generateTypeDeclarations: false, - customElementsExportBehavior: 'default', - }, - ]); - }); - - describe('"empty" field', () => { - it('defaults the "empty" field to true if not provided', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - externalRuntime: false, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: true, - externalRuntime: false, - generateTypeDeclarations: false, - customElementsExportBehavior: 'default', - }, - ]); - }); - - it('defaults the "empty" field to true it\'s not a boolean', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - empty: undefined, - externalRuntime: false, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: true, - externalRuntime: false, - generateTypeDeclarations: false, - customElementsExportBehavior: 'default', - }, - ]); - }); - }); - - describe('"externalRuntime" field', () => { - it('defaults the "externalRuntime" field to true if not provided', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - empty: false, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: false, - externalRuntime: true, - generateTypeDeclarations: false, - customElementsExportBehavior: 'default', - }, - ]); - }); - - it('defaults the "externalRuntime" field to true it\'s not a boolean', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - empty: false, - externalRuntime: undefined, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: false, - externalRuntime: true, - generateTypeDeclarations: false, - customElementsExportBehavior: 'default', - }, - ]); - }); - }); - - describe('"generateTypeDeclarations" field', () => { - it('defaults the "generateTypeDeclarations" field to true if not provided', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - empty: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_TYPES, - dir: defaultDistDir, - typesDir: join(rootDir, 'dist', 'types'), - }, - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: false, - externalRuntime: true, - generateTypeDeclarations: true, - customElementsExportBehavior: 'default', - }, - ]); - }); - - it('defaults the "generateTypeDeclarations" field to true it\'s not a boolean', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - empty: false, - generateTypeDeclarations: undefined, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_TYPES, - dir: defaultDistDir, - typesDir: join(rootDir, 'dist', 'types'), - }, - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: false, - externalRuntime: true, - generateTypeDeclarations: true, - customElementsExportBehavior: 'default', - }, - ]); - }); - - it('creates a types directory when "generateTypeDeclarations" is true', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - empty: false, - externalRuntime: false, - generateTypeDeclarations: true, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_TYPES, - dir: defaultDistDir, - typesDir: join(rootDir, 'dist', 'types'), - }, - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: false, - externalRuntime: false, - generateTypeDeclarations: true, - customElementsExportBehavior: 'default', - }, - ]); - }); - - it('creates a types directory for a custom directory', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - dir: distCustomElementsDir, - empty: false, - externalRuntime: false, - generateTypeDeclarations: true, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_TYPES, - dir: join(rootDir, distCustomElementsDir), - typesDir: join(rootDir, 'dist', 'types'), - }, - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: join(rootDir, distCustomElementsDir), - empty: false, - externalRuntime: false, - generateTypeDeclarations: true, - customElementsExportBehavior: 'default', - }, - ]); - }); - - it('doesn\'t create a types directory when "generateTypeDeclarations" is false', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - empty: false, - externalRuntime: false, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: DIST_CUSTOM_ELEMENTS, - copy: [], - dir: defaultDistDir, - empty: false, - externalRuntime: false, - generateTypeDeclarations: false, - customElementsExportBehavior: 'default', - }, - ]); - }); - }); - - describe('copy tasks', () => { - it('copies existing copy tasks over to the output target', () => { - const copyOutputTarget: d.CopyTask = { - src: 'mock/src', - dest: 'mock/dest', - }; - const copyOutputTarget2: d.CopyTask = { - src: 'mock/src2', - dest: 'mock/dest2', - }; - - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - copy: [copyOutputTarget, copyOutputTarget2], - dir: distCustomElementsDir, - empty: false, - externalRuntime: false, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - type: COPY, - dir: rootDir, - copy: [copyOutputTarget, copyOutputTarget2], - }, - { - type: DIST_CUSTOM_ELEMENTS, - copy: [copyOutputTarget, copyOutputTarget2], - dir: join(rootDir, distCustomElementsDir), - empty: false, - externalRuntime: false, - generateTypeDeclarations: false, - customElementsExportBehavior: 'default', - }, - ]); - }); - }); - - describe('"autoLoader" field', () => { - it('normalizes autoLoader: true to an object with defaults', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - autoLoader: true, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const distCustomElementsTarget = config.outputTargets.find( - (o) => o.type === DIST_CUSTOM_ELEMENTS, - ) as d.OutputTargetDistCustomElements; - - expect(distCustomElementsTarget.autoLoader).toEqual({ - fileName: 'loader', - autoStart: true, - }); - }); - - it('normalizes autoLoader object with partial options', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - autoLoader: { fileName: 'my-loader' }, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const distCustomElementsTarget = config.outputTargets.find( - (o) => o.type === DIST_CUSTOM_ELEMENTS, - ) as d.OutputTargetDistCustomElements; - - expect(distCustomElementsTarget.autoLoader).toEqual({ - fileName: 'my-loader', - autoStart: true, - }); - }); - - it('normalizes autoLoader object with autoStart: false', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - autoLoader: { autoStart: false }, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const distCustomElementsTarget = config.outputTargets.find( - (o) => o.type === DIST_CUSTOM_ELEMENTS, - ) as d.OutputTargetDistCustomElements; - - expect(distCustomElementsTarget.autoLoader).toEqual({ - fileName: 'loader', - autoStart: false, - }); - }); - - it('does not set autoLoader when not provided', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const distCustomElementsTarget = config.outputTargets.find( - (o) => o.type === DIST_CUSTOM_ELEMENTS, - ) as d.OutputTargetDistCustomElements; - - expect(distCustomElementsTarget.autoLoader).toBeUndefined(); - }); - - it('does not set autoLoader when explicitly false', () => { - const outputTarget: d.OutputTargetDistCustomElements = { - type: DIST_CUSTOM_ELEMENTS, - autoLoader: false, - generateTypeDeclarations: false, - }; - userConfig.outputTargets = [outputTarget]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const distCustomElementsTarget = config.outputTargets.find( - (o) => o.type === DIST_CUSTOM_ELEMENTS, - ) as d.OutputTargetDistCustomElements; - - expect(distCustomElementsTarget.autoLoader).toBe(false); - }); - }); - }); -}); diff --git a/src/compiler/config/test/validate-output-dist.spec.ts b/src/compiler/config/test/validate-output-dist.spec.ts deleted file mode 100644 index 0d0274892f9..00000000000 --- a/src/compiler/config/test/validate-output-dist.spec.ts +++ /dev/null @@ -1,291 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; -import { join } from '@utils'; -import path from 'path'; - -import { validateConfig } from '../validate-config'; - -describe('validateDistOutputTarget', () => { - // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) - const rootDir = path.resolve('/'); - - let userConfig: d.Config; - beforeEach(() => { - userConfig = mockConfig({ fsNamespace: 'testing' }); - }); - - it('should set dist values', () => { - const outputTarget: d.OutputTargetDist = { - type: 'dist', - dir: 'my-dist', - buildDir: 'my-build', - empty: false, - }; - userConfig.outputTargets = [outputTarget]; - userConfig.buildDist = true; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toEqual([ - { - buildDir: join(rootDir, 'my-dist', 'my-build'), - collectionDir: join(rootDir, 'my-dist', 'collection'), - copy: [], - dir: join(rootDir, 'my-dist'), - empty: false, - esmLoaderPath: join(rootDir, 'my-dist', 'loader'), - type: 'dist', - polyfills: false, - typesDir: join(rootDir, 'my-dist', 'types'), - transformAliasedImportPathsInCollection: true, - isPrimaryPackageOutputTarget: false, - }, - { - esmDir: join(rootDir, 'my-dist', 'my-build', 'testing'), - empty: false, - isBrowserBuild: true, - legacyLoaderFile: join(rootDir, 'my-dist', 'my-build', 'testing.js'), - polyfills: true, - systemDir: undefined, - systemLoaderFile: undefined, - type: 'dist-lazy', - }, - { - copyAssets: 'dist', - copy: [], - dir: join(rootDir, 'my-dist', 'my-build', 'testing'), - type: 'copy', - }, - { - file: join(rootDir, 'my-dist', 'my-build', 'testing', 'testing.css'), - type: 'dist-global-styles', - }, - { - dir: join(rootDir, 'my-dist'), - type: 'dist-types', - typesDir: join(rootDir, 'my-dist', 'types'), - }, - { - collectionDir: join(rootDir, 'my-dist', 'collection'), - dir: join(rootDir, '/my-dist'), - empty: false, - transformAliasedImportPaths: true, - type: 'dist-collection', - }, - { - copy: [{ src: '**/*.svg' }, { src: '**/*.js' }], - copyAssets: 'collection', - dir: join(rootDir, 'my-dist', 'collection'), - type: 'copy', - }, - { - type: 'dist-lazy', - cjsDir: join(rootDir, 'my-dist', 'cjs'), - cjsIndexFile: join(rootDir, 'my-dist', 'index.cjs.js'), - empty: false, - esmDir: join(rootDir, 'my-dist', 'esm'), - esmEs5Dir: undefined, - esmIndexFile: join(rootDir, 'my-dist', 'index.js'), - polyfills: true, - }, - { - cjsDir: join(rootDir, 'my-dist', 'cjs'), - componentDts: join(rootDir, 'my-dist', 'types', 'components.d.ts'), - dir: join(rootDir, 'my-dist', 'loader'), - empty: false, - esmDir: join(rootDir, 'my-dist', 'esm'), - esmEs5Dir: undefined, - type: 'dist-lazy-loader', - }, - ]); - }); - - it('should set defaults when outputTargets dist is empty', () => { - userConfig.outputTargets = [{ type: 'dist' }]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const outputTarget = config.outputTargets.find((o) => o.type === 'dist') as d.OutputTargetDist; - expect(outputTarget).toBeDefined(); - expect(outputTarget.dir).toBe(join(rootDir, 'dist')); - expect(outputTarget.buildDir).toBe(join(rootDir, '/dist')); - expect(outputTarget.empty).toBe(true); - }); - - it('should default to not add dist when outputTargets exists, but without dist', () => { - userConfig.outputTargets = []; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets.some((o) => o.type === 'dist')).toBe(false); - }); - - it('sets option to transform aliased import paths when enabled', () => { - const outputTarget: d.OutputTargetDist = { - type: 'dist', - dir: 'my-dist', - buildDir: 'my-build', - empty: false, - transformAliasedImportPathsInCollection: true, - }; - userConfig.outputTargets = [outputTarget]; - userConfig.buildDist = true; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.outputTargets).toEqual([ - { - buildDir: join(rootDir, 'my-dist', 'my-build'), - collectionDir: join(rootDir, 'my-dist', 'collection'), - copy: [], - dir: join(rootDir, 'my-dist'), - empty: false, - esmLoaderPath: join(rootDir, 'my-dist', 'loader'), - type: 'dist', - polyfills: false, - typesDir: join(rootDir, 'my-dist', 'types'), - transformAliasedImportPathsInCollection: true, - isPrimaryPackageOutputTarget: false, - }, - { - esmDir: join(rootDir, 'my-dist', 'my-build', 'testing'), - empty: false, - isBrowserBuild: true, - legacyLoaderFile: join(rootDir, 'my-dist', 'my-build', 'testing.js'), - polyfills: true, - systemDir: undefined, - systemLoaderFile: undefined, - type: 'dist-lazy', - }, - { - copyAssets: 'dist', - copy: [], - dir: join(rootDir, 'my-dist', 'my-build', 'testing'), - type: 'copy', - }, - { - file: join(rootDir, 'my-dist', 'my-build', 'testing', 'testing.css'), - type: 'dist-global-styles', - }, - { - dir: join(rootDir, 'my-dist'), - type: 'dist-types', - typesDir: join(rootDir, 'my-dist', 'types'), - }, - { - collectionDir: join(rootDir, 'my-dist', 'collection'), - dir: join(rootDir, '/my-dist'), - empty: false, - transformAliasedImportPaths: true, - type: 'dist-collection', - }, - { - copy: [{ src: '**/*.svg' }, { src: '**/*.js' }], - copyAssets: 'collection', - dir: join(rootDir, 'my-dist', 'collection'), - type: 'copy', - }, - { - type: 'dist-lazy', - cjsDir: join(rootDir, 'my-dist', 'cjs'), - cjsIndexFile: join(rootDir, 'my-dist', 'index.cjs.js'), - empty: false, - esmDir: join(rootDir, 'my-dist', 'esm'), - esmEs5Dir: undefined, - esmIndexFile: join(rootDir, 'my-dist', 'index.js'), - polyfills: true, - }, - { - cjsDir: join(rootDir, 'my-dist', 'cjs'), - componentDts: join(rootDir, 'my-dist', 'types', 'components.d.ts'), - dir: join(rootDir, 'my-dist', 'loader'), - empty: false, - esmDir: join(rootDir, 'my-dist', 'esm'), - esmEs5Dir: undefined, - type: 'dist-lazy-loader', - }, - ]); - }); - - it('sets option to validate primary package output target when enabled', () => { - const outputTarget: d.OutputTargetDist = { - type: 'dist', - dir: 'my-dist', - buildDir: 'my-build', - empty: false, - isPrimaryPackageOutputTarget: true, - }; - userConfig.outputTargets = [outputTarget]; - userConfig.buildDist = true; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.outputTargets).toEqual([ - { - buildDir: join(rootDir, 'my-dist', 'my-build'), - collectionDir: join(rootDir, 'my-dist', 'collection'), - copy: [], - dir: join(rootDir, 'my-dist'), - empty: false, - esmLoaderPath: join(rootDir, 'my-dist', 'loader'), - type: 'dist', - polyfills: false, - typesDir: join(rootDir, 'my-dist', 'types'), - transformAliasedImportPathsInCollection: true, - isPrimaryPackageOutputTarget: true, - }, - { - esmDir: join(rootDir, 'my-dist', 'my-build', 'testing'), - empty: false, - isBrowserBuild: true, - legacyLoaderFile: join(rootDir, 'my-dist', 'my-build', 'testing.js'), - polyfills: true, - systemDir: undefined, - systemLoaderFile: undefined, - type: 'dist-lazy', - }, - { - copyAssets: 'dist', - copy: [], - dir: join(rootDir, 'my-dist', 'my-build', 'testing'), - type: 'copy', - }, - { - file: join(rootDir, 'my-dist', 'my-build', 'testing', 'testing.css'), - type: 'dist-global-styles', - }, - { - dir: join(rootDir, 'my-dist'), - type: 'dist-types', - typesDir: join(rootDir, 'my-dist', 'types'), - }, - { - collectionDir: join(rootDir, 'my-dist', 'collection'), - dir: join(rootDir, '/my-dist'), - empty: false, - transformAliasedImportPaths: true, - type: 'dist-collection', - }, - { - copy: [{ src: '**/*.svg' }, { src: '**/*.js' }], - copyAssets: 'collection', - dir: join(rootDir, 'my-dist', 'collection'), - type: 'copy', - }, - { - type: 'dist-lazy', - cjsDir: join(rootDir, 'my-dist', 'cjs'), - cjsIndexFile: join(rootDir, 'my-dist', 'index.cjs.js'), - empty: false, - esmDir: join(rootDir, 'my-dist', 'esm'), - esmEs5Dir: undefined, - esmIndexFile: join(rootDir, 'my-dist', 'index.js'), - polyfills: true, - }, - { - cjsDir: join(rootDir, 'my-dist', 'cjs'), - componentDts: join(rootDir, 'my-dist', 'types', 'components.d.ts'), - dir: join(rootDir, 'my-dist', 'loader'), - empty: false, - esmDir: join(rootDir, 'my-dist', 'esm'), - esmEs5Dir: undefined, - type: 'dist-lazy-loader', - }, - ]); - }); -}); diff --git a/src/compiler/config/test/validate-output-www.spec.ts b/src/compiler/config/test/validate-output-www.spec.ts deleted file mode 100644 index 330714a546f..00000000000 --- a/src/compiler/config/test/validate-output-www.spec.ts +++ /dev/null @@ -1,380 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockLoadConfigInit } from '@stencil/core/testing'; -import { isOutputTargetCopy, isOutputTargetHydrate, isOutputTargetWww, join } from '@utils'; -import path from 'path'; - -import { ConfigFlags, createConfigFlags } from '../../../cli/config-flags'; -import { validateConfig } from '../validate-config'; - -describe('validateOutputTargetWww', () => { - // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) - const rootDir = path.resolve('/'); - let userConfig: d.Config; - let flags: ConfigFlags; - - beforeEach(() => { - flags = createConfigFlags(); - userConfig = { - rootDir: rootDir, - flags, - }; - }); - - it('should have default value', () => { - const outputTarget: d.OutputTargetWww = { - type: 'www', - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - dir: path.join('www', 'docs'), - }; - userConfig.outputTargets = [outputTarget]; - userConfig.buildEs5 = false; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.outputTargets).toEqual([ - { - appDir: join(rootDir, 'www', 'docs'), - baseUrl: '/', - buildDir: join(rootDir, 'www', 'docs', 'build'), - dir: join(rootDir, 'www', 'docs'), - empty: true, - indexHtml: join(rootDir, 'www', 'docs', 'index.html'), - polyfills: true, - serviceWorker: { - dontCacheBustURLsMatching: /p-\w{8}/, - globDirectory: join(rootDir, 'www', 'docs'), - globIgnores: [ - '**/host.config.json', - '**/*.system.entry.js', - '**/*.system.js', - '**/app.js', - '**/app.esm.js', - '**/app.css', - ], - globPatterns: ['*.html', '**/*.{js,css,json}'], - swDest: join(rootDir, 'www', 'docs', 'sw.js'), - }, - type: 'www', - }, - { - dir: join(rootDir, 'www', 'docs', 'build'), - esmDir: join(rootDir, 'www', 'docs', 'build'), - isBrowserBuild: true, - polyfills: true, - systemDir: undefined, - systemLoaderFile: undefined, - type: 'dist-lazy', - }, - { - copyAssets: 'dist', - dir: join(rootDir, 'www', 'docs', 'build'), - type: 'copy', - }, - { - copy: [ - { - src: 'assets', - warn: false, - }, - { - src: 'manifest.json', - warn: false, - }, - ], - dir: join(rootDir, 'www', 'docs'), - type: 'copy', - }, - { - file: join(rootDir, 'www', 'docs', 'build', 'app.css'), - type: 'dist-global-styles', - }, - ]); - }); - - it('should www with sub directory', () => { - const outputTarget: d.OutputTargetWww = { - type: 'www', - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - dir: path.join('www', 'docs'), - }; - userConfig.outputTargets = [outputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; - - expect(www.dir).toBe(join(rootDir, 'www', 'docs')); - expect(www.appDir).toBe(join(rootDir, 'www', 'docs')); - expect(www.buildDir).toBe(join(rootDir, 'www', 'docs', 'build')); - expect(www.indexHtml).toBe(join(rootDir, 'www', 'docs', 'index.html')); - }); - - it('should set www values', () => { - const outputTarget: d.OutputTargetWww = { - type: 'www', - dir: 'my-www', - buildDir: 'my-build', - indexHtml: 'my-index.htm', - empty: false, - }; - userConfig.outputTargets = [outputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; - - expect(www.type).toBe('www'); - expect(www.dir).toBe(join(rootDir, 'my-www')); - expect(www.buildDir).toBe(join(rootDir, 'my-www', 'my-build')); - expect(www.indexHtml).toBe(join(rootDir, 'my-www', 'my-index.htm')); - expect(www.empty).toBe(false); - }); - - it('should default to add www when outputTargets is undefined', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets).toHaveLength(5); - - const outputTarget = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; - expect(outputTarget.dir).toBe(join(rootDir, 'www')); - expect(outputTarget.buildDir).toBe(join(rootDir, 'www', 'build')); - expect(outputTarget.indexHtml).toBe(join(rootDir, 'www', 'index.html')); - expect(outputTarget.empty).toBe(true); - }); - - describe('baseUrl', () => { - it('baseUrl does not end with / with dir set', () => { - const outputTarget: d.OutputTargetWww = { - type: 'www', - dir: 'my-www', - baseUrl: '/docs', - }; - userConfig.outputTargets = [outputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; - - expect(www.type).toBe('www'); - expect(www.dir).toBe(join(rootDir, 'my-www')); - expect(www.baseUrl).toBe('/docs/'); - expect(www.appDir).toBe(join(rootDir, 'my-www/docs')); - - expect(www.buildDir).toBe(join(rootDir, 'my-www', 'docs', 'build')); - expect(www.indexHtml).toBe(join(rootDir, 'my-www', 'docs', 'index.html')); - }); - - it('baseUrl does not end with /', () => { - const outputTarget: d.OutputTargetWww = { - type: 'www', - baseUrl: '/docs', - }; - userConfig.outputTargets = [outputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; - - expect(www.type).toBe('www'); - expect(www.dir).toBe(join(rootDir, 'www')); - expect(www.baseUrl).toBe('/docs/'); - expect(www.appDir).toBe(join(rootDir, 'www/docs')); - - expect(www.buildDir).toBe(join(rootDir, 'www', 'docs', 'build')); - expect(www.indexHtml).toBe(join(rootDir, 'www', 'docs', 'index.html')); - }); - - it('baseUrl is a full url', () => { - const outputTarget: d.OutputTargetWww = { - type: 'www', - baseUrl: 'https://example.com/docs', - }; - userConfig.outputTargets = [outputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; - - expect(www.type).toBe('www'); - expect(www.dir).toBe(join(rootDir, 'www')); - expect(www.baseUrl).toBe('https://example.com/docs/'); - expect(www.appDir).toBe(join(rootDir, 'www/docs')); - - expect(www.buildDir).toBe(join(rootDir, 'www', 'docs', 'build')); - expect(www.indexHtml).toBe(join(rootDir, 'www', 'docs', 'index.html')); - }); - }); - - describe('copy', () => { - it('should add copy tasks', () => { - const outputTarget: d.OutputTargetWww = { - type: 'www', - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - dir: path.join('www', 'docs'), - copy: [ - { - src: 'index-modules.html', - dest: 'index-2.html', - }, - ], - }; - userConfig.outputTargets = [outputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - const copyTargets = config.outputTargets.filter(isOutputTargetCopy); - expect(copyTargets).toEqual([ - { - copyAssets: 'dist', - dir: join(rootDir, 'www', 'docs', 'build'), - type: 'copy', - }, - { - copy: [ - { - dest: 'index-2.html', - src: 'index-modules.html', - }, - { - src: 'assets', - warn: false, - }, - { - src: 'manifest.json', - warn: false, - }, - ], - dir: join(rootDir, 'www', 'docs'), - type: 'copy', - }, - ]); - }); - - it('should replace copy tasks', () => { - const outputTarget: d.OutputTargetWww = { - type: 'www', - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - dir: path.join('www', 'docs'), - copy: [ - { - src: 'assets', - dest: 'assets2', - }, - ], - }; - userConfig.outputTargets = [outputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - const copyTargets = config.outputTargets.filter(isOutputTargetCopy); - expect(copyTargets).toEqual([ - { - copyAssets: 'dist', - dir: join(rootDir, 'www', 'docs', 'build'), - type: 'copy', - }, - { - copy: [ - { - dest: 'assets2', - src: 'assets', - }, - { - src: 'manifest.json', - warn: false, - }, - ], - dir: join(rootDir, 'www', 'docs'), - type: 'copy', - }, - ]); - }); - - it('should disable copy tasks', () => { - const outputTarget: d.OutputTargetWww = { - type: 'www', - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - dir: path.join('www', 'docs'), - copy: null, - }; - userConfig.outputTargets = [outputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - const copyTargets = config.outputTargets.filter(isOutputTargetCopy); - expect(copyTargets).toEqual([ - { - copyAssets: 'dist', - dir: join(rootDir, 'www', 'docs', 'build'), - type: 'copy', - }, - { - copy: [], - dir: join(rootDir, 'www', 'docs'), - type: 'copy', - }, - ]); - }); - }); - - describe('dist-hydrate-script', () => { - it('should not add hydrate by default', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(false); - expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); - }); - - it('should not add hydrate with user www', () => { - const wwwOutputTarget: d.OutputTargetWww = { - type: 'www', - }; - userConfig.outputTargets = [wwwOutputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(false); - expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); - }); - - it('should add hydrate with user hydrate and www outputs', () => { - const wwwOutputTarget: d.OutputTargetWww = { - type: 'www', - }; - const hydrateOutputTarget: d.OutputTargetHydrate = { - type: 'dist-hydrate-script', - }; - userConfig.outputTargets = [wwwOutputTarget, hydrateOutputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(true); - expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); - }); - - it('should add hydrate with --prerender flag', () => { - userConfig.flags = { ...flags, prerender: true }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(true); - expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); - }); - - it('should add hydrate with --ssr flag', () => { - userConfig.flags = { ...flags, ssr: true }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(true); - expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); - }); - - it('should add externals and defaults', () => { - const hydrateOutputTarget: d.OutputTargetHydrate = { - type: 'dist-hydrate-script', - external: ['lodash', 'left-pad'], - }; - userConfig.outputTargets = [hydrateOutputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const o = config.outputTargets.find(isOutputTargetHydrate) as d.OutputTargetHydrate; - expect(o.external).toContain('lodash'); - expect(o.external).toContain('left-pad'); - expect(o.external).toContain('fs'); - expect(o.external).toContain('path'); - expect(o.external).toContain('crypto'); - }); - - it('should add node builtins to external by default', () => { - userConfig.flags = { ...flags, prerender: true }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const o = config.outputTargets.find(isOutputTargetHydrate) as d.OutputTargetHydrate; - expect(o.external).toContain('fs'); - expect(o.external).toContain('path'); - expect(o.external).toContain('crypto'); - }); - }); -}); diff --git a/src/compiler/config/test/validate-paths.spec.ts b/src/compiler/config/test/validate-paths.spec.ts deleted file mode 100644 index 666071c7143..00000000000 --- a/src/compiler/config/test/validate-paths.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockCompilerSystem, mockLoadConfigInit, mockLogger } from '@stencil/core/testing'; -import { join } from '@utils'; -import path from 'path'; - -import { validateConfig } from '../validate-config'; - -describe('validatePaths', () => { - let userConfig: d.Config; - const logger = mockLogger(); - const sys = mockCompilerSystem(); - - // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) - const ROOT = path.resolve('/'); - - beforeEach(() => { - userConfig = { - sys: sys as any, - logger: logger, - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - rootDir: path.join(ROOT, 'User', 'my-app'), - namespace: 'Testing', - }; - }); - - it('should set absolute cacheDir', () => { - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - userConfig.cacheDir = path.join(ROOT, 'some', 'custom', 'cache'); - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.cacheDir).toBe(join(ROOT, 'some', 'custom', 'cache')); - }); - - it('should set relative cacheDir', () => { - userConfig.cacheDir = 'custom-cache'; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.cacheDir).toBe(join(ROOT, 'User', 'my-app', 'custom-cache')); - }); - - it('should set default cacheDir', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.cacheDir).toBe(join(ROOT, 'User', 'my-app', '.stencil')); - }); - - it('should set default wwwIndexHtml and convert to absolute path', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(path.basename((config.outputTargets as d.OutputTargetWww[])[0].indexHtml!)).toBe('index.html'); - expect(path.isAbsolute((config.outputTargets as d.OutputTargetWww[])[0].indexHtml!)).toBe(true); - }); - - it('should convert a custom wwwIndexHtml to absolute path', () => { - userConfig.outputTargets = [ - { - type: 'www', - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - indexHtml: path.join('assets', 'custom-index.html'), - }, - ] as d.OutputTargetWww[]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(path.basename((config.outputTargets as d.OutputTargetWww[])[0].indexHtml!)).toBe('custom-index.html'); - expect(path.isAbsolute((config.outputTargets as d.OutputTargetWww[])[0].indexHtml!)).toBe(true); - }); - - it('should set default indexHtmlSrc and convert to absolute path', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(path.basename(config.srcIndexHtml)).toBe('index.html'); - expect(path.isAbsolute(config.srcIndexHtml)).toBe(true); - }); - - it('should set emptyDist to false', () => { - userConfig.outputTargets = [ - { - type: 'www', - empty: false, - }, - ] as d.OutputTargetWww[]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect((config.outputTargets as d.OutputTargetWww[])[0].empty).toBe(false); - }); - - it('should set default emptyWWW to true', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect((config.outputTargets as d.OutputTargetWww[])[0].empty).toBe(true); - }); - - it('should set emptyWWW to false', () => { - userConfig.outputTargets = [ - { - type: 'www', - empty: false, - }, - ] as d.OutputTargetWww[]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect((config.outputTargets as d.OutputTargetWww[])[0].empty).toBe(false); - }); - - it('should set default collection dir and convert to absolute path', () => { - userConfig.outputTargets = [ - { - type: 'dist', - }, - ]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(path.basename((config.outputTargets as d.OutputTargetDist[])[0].collectionDir!)).toBe('collection'); - expect(path.isAbsolute((config.outputTargets as d.OutputTargetDist[])[0].collectionDir!)).toBe(true); - }); - - it('should set default types dir and convert to absolute path', () => { - userConfig.outputTargets = [ - { - type: 'dist', - }, - ]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(path.basename((config.outputTargets as d.OutputTargetDist[])[0].typesDir!)).toBe('types'); - expect(path.isAbsolute((config.outputTargets as d.OutputTargetDist[])[0].typesDir!)).toBe(true); - }); - - it('should set default build dir and convert to absolute path', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - // the path will be normalized by Stencil us use '/', split on that regardless of platform - const parts = (config.outputTargets as d.OutputTargetDist[])[0].buildDir!.split('/'); - expect(parts[parts.length - 1]).toBe('build'); - expect(parts[parts.length - 2]).toBe('www'); - expect(path.isAbsolute((config.outputTargets as d.OutputTargetDist[])[0].buildDir!)).toBe(true); - }); - - it('should set build dir w/ custom www', () => { - userConfig.outputTargets = [ - { - type: 'www', - dir: 'custom-www', - }, - ] as d.OutputTargetWww[]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - // the path will be normalized by Stencil us use '/', split on that regardless of platform - const parts = (config.outputTargets as d.OutputTargetDist[])[0].buildDir!.split('/'); - expect(parts[parts.length - 1]).toBe('build'); - expect(parts[parts.length - 2]).toBe('custom-www'); - expect(path.isAbsolute((config.outputTargets as d.OutputTargetDist[])[0].buildDir!)).toBe(true); - }); - - it('should set default src dir and convert to absolute path', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(path.basename(config.srcDir)).toBe('src'); - expect(path.isAbsolute(config.srcDir)).toBe(true); - }); - - it('should set src dir and convert to absolute path', () => { - userConfig.srcDir = 'app'; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(path.basename(config.srcDir)).toBe('app'); - expect(path.isAbsolute(config.srcDir)).toBe(true); - }); - - it('should convert globalScript to absolute path, if a globalScript property was provided', () => { - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - userConfig.globalScript = path.join('src', 'global', 'index.ts'); - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(path.basename(config.globalScript!)).toBe('index.ts'); - expect(path.isAbsolute(config.globalScript!)).toBe(true); - }); - - it('should convert globalStyle string to absolute path array, if a globalStyle property was provided', () => { - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - userConfig.globalStyle = path.join('src', 'global', 'styles.css'); - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(path.basename(config.globalStyle!)).toBe('styles.css'); - expect(path.isAbsolute(config.globalStyle!)).toBe(true); - }); -}); diff --git a/src/compiler/config/test/validate-rollup-config.spec.ts b/src/compiler/config/test/validate-rollup-config.spec.ts deleted file mode 100644 index 61c298282b7..00000000000 --- a/src/compiler/config/test/validate-rollup-config.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type * as d from '@stencil/core/declarations'; - -import { validateRollupConfig } from '../validate-rollup-config'; - -describe('validateStats', () => { - let config: d.Config; - - beforeEach(() => { - config = {}; - }); - - it('should use default if no config provided', () => { - const rollupConfig = validateRollupConfig(config); - expect(rollupConfig).toEqual({ - inputOptions: {}, - outputOptions: {}, - }); - }); - - it('should set based on inputOptions if provided', () => { - config.rollupConfig = { - inputOptions: { - context: 'window', - }, - }; - const rollupConfig = validateRollupConfig(config); - expect(rollupConfig).toEqual({ - inputOptions: { - context: 'window', - }, - outputOptions: {}, - }); - }); - - it('should use default if inputOptions is not provided but outputOptions is', () => { - config.rollupConfig = { - outputOptions: { - globals: { - jquery: '$', - }, - }, - }; - - const rollupConfig = validateRollupConfig(config); - expect(rollupConfig).toEqual({ - inputOptions: {}, - outputOptions: { - globals: { - jquery: '$', - }, - }, - }); - }); - - it('should pass all valid config data through and not those that are extraneous', () => { - config.rollupConfig = { - inputOptions: { - context: 'window', - external: 'external_symbol', - notAnOption: {}, - }, - outputOptions: { - globals: { - jquery: '$', - }, - }, - } as d.RollupConfig; - - const rollupConfig = validateRollupConfig(config); - expect(rollupConfig).toEqual({ - inputOptions: { - context: 'window', - external: 'external_symbol', - }, - outputOptions: { - globals: { - jquery: '$', - }, - }, - }); - }); -}); diff --git a/src/compiler/config/test/validate-stats.spec.ts b/src/compiler/config/test/validate-stats.spec.ts deleted file mode 100644 index 2c2eaa90a56..00000000000 --- a/src/compiler/config/test/validate-stats.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; - -import { validateConfig } from '../validate-config'; - -describe('validateStats', () => { - let userConfig: d.Config; - - beforeEach(() => { - userConfig = mockConfig(); - }); - - it('adds stats from flags, w/ no outputTargets', () => { - // the flags field is expected to have been set by the mock creation function for unvalidated configs, hence the - // bang operator - userConfig.flags!.stats = true; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; - expect(o).toBeDefined(); - expect(o.file).toContain('stencil-stats.json'); - }); - - it('uses stats config, custom path', () => { - userConfig.outputTargets = [ - { - type: 'stats', - file: 'custom-path.json', - } as d.OutputTargetStats, - ]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; - expect(o).toBeDefined(); - expect(o.file).toContain('custom-path.json'); - }); - - it('uses stats config, defaults file', () => { - userConfig.outputTargets = [ - { - type: 'stats', - }, - ]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; - expect(o).toBeDefined(); - expect(o.file).toContain('stencil-stats.json'); - }); - - it('default no stats', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.outputTargets.some((o) => o.type === 'stats')).toBe(false); - }); - - it('adds stats from flags with custom path string', () => { - // Test --stats dist/stats.json behavior - userConfig.flags!.stats = 'dist/custom-stats.json'; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; - expect(o).toBeDefined(); - expect(o.file).toContain('dist/custom-stats.json'); - }); - - it('adds stats from flags with custom path (absolute)', () => { - // Test --stats /tmp/stats.json behavior - userConfig.flags!.stats = '/tmp/absolute-stats.json'; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; - expect(o).toBeDefined(); - expect(o.file).toBe('/tmp/absolute-stats.json'); - }); - - it('flags stats path takes precedence over default when no outputTarget', () => { - // When --stats has a path, it should be used instead of the default - userConfig.flags!.stats = 'custom-location/stats.json'; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; - expect(o).toBeDefined(); - expect(o.file).toContain('custom-location/stats.json'); - expect(o.file).not.toContain('stencil-stats.json'); - }); - - it('does not override existing stats outputTarget when flag has path', () => { - // When there's already a stats outputTarget in config, flag should not add another - userConfig.outputTargets = [ - { - type: 'stats', - file: 'config-defined.json', - } as d.OutputTargetStats, - ]; - userConfig.flags!.stats = 'flag-defined.json'; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const statsTargets = config.outputTargets.filter((o) => o.type === 'stats'); - expect(statsTargets.length).toBe(1); - expect(statsTargets[0].file).toContain('config-defined.json'); - }); -}); diff --git a/src/compiler/config/test/validate-testing.spec.ts b/src/compiler/config/test/validate-testing.spec.ts deleted file mode 100644 index 565ec1f0ea2..00000000000 --- a/src/compiler/config/test/validate-testing.spec.ts +++ /dev/null @@ -1,941 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockCompilerSystem, mockLoadConfigInit, mockLogger } from '@stencil/core/testing'; -import { join } from '@utils'; -import path from 'path'; - -import { ConfigFlags, createConfigFlags } from '../../../cli/config-flags'; -import { validateConfig } from '../validate-config'; - -describe('validateTesting', () => { - // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) - const ROOT = path.resolve('/'); - const sys = mockCompilerSystem(); - const logger = mockLogger(); - let userConfig: d.Config; - let flags: ConfigFlags; - - beforeEach(() => { - flags = createConfigFlags(); - userConfig = { - sys: sys as any, - logger: logger, - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - rootDir: path.join(ROOT, 'User', 'some', 'path'), - srcDir: path.join(ROOT, 'User', 'some', 'path', 'src'), - flags, - namespace: 'Testing', - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - configPath: path.join(ROOT, 'User', 'some', 'path', 'stencil.config.ts'), - }; - userConfig.outputTargets = [ - { - type: 'www', - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - dir: path.join(ROOT, 'www'), - } as any as d.OutputTargetStats, - ]; - }); - - describe('no testing flags', () => { - it('returns an empty testing config when no testing config nor testing flags are provided', () => { - userConfig.flags = { ...flags, e2e: false, spec: false }; - delete userConfig.testing; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing).toEqual({}); - }); - - it('returns the provided testing config when neither testing flag is provided', () => { - const testingConfig: d.TestingConfig = { - bail: false, - }; - userConfig.flags = { ...flags, e2e: false, spec: false }; - userConfig.testing = testingConfig; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing).toEqual(testingConfig); - }); - }); - - describe('browserHeadless', () => { - const originalCI = process.env.CI; - beforeEach(() => { - delete process.env.CI; - }); - - afterEach(() => { - process.env.CI = originalCI; - }); - - describe("using 'headless' value from cli", () => { - it.each([false, 'shell'])('sets browserHeadless to %s', (headless) => { - userConfig.flags = { ...flags, e2e: true, headless }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe(headless); - }); - - it('throws if browser headless is set to deprecated value `true`', () => { - userConfig.flags = { ...flags, e2e: true, headless: true }; - expect(() => validateConfig(userConfig, mockLoadConfigInit())).toThrow( - 'Setting "browserHeadless" config to `true` is not supported anymore, please set it to "shell"!', - ); - }); - - it('defaults to "shell" outside of CI', () => { - userConfig.flags = { ...flags, e2e: true }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe('shell'); - }); - }); - - describe('with ci enabled', () => { - it("forces using the shell headless mode when 'headless: false'", () => { - userConfig.flags = { ...flags, ci: true, e2e: true, headless: false }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe('shell'); - }); - - it('allows the shell headless mode to be used', () => { - userConfig.flags = { ...flags, ci: true, e2e: true, headless: 'shell' }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe('shell'); - }); - }); - - describe('`testing` configuration', () => { - beforeEach(() => { - userConfig.flags = { ...flags, e2e: true, headless: undefined }; - }); - - it.each([false, 'shell'])( - 'uses %s browserHeadless mode from testing config', - (browserHeadlessValue) => { - userConfig.testing = { browserHeadless: browserHeadlessValue }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe(browserHeadlessValue); - }, - ); - - it('throws if browser headless is set to deprecated value `true`', () => { - userConfig.testing = { browserHeadless: true }; - expect(() => validateConfig(userConfig, mockLoadConfigInit())).toThrow( - 'Setting "browserHeadless" config to `true` is not supported anymore, please set it to "shell"!', - ); - }); - - it('defaults the headless mode to "shell" when browserHeadless is not provided', () => { - userConfig.testing = {}; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe('shell'); - }); - }); - }); - - describe('devTools', () => { - const originalCI = process.env.CI; - beforeEach(() => { - delete process.env.CI; - }); - - afterEach(() => { - process.env.CI = originalCI; - }); - - it('ignores devTools settings if CI is enabled', () => { - userConfig.flags = { ...flags, ci: true, devtools: true, e2e: true }; - userConfig.testing = {}; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.browserDevtools).toBeUndefined(); - }); - - it('sets browserDevTools to true when the devtools flag is set', () => { - userConfig.flags = { ...flags, devtools: true, e2e: true }; - userConfig.testing = {}; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.browserDevtools).toBe(true); - // browserHeadless must be false to enabled dev tools (which are headful by definition) - expect(config.testing.browserHeadless).toBe(false); - }); - - it("sets browserDevTools to true when set in a project's config", () => { - userConfig.flags = { ...flags, devtools: false, e2e: true }; - userConfig.testing = { browserDevtools: true }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.browserDevtools).toBe(true); - // browserHeadless must be false to enabled dev tools (which are headful by definition) - expect(config.testing.browserHeadless).toBe(false); - }); - }); - - describe('browserWaitUntil', () => { - it('sets the default to "load" if no value is provided', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = {}; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.browserWaitUntil).toBe('load'); - }); - - it('does not override a provided value', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - browserWaitUntil: 'domcontentloaded', - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.browserWaitUntil).toBe('domcontentloaded'); - }); - }); - - describe('browserArgs', () => { - const originalCI = process.env.CI; - beforeEach(() => { - delete process.env.CI; - }); - - afterEach(() => { - process.env.CI = originalCI; - }); - - it('does not add duplicate default fields', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - browserArgs: ['--unique', '--font-render-hinting=medium'], - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.browserArgs).toEqual(['--unique', '--font-render-hinting=medium', '--incognito']); - }); - - describe('adds default browser args', () => { - const originalCI = process.env.CI; - - beforeAll(() => { - delete process.env.CI; - }); - - afterAll(() => { - process.env.CI = originalCI; - }); - - it('adds default browser args when not in CI', () => { - userConfig.flags = { ...flags, e2e: true }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.browserArgs).toEqual(['--font-render-hinting=medium', '--incognito']); - }); - }); - - it("adds additional browser args when the 'ci' flag is set", () => { - userConfig.flags = { ...flags, ci: true, e2e: true }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserArgs).toEqual([ - '--font-render-hinting=medium', - '--incognito', - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - ]); - }); - - describe('adds additional browser args when process.env.CI is set', () => { - const originalCI = process.env.CI; - beforeAll(() => { - process.env.CI = 'true'; - }); - - afterAll(() => { - process.env.CI = originalCI; - }); - - it('adds default browser args when CI is set', () => { - userConfig.flags = { ...flags, ci: true, e2e: true }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserArgs).toEqual([ - '--font-render-hinting=medium', - '--incognito', - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - ]); - }); - }); - }); - - describe('browserArgs in CI', () => { - const originalCI = process.env.CI; - beforeEach(() => { - process.env.CI = 'true'; - }); - - afterEach(() => { - process.env.CI = originalCI; - }); - - it("adds additional browser args when 'CI' environment variable is set", () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - browserArgs: ['--unique', '--font-render-hinting=medium'], - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.browserArgs).toEqual([ - '--unique', - '--font-render-hinting=medium', - '--incognito', - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - ]); - }); - }); - - describe('screenshotConnector', () => { - it('assigns the screenshotConnector value from the provided flags', () => { - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - userConfig.flags = { ...flags, e2e: true, screenshotConnector: path.join(ROOT, 'mock', 'path') }; - userConfig.testing = { screenshotConnector: path.join(ROOT, 'another', 'mock', 'path') }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.screenshotConnector).toBe(join(ROOT, 'mock', 'path')); - }); - - it("uses the config's root dir to make the screenshotConnector path absolute", () => { - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - userConfig.flags = { ...flags, e2e: true, screenshotConnector: path.join('mock', 'path') }; - userConfig.testing = { screenshotConnector: path.join('another', 'mock', 'path') }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.screenshotConnector).toBe(join(ROOT, 'User', 'some', 'path', 'mock', 'path')); - }); - - it('sets screenshotConnector if a non-string is provided', () => { - userConfig.flags = { ...flags, e2e: true }; - // the nature of this test is to evaluate a non-string, hence the type assertion - userConfig.testing = { screenshotConnector: true as unknown as string }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.screenshotConnector).toBe(join('screenshot', 'local-connector.js')); - }); - }); - - describe('screenshotTimeout', () => { - it('sets screenshotTimeout to null if not provided', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = {}; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.screenshotTimeout).toEqual(null); - }); - - it('sets screenshotTimeout to null if it has an unexpected value', () => { - userConfig.flags = { ...flags, e2e: true }; - // @ts-expect-error - the nature of this test requires a non-string value - userConfig.testing = { screenshotTimeout: '4s' }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.screenshotTimeout).toEqual(null); - }); - - it('keeps the value if set correctly', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { screenshotTimeout: 4000 }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.screenshotTimeout).toEqual(4000); - }); - }); - - describe('testPathIgnorePatterns', () => { - it('does not alter a provided testPathIgnorePatterns', () => { - userConfig.flags = { ...flags, e2e: true }; - - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - const mockPath1 = path.join('this', 'is', 'a', 'mock', 'path'); - const mockPath2 = path.join('this', 'is', 'another', 'mock', 'path'); - userConfig.testing = { testPathIgnorePatterns: [mockPath1, mockPath2] }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.testPathIgnorePatterns).toEqual([mockPath1, mockPath2]); - }); - - it('sets the default testPathIgnorePatterns if no array is provided', () => { - userConfig.flags = { ...flags, e2e: true }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.testPathIgnorePatterns).toEqual([ - join(ROOT, 'User', 'some', 'path', '.vscode'), - join(ROOT, 'User', 'some', 'path', '.stencil'), - join(ROOT, 'User', 'some', 'path', 'node_modules'), - // use Node's join() here as the normalization process doesn't necessarily occur for this field - path.join(ROOT, 'www'), - ]); - }); - - it('sets the default testPathIgnorePatterns with custom outputTargets', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.outputTargets = [ - { type: 'dist', dir: 'dist-folder' }, - { type: 'www', dir: 'www-folder' }, - { type: 'docs-readme', dir: 'docs' }, - ]; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.testPathIgnorePatterns).toEqual([ - join(ROOT, 'User', 'some', 'path', '.vscode'), - join(ROOT, 'User', 'some', 'path', '.stencil'), - join(ROOT, 'User', 'some', 'path', 'node_modules'), - join(ROOT, 'User', 'some', 'path', 'www-folder'), - join(ROOT, 'User', 'some', 'path', 'dist-folder'), - ]); - }); - }); - - describe('preset', () => { - it.each([null, true])("uses stencil's default preset if a non-string (%s) is provided", (nonStringPreset) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - // the nature of this test requires a non-string value, hence the type assertion - preset: nonStringPreset as unknown as string, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - // 'testing' is the internal directory where `jest-preset.js` can be found - expect(config.testing.preset).toEqual('testing'); - }); - - it('forces a provided preset path to be absolute', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - preset: path.join('mock', 'path'), - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.preset).toEqual(join(ROOT, 'User', 'some', 'path', 'mock', 'path')); - }); - - it('does not change an already absolute preset path', () => { - userConfig.flags = { ...flags, e2e: true }; - - // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these - // tests are run on) for their input - const presetPath = path.join(ROOT, 'mock', 'path'); - userConfig.testing = { - preset: presetPath, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - // per the test name, we should not change an already absolute path - assert against the preset path that was - // generated using Node's join() - expect(config.testing.preset).toEqual(presetPath); - }); - }); - - describe('setupFilesAfterEnv', () => { - it.each([null, true])('forces a non-array (%s) of setup files to a default', (nonSetupFiles) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - // the nature of this test requires a non-string value, hence the type assertion - setupFilesAfterEnv: nonSetupFiles as unknown as string[], - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - // 'testing' is the internal directory where the default setup file can be found - expect(config.testing.setupFilesAfterEnv).toEqual([join('testing', 'jest-setuptestframework.js')]); - }); - - it.each([[[]], [['mock-setup-file.js']]])( - "prepends stencil's default file to an array: %s", - (setupFilesAfterEnv) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - setupFilesAfterEnv: [...setupFilesAfterEnv], - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.setupFilesAfterEnv).toEqual([ - // 'testing' is the internal directory where the default setup file can be found - join('testing', 'jest-setuptestframework.js'), - ...setupFilesAfterEnv, - ]); - }, - ); - }); - - describe('testEnvironment', () => { - it('sets a relative testEnvironment to absolute', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - testEnvironment: './rel-path.js', - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(path.isAbsolute(config.testing.testEnvironment)).toBe(true); - expect(path.basename(config.testing.testEnvironment)).toEqual('rel-path.js'); - }); - - it('allows a node module testEnvironment', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - testEnvironment: 'jsdom', - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.testEnvironment).toEqual('jsdom'); - }); - - it('does nothing for an empty testEnvironment', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.testEnvironment).toBeUndefined(); - }); - }); - - describe('allowableMismatchedPixels', () => { - it.each([0, 123])('does nothing is a non-negative number (%s) is provided', (pixelCount) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - allowableMismatchedPixels: pixelCount, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.allowableMismatchedPixels).toBe(pixelCount); - }); - - it('creates an error if a negative number is provided', () => { - const pixelCount = -1; - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - allowableMismatchedPixels: pixelCount, - }; - - const { config, diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.allowableMismatchedPixels).toBe(pixelCount); - expect(diagnostics).toHaveLength(1); - expect(diagnostics[0]).toEqual({ - absFilePath: undefined, - header: 'Build Error', - level: 'error', - lines: [], - messageText: 'allowableMismatchedPixels must be a value that is 0 or greater', - relFilePath: undefined, - type: 'build', - }); - }); - - it.each([true, null])('defaults to a reasonable value if a non-number (%s) is provided', (pixelCount) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - // the nature of this test requires using a non-number, hence th type assertion - allowableMismatchedPixels: pixelCount as unknown as number, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.allowableMismatchedPixels).toBe(100); - }); - }); - - describe('allowableMismatchedRatio', () => { - it.each([-0, 0, 0.5, 1.0])( - 'does nothing if a value between 0 and 1 is provided (%s)', - (allowableMismatchedRatio) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - allowableMismatchedRatio, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.allowableMismatchedRatio).toBe(allowableMismatchedRatio); - }, - ); - - it.each([-1, -0.1, 1.1, 2])( - 'creates an error if a number outside 0 and 1 is provided (%s)', - (allowableMismatchedRatio) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - allowableMismatchedRatio, - }; - - const { config, diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.allowableMismatchedRatio).toBe(allowableMismatchedRatio); - expect(diagnostics).toHaveLength(1); - expect(diagnostics[0]).toEqual({ - absFilePath: undefined, - header: 'Build Error', - level: 'error', - lines: [], - messageText: 'allowableMismatchedRatio must be a value ranging from 0 to 1', - relFilePath: undefined, - type: 'build', - }); - }, - ); - - it.each([true, null])('does nothing when a non-number (%s) is provided', (allowableMismatchedRatio) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - // the nature of this test requires using a non-number, hence th type assertion - allowableMismatchedRatio: allowableMismatchedRatio as unknown as number, - }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.allowableMismatchedRatio).toBe(allowableMismatchedRatio); - }); - }); - - describe('pixelmatchThreshold', () => { - it.each([-0, 0, 0.5, 1.0])('does nothing if a value between 0 and 1 is provided (%s)', (pixelmatchThreshold) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - pixelmatchThreshold, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.pixelmatchThreshold).toBe(pixelmatchThreshold); - }); - - it.each([-0.1, -1, 1.1, 2])( - 'creates an error if a number outside 0 and 1 is provided (%s)', - (pixelmatchThreshold) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - pixelmatchThreshold, - }; - - const { config, diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.pixelmatchThreshold).toBe(pixelmatchThreshold); - expect(diagnostics).toHaveLength(1); - expect(diagnostics[0]).toEqual({ - absFilePath: undefined, - header: 'Build Error', - level: 'error', - lines: [], - messageText: 'pixelmatchThreshold must be a value ranging from 0 to 1', - relFilePath: undefined, - type: 'build', - }); - }, - ); - - it.each([true, null])('defaults to a reasonable value if a non-number (%s) is provided', (pixelmatchThreshold) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - // the nature of this test requires using a non-number, hence th type assertion - pixelmatchThreshold: pixelmatchThreshold as unknown as number, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.allowableMismatchedPixels).toBe(100); - }); - }); - - describe('testRegex', () => { - let testRegex: RegExp; - - beforeEach(() => { - userConfig.flags = { ...flags, spec: true }; - - const { testing: testConfig } = validateConfig(userConfig, mockLoadConfigInit()).config; - const testRegexSetting = testConfig?.testRegex; - - if (!testRegexSetting) { - throw new Error('No testRegex was found in the Stencil TestingConfig. Failing test.'); - } - - testRegex = new RegExp(testRegexSetting[0]); - }); - - describe('test.* extensions', () => { - it.each([ - 'my-component.test.ts', - 'my-component.test.tsx', - 'my-component.test.js', - 'my-component.test.jsx', - 'some/path/test.ts', - 'some/path/test.tsx', - 'some/path/test.js', - 'some/path/test.jsx', - ])(`matches the file '%s'`, (filename) => { - expect(testRegex.test(filename)).toBe(true); - }); - - it.each([ - 'my-component.test.ts.snap', - 'my-component.test.tsx.snap', - 'my-component.test.js.snap', - 'my-component.test.jsx.snap', - 'my-component-test.ts', - 'my-component-test.tsx', - 'my-component-test.js', - 'my-component-test.jsx', - 'my-component.test.t', - 'my-component.test.j', - ])(`doesn't match the file '%s'`, (filename) => { - expect(testRegex.test(filename)).toBe(false); - }); - }); - - describe('spec.* extensions', () => { - it.each([ - 'my-component.spec.ts', - 'my-component.spec.tsx', - 'my-component.spec.js', - 'my-component.spec.jsx', - 'some/path/spec.ts', - 'some/path/spec.tsx', - 'some/path/spec.js', - 'some/path/spec.jsx', - ])(`matches the file '%s'`, (filename) => { - expect(testRegex.test(filename)).toBe(true); - }); - - it.each([ - 'my-component.spec.ts.snap', - 'my-component.spec.tsx.snap', - 'my-component.spec.js.snap', - 'my-component.spec.jsx.snap', - 'my-component-spec.ts', - 'my-component-spec.tsx', - 'my-component-spec.js', - 'my-component-spec.jsx', - 'my-component.spec.t', - 'my-component.spec.j', - ])(`doesn't match the file '%s'`, (filename) => { - expect(testRegex.test(filename)).toBe(false); - }); - }); - - describe('e2e.* extensions', () => { - it.each([ - 'my-component.e2e.ts', - 'my-component.e2e.tsx', - 'my-component.e2e.js', - 'my-component.e2e.jsx', - 'some/path/e2e.ts', - 'some/path/e2e.tsx', - 'some/path/e2e.js', - 'some/path/e2e.jsx', - ])(`matches the file '%s'`, (filename) => { - expect(testRegex.test(filename)).toBe(true); - }); - - it.each([ - 'my-component.e2e.ts.snap', - 'my-component.e2e.tsx.snap', - 'my-component.e2e.js.snap', - 'my-component.e2e.jsx.snap', - 'my-component-e2e.ts', - 'my-component-e2e.tsx', - 'my-component-e2e.js', - 'my-component-e2e.jsx', - 'my-component.e2e.t', - 'my-component.e2e.j', - ])(`doesn't match the file '%s'`, (filename) => { - expect(testRegex.test(filename)).toBe(false); - }); - }); - }); - - describe('testMatch', () => { - it('removes testRegex from the config when testMatch is an array', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - testMatch: ['mockMatcher'], - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.testMatch).toEqual(['mockMatcher']); - expect(config.testing.testRegex).toBeUndefined(); - }); - - it('removes testMatch from the config when testRegex is a string', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - testMatch: undefined, - testRegex: ['/regexStr/'], - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.testMatch).toBeUndefined(); - expect(config.testing.testRegex).toEqual(['/regexStr/']); - }); - - it('transforms testRegex to an array if passed in as string', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - testMatch: undefined, - // @ts-expect-error invalid type because of type update - testRegex: '/regexStr/', - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.testMatch).toBeUndefined(); - expect(config.testing.testRegex).toEqual(['/regexStr/']); - }); - }); - - describe('runner', () => { - it('does nothing if the runner property is a string', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - runner: 'my-runner.js', - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.runner).toEqual('my-runner.js'); - }); - - it('sets the runner if a non-string value is provided', () => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - runner: undefined, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - // 'testing' is the internal directory where the default runner file can be found - expect(config.testing.runner).toEqual(join('testing', 'jest-runner.js')); - }); - }); - - describe('waitBeforeScreenshot', () => { - it.each([-0, 0, 0.5, 1.0])('does nothing for a non-negative value (%s)', (waitBeforeScreenshot) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - waitBeforeScreenshot, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.waitBeforeScreenshot).toBe(waitBeforeScreenshot); - }); - - it('creates an error if the value provided is negative', () => { - const waitBeforeScreenshot = -1; - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - waitBeforeScreenshot, - }; - - const { config, diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.waitBeforeScreenshot).toBe(waitBeforeScreenshot); - expect(diagnostics).toHaveLength(1); - expect(diagnostics[0]).toEqual({ - absFilePath: undefined, - header: 'Build Error', - level: 'error', - lines: [], - messageText: 'waitBeforeScreenshot must be a value that is 0 or greater', - relFilePath: undefined, - type: 'build', - }); - }); - - it.each([true, null])('defaults to a reasonable value if a non-number (%s) is provided', (waitBeforeScreenshot) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - // the nature of this test requires using a non-number, hence the type assertion - pixelmatchThreshold: waitBeforeScreenshot as unknown as number, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.waitBeforeScreenshot).toBe(10); - }); - }); - - describe('emulate', () => { - it.each([[undefined], [[]]])('provides a reasonable default for %s', (emulate) => { - userConfig.flags = { ...flags, e2e: true }; - userConfig.testing = { - emulate, - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.emulate).toEqual([ - { - userAgent: 'default', - viewport: { - width: 600, - height: 600, - deviceScaleFactor: 1, - isMobile: false, - hasTouch: false, - isLandscape: false, - }, - }, - ]); - }); - - it('does nothing when a non-zero length array is provided', () => { - userConfig.flags = { ...flags, e2e: true }; - - const emulateConfig: d.EmulateConfig = { - userAgent: 'mockAgent', - viewport: { - width: 100, - height: 100, - deviceScaleFactor: 1, - isMobile: true, - hasTouch: true, - isLandscape: false, - }, - }; - userConfig.testing = { - emulate: [emulateConfig], - }; - - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - - expect(config.testing.emulate).toEqual([emulateConfig]); - }); - }); -}); diff --git a/src/compiler/config/test/validate-workers.spec.ts b/src/compiler/config/test/validate-workers.spec.ts deleted file mode 100644 index 715a37eb0c1..00000000000 --- a/src/compiler/config/test/validate-workers.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockLoadConfigInit, mockLogger } from '@stencil/core/testing'; -import path from 'path'; - -import { createConfigFlags } from '../../../cli/config-flags'; -import { validateConfig } from '../validate-config'; - -describe('validate-workers', () => { - let userConfig: d.Config; - const logger = mockLogger(); - - beforeEach(() => { - userConfig = { - sys: { - path: path, - } as any, - logger: logger, - rootDir: '/', - namespace: 'Testing', - }; - }); - - it('set maxConcurrentWorkers, but dont let it go under 0', () => { - userConfig.maxConcurrentWorkers = -1; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.maxConcurrentWorkers).toBe(0); - }); - - it('set maxConcurrentWorkers from ci flags', () => { - userConfig.flags = createConfigFlags({ - ci: true, - }); - userConfig.maxConcurrentWorkers = 2; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.maxConcurrentWorkers).toBe(4); - }); - - it('set maxConcurrentWorkers from flags', () => { - userConfig.flags = createConfigFlags({ - maxWorkers: 1, - }); - userConfig.maxConcurrentWorkers = 4; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.maxConcurrentWorkers).toBe(1); - }); - - it('set maxConcurrentWorkers', () => { - userConfig.maxConcurrentWorkers = 4; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.maxConcurrentWorkers).toBe(4); - }); -}); diff --git a/src/compiler/config/validate-config.ts b/src/compiler/config/validate-config.ts deleted file mode 100644 index 1f308f018b6..00000000000 --- a/src/compiler/config/validate-config.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { createNodeLogger, createNodeSys } from '@sys-api-node'; -import { buildError, buildWarn, isBoolean, isNumber, isString, sortBy } from '@utils'; - -import { - ConfigBundle, - ConfigExtras, - Diagnostic, - LoadConfigInit, - LogLevel, - UnvalidatedConfig, - ValidatedConfig, -} from '../../declarations'; -import { setBooleanConfig } from './config-utils'; -import { - DEFAULT_DEV_MODE, - DEFAULT_HASHED_FILENAME_LENGTH, - MAX_HASHED_FILENAME_LENGTH, - MIN_HASHED_FILENAME_LENGTH, -} from './constants'; -import { validateOutputTargets } from './outputs'; -import { validateDevServer } from './validate-dev-server'; -import { validateDocs } from './validate-docs'; -import { validateHydrated } from './validate-hydrated'; -import { validateDistNamespace } from './validate-namespace'; -import { validateNamespace } from './validate-namespace'; -import { validatePaths } from './validate-paths'; -import { validatePlugins } from './validate-plugins'; -import { validateRollupConfig } from './validate-rollup-config'; -import { validateTesting } from './validate-testing'; -import { validateWorkers } from './validate-workers'; - -/** - * Represents the results of validating a previously unvalidated configuration - */ -type ConfigValidationResults = { - /** - * The validated configuration, with well-known default values set if they weren't previously provided - */ - config: ValidatedConfig; - /** - * A collection of errors and warnings that occurred during the configuration validation process - */ - diagnostics: Diagnostic[]; -}; - -/** - * We never really want to re-run validation for a Stencil configuration. - * Besides the cost of doing so, our validation pipeline is unfortunately not - * idempotent, so we want to have a guarantee that even if we call - * {@link validateConfig} in a few places that the same configuration object - * won't be passed through multiple times. So we cache the result of our work - * here. - */ -let CACHED_VALIDATED_CONFIG: ValidatedConfig | null = null; - -/** - * Validate a Config object, ensuring that all its field are present and - * consistent with our expectations. This function transforms an - * {@link UnvalidatedConfig} to a {@link ValidatedConfig}. - * - * **NOTE**: this function _may_ return a previously-cached configuration - * object. It will do so if the cached object is `===` to the one passed in. - * - * @param userConfig an unvalidated config that we've gotten from a user - * @param bootstrapConfig the initial configuration provided by the user (or - * generated by Stencil) used to bootstrap configuration loading and validation - * @returns an object with config and diagnostics props - */ -export const validateConfig = ( - userConfig: UnvalidatedConfig = {}, - bootstrapConfig: LoadConfigInit, -): ConfigValidationResults => { - const diagnostics: Diagnostic[] = []; - - if (CACHED_VALIDATED_CONFIG !== null && CACHED_VALIDATED_CONFIG === userConfig) { - // We've previously done the work to validate a Stencil config. Since our - // overall validation pipeline is unfortunately not idempotent we do not - // want to simply validate again. Leaving aside the performance - // implications of needlessly repeating the validation, we don't want to do - // certain operations multiple times. - // - // For the sake of correctness we check both that the cache is not null and - // that it's the same object as the one passed in. - return { - config: userConfig as ValidatedConfig, - diagnostics, - }; - } - - const config = Object.assign({}, userConfig); - - const logger = bootstrapConfig.logger || config.logger || createNodeLogger(); - - // flags _should_ be JSON safe here - // - // we access `'flags'` on validated config to avoid having to introduce an - // import of the CLI module - const flags: ValidatedConfig['flags'] = JSON.parse(JSON.stringify(config.flags || {})); - - // default level is 'info' - let logLevel: LogLevel = 'info'; - if (flags.debug || flags.verbose) { - logLevel = 'debug'; - } else if (flags.logLevel) { - logLevel = flags.logLevel; - } - - logger.setLevel(logLevel); - - let devMode = config.devMode ?? DEFAULT_DEV_MODE; - if (flags.prod) { - devMode = false; - } else if (flags.dev) { - devMode = true; - } else if (!isBoolean(config.devMode)) { - devMode = DEFAULT_DEV_MODE; - } - - const hashFileNames = config.hashFileNames ?? !devMode; - - const validatedConfig: ValidatedConfig = { - devServer: {}, // assign `devServer` before spreading `config`, in the event 'devServer' is not a key on `config` - ...config, - buildEs5: config.buildEs5 === true || (!devMode && config.buildEs5 === 'prod'), - devMode, - extras: config.extras || {}, - flags, - generateExportMaps: isBoolean(config.generateExportMaps) ? config.generateExportMaps : false, - hashFileNames, - hashedFileNameLength: config.hashedFileNameLength ?? DEFAULT_HASHED_FILENAME_LENGTH, - hydratedFlag: validateHydrated(config), - logLevel, - logger, - minifyCss: config.minifyCss ?? !devMode, - minifyJs: config.minifyJs ?? !devMode, - outputTargets: config.outputTargets ?? [], - rollupConfig: validateRollupConfig(config), - sourceMap: - config.sourceMap === true || (devMode && (config.sourceMap === 'dev' || typeof config.sourceMap === 'undefined')), - sys: config.sys ?? bootstrapConfig.sys ?? createNodeSys({ logger }), - testing: config.testing ?? {}, - docs: validateDocs(config, logger), - transformAliasedImportPaths: isBoolean(userConfig.transformAliasedImportPaths) - ? userConfig.transformAliasedImportPaths - : true, - validatePrimaryPackageOutputTarget: userConfig.validatePrimaryPackageOutputTarget ?? false, - ...validateNamespace(config.namespace, config.fsNamespace, diagnostics), - ...validatePaths(config), - }; - - // Set the log file path on the logger if writeLog is enabled - if (validatedConfig.buildLogFilePath) { - logger.setLogFilePath(validatedConfig.buildLogFilePath); - } - - validatedConfig.extras.lifecycleDOMEvents = !!validatedConfig.extras.lifecycleDOMEvents; - validatedConfig.extras.scriptDataOpts = !!validatedConfig.extras.scriptDataOpts; - validatedConfig.extras.initializeNextTick = !!validatedConfig.extras.initializeNextTick; - validatedConfig.extras.tagNameTransform = !!validatedConfig.extras.tagNameTransform; - validatedConfig.extras.additionalTagTransformers = - validatedConfig.extras.additionalTagTransformers === true || - (!devMode && validatedConfig.extras.additionalTagTransformers === 'prod'); - validatedConfig.extras.addGlobalStyleToComponents = isBoolean(validatedConfig.extras.addGlobalStyleToComponents) - ? validatedConfig.extras.addGlobalStyleToComponents - : 'client'; - - // TODO(STENCIL-914): remove when `experimentalSlotFixes` is the default behavior - // If the user set `experimentalSlotFixes` and any individual slot fix flags to `false`, we need to log a warning - // to the user that we will "override" the individual flags - if (validatedConfig.extras.experimentalSlotFixes === true) { - const possibleFlags: (keyof ConfigExtras)[] = [ - 'appendChildSlotFix', - 'slotChildNodesFix', - 'cloneNodeFix', - 'scopedSlotTextContentFix', - 'experimentalScopedSlotChanges', - ]; - const conflictingFlags = possibleFlags.filter((flag) => validatedConfig.extras[flag] === false); - if (conflictingFlags.length > 0) { - const warning = buildError(diagnostics); - warning.level = 'warn'; - warning.messageText = `If the 'experimentalSlotFixes' flag is enabled it will override any slot fix flags which are disabled. In particular, the following currently-disabled flags will be ignored: ${conflictingFlags.join( - ', ', - )}. Please update your Stencil config accordingly.`; - } - } - - // TODO(STENCIL-914): remove `experimentalSlotFixes` when it's the default behavior - validatedConfig.extras.experimentalSlotFixes = !!validatedConfig.extras.experimentalSlotFixes; - if (validatedConfig.extras.experimentalSlotFixes === true) { - validatedConfig.extras.appendChildSlotFix = true; - validatedConfig.extras.cloneNodeFix = true; - validatedConfig.extras.slotChildNodesFix = true; - validatedConfig.extras.scopedSlotTextContentFix = true; - validatedConfig.extras.experimentalScopedSlotChanges = true; - } else { - validatedConfig.extras.appendChildSlotFix = !!validatedConfig.extras.appendChildSlotFix; - validatedConfig.extras.cloneNodeFix = !!validatedConfig.extras.cloneNodeFix; - validatedConfig.extras.slotChildNodesFix = !!validatedConfig.extras.slotChildNodesFix; - validatedConfig.extras.scopedSlotTextContentFix = !!validatedConfig.extras.scopedSlotTextContentFix; - // TODO(STENCIL-1086): remove this option when it's the default behavior - validatedConfig.extras.experimentalScopedSlotChanges = !!validatedConfig.extras.experimentalScopedSlotChanges; - } - - setBooleanConfig(validatedConfig, 'watch', 'watch', false); - setBooleanConfig(validatedConfig, 'buildDocs', 'docs', !validatedConfig.devMode); - setBooleanConfig(validatedConfig, 'buildDist', 'esm', !validatedConfig.devMode || !!validatedConfig.buildEs5); - setBooleanConfig(validatedConfig, 'profile', 'profile', validatedConfig.devMode); - setBooleanConfig(validatedConfig, 'writeLog', 'log', false); - setBooleanConfig(validatedConfig, 'buildAppCore', null, true); - setBooleanConfig(validatedConfig, 'autoprefixCss', null, validatedConfig.buildEs5); - setBooleanConfig(validatedConfig, 'validateTypes', null, !validatedConfig._isTesting); - setBooleanConfig(validatedConfig, 'allowInlineScripts', null, true); - setBooleanConfig(validatedConfig, 'suppressReservedPublicNameWarnings', null, false); - - if (!isString(validatedConfig.taskQueue)) { - validatedConfig.taskQueue = 'async'; - } - - // hash file names - if (!isBoolean(validatedConfig.hashFileNames)) { - validatedConfig.hashFileNames = !validatedConfig.devMode; - } - if (!isNumber(validatedConfig.hashedFileNameLength)) { - validatedConfig.hashedFileNameLength = DEFAULT_HASHED_FILENAME_LENGTH; - } - if (validatedConfig.hashedFileNameLength < MIN_HASHED_FILENAME_LENGTH) { - const err = buildError(diagnostics); - err.messageText = `validatedConfig.hashedFileNameLength must be at least ${MIN_HASHED_FILENAME_LENGTH} characters`; - } - if (validatedConfig.hashedFileNameLength > MAX_HASHED_FILENAME_LENGTH) { - const err = buildError(diagnostics); - err.messageText = `validatedConfig.hashedFileNameLength cannot be more than ${MAX_HASHED_FILENAME_LENGTH} characters`; - } - if (!validatedConfig.env) { - validatedConfig.env = {}; - } - - // outputTargets - validateOutputTargets(validatedConfig, diagnostics); - - // plugins - validatePlugins(validatedConfig, diagnostics); - - // dev server - validatedConfig.devServer = validateDevServer(validatedConfig, diagnostics); - - // testing - validateTesting(validatedConfig, diagnostics); - - // bundles - if (Array.isArray(validatedConfig.bundles)) { - validatedConfig.bundles = sortBy(validatedConfig.bundles, (a: ConfigBundle) => a.components.length); - } else { - validatedConfig.bundles = []; - } - - // exclude components (tag list) - if (!Array.isArray(validatedConfig.excludeComponents)) { - validatedConfig.excludeComponents = []; - } - - // validate how many workers we can use - validateWorkers(validatedConfig); - - // default devInspector to whatever devMode is - setBooleanConfig(validatedConfig, 'devInspector', null, validatedConfig.devMode); - - if (!validatedConfig._isTesting) { - validateDistNamespace(validatedConfig, diagnostics); - } - - setBooleanConfig(validatedConfig, 'enableCache', 'cache', true); - - if (!Array.isArray(validatedConfig.watchIgnoredRegex) && validatedConfig.watchIgnoredRegex != null) { - validatedConfig.watchIgnoredRegex = [validatedConfig.watchIgnoredRegex]; - } - validatedConfig.watchIgnoredRegex = ((validatedConfig.watchIgnoredRegex as RegExp[]) || []).reduce((arr, reg) => { - if (reg instanceof RegExp) { - arr.push(reg); - } - return arr; - }, [] as RegExp[]); - - // TODO(STENCIL-1107): Remove this check. It'll be unneeded (and raise a compilation error when we build Stencil) once - // this property is removed. - if (validatedConfig.nodeResolve?.customResolveOptions) { - const warn = buildWarn(diagnostics); - // this message is particularly long - let the underlying logger implementation take responsibility for breaking it - // up to fit in a terminal window - warn.messageText = `nodeResolve.customResolveOptions is a deprecated option in a Stencil Configuration file. If you need this option, please open a new issue in the Stencil repository (https://github.com/stenciljs/core/issues/new/choose)`; - } - - CACHED_VALIDATED_CONFIG = validatedConfig; - - return { - config: validatedConfig, - diagnostics, - }; -}; diff --git a/src/compiler/config/validate-docs.ts b/src/compiler/config/validate-docs.ts deleted file mode 100644 index 9b2976d49a8..00000000000 --- a/src/compiler/config/validate-docs.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as d from '../../declarations'; -import { UnvalidatedConfig } from '../../declarations'; -import { isHexColor } from '../docs/readme/docs-util'; -import { DEFAULT_TARGET_COMPONENT_STYLES } from './constants'; - -/** - * Validate the `.docs` property on the supplied config object and - * return a properly-validated value. - * - * @param config the configuration we're examining - * @param logger the logger that will be set on the config - * @returns a suitable/default value for the docs property - */ -export const validateDocs = (config: UnvalidatedConfig, logger: d.Logger): d.ValidatedConfig['docs'] => { - const { background: defaultBackground, textColor: defaultTextColor } = DEFAULT_TARGET_COMPONENT_STYLES; - - let { background = defaultBackground, textColor = defaultTextColor } = - config.docs?.markdown?.targetComponent ?? DEFAULT_TARGET_COMPONENT_STYLES; - - if (!isHexColor(background)) { - logger.warn( - `'${background}' is not a valid hex color. The default value for diagram backgrounds ('${defaultBackground}') will be used.`, - ); - background = defaultBackground; - } - - if (!isHexColor(textColor)) { - logger.warn( - `'${textColor}' is not a valid hex color. The default value for diagram text ('${defaultTextColor}') will be used.`, - ); - textColor = defaultTextColor; - } - - return { - markdown: { - targetComponent: { - background, - textColor, - }, - }, - }; -}; diff --git a/src/compiler/config/validate-plugins.ts b/src/compiler/config/validate-plugins.ts deleted file mode 100644 index 55655eb779a..00000000000 --- a/src/compiler/config/validate-plugins.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { buildWarn } from '@utils'; - -import type * as d from '../../declarations'; - -export const validatePlugins = (config: d.UnvalidatedConfig, diagnostics: d.Diagnostic[]) => { - const userPlugins = config.plugins; - - if (!config.rollupPlugins) { - config.rollupPlugins = {}; - } - if (!Array.isArray(userPlugins)) { - config.plugins = []; - return; - } - - const rollupPlugins = userPlugins.filter((plugin) => { - return !!(plugin && typeof plugin === 'object' && !plugin.pluginType); - }); - - const hasResolveNode = rollupPlugins.some((p) => p.name === 'node-resolve'); - const hasCommonjs = rollupPlugins.some((p) => p.name === 'commonjs'); - - if (hasCommonjs) { - const warn = buildWarn(diagnostics); - warn.messageText = `Stencil already uses "@rollup/plugin-commonjs", please remove it from your "stencil.config.ts" plugins. - You can configure the commonjs settings using the "commonjs" property in "stencil.config.ts`; - } - - if (hasResolveNode) { - const warn = buildWarn(diagnostics); - warn.messageText = `Stencil already uses "@rollup/plugin-commonjs", please remove it from your "stencil.config.ts" plugins. - You can configure the commonjs settings using the "commonjs" property in "stencil.config.ts`; - } - - config.rollupPlugins.before = [ - ...(config.rollupPlugins.before || []), - ...rollupPlugins.filter(({ name }) => name !== 'node-resolve' && name !== 'commonjs'), - ]; - - config.plugins = userPlugins.filter((plugin) => { - return !!(plugin && typeof plugin === 'object' && plugin.pluginType); - }); -}; diff --git a/src/compiler/config/validate-rollup-config.ts b/src/compiler/config/validate-rollup-config.ts deleted file mode 100644 index d3940836a9a..00000000000 --- a/src/compiler/config/validate-rollup-config.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { isObject, pluck } from '@utils'; - -import type * as d from '../../declarations'; - -/** - * Ensure that a valid baseline rollup configuration is set on the validated - * config. - * - * If a config is present this will return a new config based on the user - * supplied one. - * - * If no config is present, this will return a default config. - * - * @param config a validated user-supplied configuration object - * @returns a validated rollup configuration - */ -export const validateRollupConfig = (config: d.Config): d.RollupConfig => { - let cleanRollupConfig = { ...DEFAULT_ROLLUP_CONFIG }; - - const rollupConfig = config.rollupConfig; - - if (!rollupConfig || !isObject(rollupConfig)) { - return cleanRollupConfig; - } - - if (rollupConfig.inputOptions && isObject(rollupConfig.inputOptions)) { - cleanRollupConfig = { - ...cleanRollupConfig, - inputOptions: pluck(rollupConfig.inputOptions, [ - 'context', - 'moduleContext', - 'treeshake', - 'external', - 'maxParallelFileOps', - ]), - }; - } - - if (rollupConfig.outputOptions && isObject(rollupConfig.outputOptions)) { - cleanRollupConfig = { - ...cleanRollupConfig, - outputOptions: pluck(rollupConfig.outputOptions, ['globals']), - }; - } - - return cleanRollupConfig; -}; - -const DEFAULT_ROLLUP_CONFIG: d.RollupConfig = { - inputOptions: {}, - outputOptions: {}, -}; diff --git a/src/compiler/config/validate-testing.ts b/src/compiler/config/validate-testing.ts deleted file mode 100644 index 767c7ef288e..00000000000 --- a/src/compiler/config/validate-testing.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { buildError, isOutputTargetDist, isOutputTargetWww, isString, join, normalizePath } from '@utils'; -import { basename, dirname, isAbsolute } from 'path'; - -import type * as d from '../../declarations'; -import { isLocalModule } from '../sys/resolve/resolve-utils'; - -export const validateTesting = (config: d.ValidatedConfig, diagnostics: d.Diagnostic[]) => { - const testing = (config.testing = Object.assign({}, config.testing || {})); - - if (!config.flags.e2e && !config.flags.spec) { - return; - } - - let configPathDir = config.configPath!; - if (isString(configPathDir)) { - if (basename(configPathDir).includes('.')) { - configPathDir = dirname(configPathDir); - } - } else { - configPathDir = config.rootDir!; - } - - if (typeof config.flags.headless === 'boolean' || config.flags.headless === 'shell') { - testing.browserHeadless = config.flags.headless; - } else if (typeof testing.browserHeadless !== 'boolean' && testing.browserHeadless !== 'shell') { - testing.browserHeadless = 'shell'; - } - - /** - * Using the deprecated `browserHeadless: true` flag causes Chrome to crash when running tests. - * Ensure users don't run into this by throwing a deliberate error. - */ - if (typeof testing.browserHeadless === 'boolean' && testing.browserHeadless) { - throw new Error(`Setting "browserHeadless" config to \`true\` is not supported anymore, please set it to "shell"!`); - } - - if (!testing.browserWaitUntil) { - testing.browserWaitUntil = 'load'; - } - - /** - * ensure we always test on stable Chrome - */ - if (!isString(testing.browserChannel)) { - testing.browserChannel = 'chrome'; - } - - testing.browserArgs = testing.browserArgs || []; - addTestingConfigOption(testing.browserArgs, '--font-render-hinting=medium'); - addTestingConfigOption(testing.browserArgs, '--incognito'); - if (config.flags.ci || process.env.CI) { - addTestingConfigOption(testing.browserArgs, '--no-sandbox'); - addTestingConfigOption(testing.browserArgs, '--disable-setuid-sandbox'); - addTestingConfigOption(testing.browserArgs, '--disable-dev-shm-usage'); - testing.browserHeadless = 'shell'; - } else if (config.flags.devtools || testing.browserDevtools) { - testing.browserDevtools = true; - testing.browserHeadless = false; - } - - if (typeof testing.rootDir === 'string') { - if (!isAbsolute(testing.rootDir)) { - testing.rootDir = join(config.rootDir!, testing.rootDir); - } - } else { - testing.rootDir = config.rootDir; - } - - if (typeof config.flags.screenshotConnector === 'string') { - testing.screenshotConnector = config.flags.screenshotConnector; - } - - if (typeof testing.screenshotConnector === 'string') { - if (!isAbsolute(testing.screenshotConnector)) { - testing.screenshotConnector = join(config.rootDir!, testing.screenshotConnector); - } else { - testing.screenshotConnector = normalizePath(testing.screenshotConnector); - } - } else { - testing.screenshotConnector = join( - config.sys!.getCompilerExecutingPath(), - '..', - '..', - 'screenshot', - 'local-connector.js', - ); - } - - /** - * We only allow numbers or null for the screenshotTimeout, so if we detect anything - * else, we set it to null. - */ - if (typeof testing.screenshotTimeout != 'number') { - testing.screenshotTimeout = null; - } - - if (!Array.isArray(testing.testPathIgnorePatterns)) { - testing.testPathIgnorePatterns = DEFAULT_IGNORE_PATTERNS.map((ignorePattern) => { - return join(testing.rootDir!, ignorePattern); - }); - - (config.outputTargets ?? []) - .filter( - (o): o is d.OutputTargetWww | d.OutputTargetDist => (isOutputTargetDist(o) || isOutputTargetWww(o)) && !!o.dir, - ) - .forEach((outputTarget) => { - testing.testPathIgnorePatterns?.push(outputTarget.dir!); - }); - } - - if (typeof testing.preset !== 'string') { - testing.preset = join(config.sys!.getCompilerExecutingPath(), '..', '..', 'testing'); - } else if (!isAbsolute(testing.preset)) { - testing.preset = join(configPathDir, testing.preset); - } - - if (!Array.isArray(testing.setupFilesAfterEnv)) { - testing.setupFilesAfterEnv = []; - } - - testing.setupFilesAfterEnv.unshift( - join(config.sys!.getCompilerExecutingPath(), '..', '..', 'testing', 'jest-setuptestframework.js'), - ); - - if (isString(testing.testEnvironment)) { - if (!isAbsolute(testing.testEnvironment) && isLocalModule(testing.testEnvironment)) { - testing.testEnvironment = join(configPathDir, testing.testEnvironment); - } - } - - if (typeof testing.allowableMismatchedPixels === 'number') { - if (testing.allowableMismatchedPixels < 0) { - const err = buildError(diagnostics); - err.messageText = `allowableMismatchedPixels must be a value that is 0 or greater`; - } - } else { - testing.allowableMismatchedPixels = DEFAULT_ALLOWABLE_MISMATCHED_PIXELS; - } - - if (typeof testing.allowableMismatchedRatio === 'number') { - if (testing.allowableMismatchedRatio < 0 || testing.allowableMismatchedRatio > 1) { - const err = buildError(diagnostics); - err.messageText = `allowableMismatchedRatio must be a value ranging from 0 to 1`; - } - } - - if (typeof testing.pixelmatchThreshold === 'number') { - if (testing.pixelmatchThreshold < 0 || testing.pixelmatchThreshold > 1) { - const err = buildError(diagnostics); - err.messageText = `pixelmatchThreshold must be a value ranging from 0 to 1`; - } - } else { - testing.pixelmatchThreshold = DEFAULT_PIXEL_MATCH_THRESHOLD; - } - - if (testing.testRegex === undefined) { - /** - * The test regex covers cases of: - * - files under a `__tests__` directory - * - the case where a test file has a name such as `test.ts`, `spec.ts` or `e2e.ts`. - * - these files can use any of the following file extensions: .ts, .tsx, .js, .jsx. - * - this regex only handles the entire path of a file, e.g. `/some/path/e2e.ts` - * - the case where a test file ends with `.test.ts`, `.spec.ts`, or `.e2e.ts`. - * - these files can use any of the following file extensions: .ts, .tsx, .js, .jsx. - * - this regex case shall match file names such as `my-cmp.spec.ts`, `test.spec.ts` - * - this regex case shall not match file names such as `attest.ts`, `bespec.ts` - */ - testing.testRegex = ['(/__tests__/.*|(\\.|/)(test|spec|e2e))\\.[jt]sx?$']; - } else if (typeof testing.testRegex === 'string') { - testing.testRegex = [testing.testRegex]; - } - - if (Array.isArray(testing.testMatch)) { - delete testing.testRegex; - } else if (typeof testing.testRegex === 'string') { - delete testing.testMatch; - } - - if (typeof testing.runner !== 'string') { - testing.runner = join(config.sys!.getCompilerExecutingPath(), '..', '..', 'testing', 'jest-runner.js'); - } - - if (typeof testing.waitBeforeScreenshot === 'number') { - if (testing.waitBeforeScreenshot < 0) { - const err = buildError(diagnostics); - err.messageText = `waitBeforeScreenshot must be a value that is 0 or greater`; - } - } else { - testing.waitBeforeScreenshot = 10; - } - - if (!Array.isArray(testing.emulate) || testing.emulate.length === 0) { - testing.emulate = [ - { - userAgent: 'default', - viewport: { - width: 600, - height: 600, - deviceScaleFactor: 1, - isMobile: false, - hasTouch: false, - isLandscape: false, - }, - }, - ]; - } -}; - -const addTestingConfigOption = (setArray: string[], option: string) => { - if (!setArray.includes(option)) { - setArray.push(option); - } -}; - -const DEFAULT_ALLOWABLE_MISMATCHED_PIXELS = 100; -const DEFAULT_PIXEL_MATCH_THRESHOLD = 0.1; -const DEFAULT_IGNORE_PATTERNS = ['.vscode', '.stencil', 'node_modules']; diff --git a/src/compiler/config/validate-workers.ts b/src/compiler/config/validate-workers.ts deleted file mode 100644 index 82ab0467c83..00000000000 --- a/src/compiler/config/validate-workers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type * as d from '../../declarations'; - -export const validateWorkers = (config: d.ValidatedConfig) => { - if (typeof config.maxConcurrentWorkers !== 'number') { - config.maxConcurrentWorkers = 8; - } - - if (typeof config.flags.maxWorkers === 'number') { - config.maxConcurrentWorkers = config.flags.maxWorkers; - } else if (config.flags.ci) { - config.maxConcurrentWorkers = 4; - } - - config.maxConcurrentWorkers = Math.max(Math.min(config.maxConcurrentWorkers, 16), 0); - - if (config.devServer) { - config.devServer.worker = config.maxConcurrentWorkers > 0; - } -}; diff --git a/src/compiler/docs/cem/index.ts b/src/compiler/docs/cem/index.ts deleted file mode 100644 index 39aef525446..00000000000 --- a/src/compiler/docs/cem/index.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { dashToPascalCase, isOutputTargetDocsCustomElementsManifest } from '@utils'; - -import type * as d from '../../../declarations'; - -/** - * Generate Custom Elements Manifest (custom-elements.json) output - * conforming to the Custom Elements Manifest specification. - * @see https://github.com/webcomponents/custom-elements-manifest - * - * @param compilerCtx the current compiler context - * @param docsData the generated docs data from Stencil components - * @param outputTargets the output targets configured for the build - */ -export const generateCustomElementsManifestDocs = async ( - compilerCtx: d.CompilerCtx, - docsData: d.JsonDocs, - outputTargets: d.OutputTarget[], -): Promise => { - const cemOutputTargets = outputTargets.filter(isOutputTargetDocsCustomElementsManifest); - if (cemOutputTargets.length === 0) { - return; - } - - const manifest = generateManifest(docsData); - const jsonContent = JSON.stringify(manifest, null, 2); - - await Promise.all(cemOutputTargets.map((outputTarget) => compilerCtx.fs.writeFile(outputTarget.file!, jsonContent))); -}; - -/** - * Generate the Custom Elements Manifest from Stencil docs data - * @param docsData the generated docs data - * @returns the Custom Elements Manifest object - */ -const generateManifest = (docsData: d.JsonDocs): CustomElementsManifest => { - // Group components by their source file path - const componentsByFile = new Map(); - - for (const component of docsData.components) { - const filePath = component.filePath; - if (!componentsByFile.has(filePath)) { - componentsByFile.set(filePath, []); - } - componentsByFile.get(filePath)!.push(component); - } - - const modules: JavaScriptModule[] = []; - - for (const [filePath, components] of componentsByFile) { - const declarations: CustomElementDeclaration[] = components.map((component) => componentToDeclaration(component)); - - const exports: (JavaScriptExport | CustomElementExport)[] = components.flatMap((component) => { - const className = dashToPascalCase(component.tag); - return [ - { - kind: 'js' as const, - name: className, - declaration: { - name: className, - }, - }, - { - kind: 'custom-element-definition' as const, - name: component.tag, - declaration: { - name: className, - }, - }, - ]; - }); - - modules.push({ - kind: 'javascript-module', - path: filePath, - declarations, - exports, - }); - } - - return { - schemaVersion: '2.1.0', - modules, - }; -}; - -/** - * Convert Stencil's ComponentCompilerTypeReferences to CEM TypeReference array - * @param references Stencil's type references map - * @returns CEM TypeReference array - */ -const convertTypeReferences = (references?: d.ComponentCompilerTypeReferences): TypeReference[] | undefined => { - if (!references || Object.keys(references).length === 0) { - return undefined; - } - - return Object.entries(references).map(([name, ref]) => ({ - name, - // Global types (like HTMLElement, Array) get 'global:' package - ...(ref.location === 'global' && { package: 'global:' }), - // Imported types get their module path - ...(ref.location === 'import' && ref.path && { module: ref.path }), - // Local types don't need package or module (they're in the same module) - })); -}; - -/** - * Create a CEM Type object from a type string and optional references - * @param text the type string - * @param references Stencil's type references map - * @returns CEM Type object - */ -const createType = (text: string, references?: d.ComponentCompilerTypeReferences): Type => { - const typeRefs = convertTypeReferences(references); - return { - text, - ...(typeRefs && { references: typeRefs }), - }; -}; - -/** - * Convert a Stencil component to a Custom Element Declaration - * @param component the Stencil component docs data - * @returns the Custom Element Declaration - */ -const componentToDeclaration = (component: d.JsonDocsComponent): CustomElementDeclaration => { - const className = dashToPascalCase(component.tag); - - const attributes: Attribute[] = component.props - .filter((prop) => prop.attr !== undefined) - .map((prop) => ({ - name: prop.attr!, - ...(prop.docs && { description: prop.docs }), - ...(prop.type && { type: createType(prop.type, prop.complexType?.references) }), - ...(prop.default !== undefined && { default: prop.default }), - fieldName: prop.name, - ...(prop.deprecation !== undefined && { deprecated: prop.deprecation || true }), - })); - - const members: (CustomElementField | ClassMethod)[] = [ - // Fields (properties) - ...component.props.map( - (prop): CustomElementField => ({ - kind: 'field', - name: prop.name, - ...(prop.docs && { description: prop.docs }), - ...(prop.type && { type: createType(prop.type, prop.complexType?.references) }), - ...(prop.default !== undefined && { default: prop.default }), - ...(prop.deprecation !== undefined && { deprecated: prop.deprecation || true }), - ...(!prop.mutable && { readonly: true }), - ...(prop.attr && { attribute: prop.attr }), - ...(prop.reflectToAttr && { reflects: true }), - }), - ), - // Methods - ...component.methods.map( - (method): ClassMethod => ({ - kind: 'method', - name: method.name, - ...(method.docs && { description: method.docs }), - ...(method.deprecation !== undefined && { deprecated: method.deprecation || true }), - ...(method.parameters && - method.parameters.length > 0 && { - parameters: method.parameters.map((param) => ({ - name: param.name, - ...(param.docs && { description: param.docs }), - ...(param.type && { type: createType(param.type, method.complexType?.references) }), - })), - }), - ...(method.returns && { - return: { - ...(method.returns.type && { type: createType(method.returns.type, method.complexType?.references) }), - ...(method.returns.docs && { description: method.returns.docs }), - }, - }), - }), - ), - ]; - - const events: Event[] = component.events.map((event) => ({ - name: event.event, - ...(event.docs && { description: event.docs }), - type: createType(event.detail ? `CustomEvent<${event.detail}>` : 'CustomEvent', event.complexType?.references), - ...(event.deprecation !== undefined && { deprecated: event.deprecation || true }), - })); - - const slots: Slot[] = component.slots.map((slot) => ({ - name: slot.name, - ...(slot.docs && { description: slot.docs }), - })); - - const cssParts: CssPart[] = component.parts.map((part) => ({ - name: part.name, - ...(part.docs && { description: part.docs }), - })); - - const cssProperties: CssCustomProperty[] = component.styles - .filter((style) => style.annotation === 'prop') - .map((style) => ({ - name: style.name, - ...(style.docs && { description: style.docs }), - })); - - // Generate demos from usage examples - const demos: Demo[] = Object.entries(component.usage || {}).map(([name, content]) => ({ - // Create relative URL from usagesDir + filename - url: component.usagesDir ? `${component.usagesDir}/${name}.md` : `${name}.md`, - ...(content && { description: content }), - })); - - return { - kind: 'class', - customElement: true, - tagName: component.tag, - name: className, - ...(component.docs && { description: component.docs }), - ...(component.deprecation !== undefined && { deprecated: component.deprecation || true }), - ...(attributes.length > 0 && { attributes }), - ...(members.length > 0 && { members }), - ...(events.length > 0 && { events }), - ...(slots.length > 0 && { slots }), - ...(cssParts.length > 0 && { cssParts }), - ...(cssProperties.length > 0 && { cssProperties }), - ...(component.customStates.length > 0 && { - customStates: component.customStates.map((state) => ({ - name: state.name, - initialValue: state.initialValue, - ...(state.docs && { description: state.docs }), - })), - }), - ...(demos.length > 0 && { demos }), - }; -}; - -// Custom Elements Manifest Types -// Based on https://github.com/webcomponents/custom-elements-manifest/blob/main/schema.d.ts - -interface CustomElementsManifest { - schemaVersion: string; - modules: JavaScriptModule[]; -} - -interface JavaScriptModule { - kind: 'javascript-module'; - path: string; - declarations?: CustomElementDeclaration[]; - exports?: (JavaScriptExport | CustomElementExport)[]; -} - -interface JavaScriptExport { - kind: 'js'; - name: string; - declaration: Reference; -} - -interface CustomElementExport { - kind: 'custom-element-definition'; - name: string; - declaration: Reference; -} - -interface Reference { - name: string; - package?: string; - module?: string; -} - -interface CustomElementDeclaration { - kind: 'class'; - customElement: true; - tagName: string; - name: string; - description?: string; - deprecated?: boolean | string; - attributes?: Attribute[]; - members?: (CustomElementField | ClassMethod)[]; - events?: Event[]; - slots?: Slot[]; - cssParts?: CssPart[]; - cssProperties?: CssCustomProperty[]; - customStates?: CustomState[]; - demos?: Demo[]; -} - -interface Demo { - url: string; - description?: string; -} - -interface Attribute { - name: string; - description?: string; - type?: Type; - default?: string; - fieldName?: string; - deprecated?: boolean | string; -} - -interface Type { - text: string; - references?: TypeReference[]; -} - -interface TypeReference { - name: string; - package?: string; - module?: string; -} - -interface CustomElementField { - kind: 'field'; - name: string; - description?: string; - type?: Type; - default?: string; - deprecated?: boolean | string; - readonly?: boolean; - attribute?: string; - reflects?: boolean; -} - -interface ClassMethod { - kind: 'method'; - name: string; - description?: string; - deprecated?: boolean | string; - parameters?: Parameter[]; - return?: { - type?: Type; - description?: string; - }; -} - -interface Parameter { - name: string; - description?: string; - type?: Type; -} - -interface Event { - name: string; - description?: string; - type: Type; - deprecated?: boolean | string; -} - -interface Slot { - name: string; - description?: string; -} - -interface CssPart { - name: string; - description?: string; -} - -/** - * Custom state that can be targeted with the CSS :state() pseudo-class. - * This is a custom extension to the CEM spec. - */ -interface CustomState { - name: string; - initialValue: boolean; - description?: string; -} - -interface CssCustomProperty { - name: string; - description?: string; -} diff --git a/src/compiler/docs/custom/index.ts b/src/compiler/docs/custom/index.ts deleted file mode 100644 index 600f25e1a78..00000000000 --- a/src/compiler/docs/custom/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { isOutputTargetDocsCustom } from '@utils'; - -import type * as d from '../../../declarations'; - -export const generateCustomDocs = async ( - config: d.ValidatedConfig, - docsData: d.JsonDocs, - outputTargets: d.OutputTarget[], -) => { - const customOutputTargets = outputTargets.filter(isOutputTargetDocsCustom); - if (customOutputTargets.length === 0) { - return; - } - await Promise.all( - customOutputTargets.map(async (customOutput) => { - try { - await customOutput.generator(docsData, config); - } catch (e) { - config.logger.error(`uncaught custom docs error: ${e}`); - } - }), - ); -}; diff --git a/src/compiler/docs/json/index.ts b/src/compiler/docs/json/index.ts deleted file mode 100644 index 24dd13418f7..00000000000 --- a/src/compiler/docs/json/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { isOutputTargetDocsJson, join } from '@utils'; - -import type * as d from '../../../declarations'; - -export const generateJsonDocs = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - docsData: d.JsonDocs, - outputTargets: d.OutputTarget[], -) => { - const jsonOutputTargets = outputTargets.filter(isOutputTargetDocsJson); - if (jsonOutputTargets.length === 0) { - return; - } - const docsDtsPath = join(config.sys.getCompilerExecutingPath(), '..', '..', 'internal', 'stencil-public-docs.d.ts'); - let docsDts = await compilerCtx.fs.readFile(docsDtsPath); - // this file was written by dts-bundle-generator, which uses tabs for - // indentation. Instead, let's replace those with spaces! - docsDts = docsDts - .split('\n') - .map((line) => line.replace(/\t/g, ' ')) - .join('\n'); - - const typesContent = ` -/** - * This is an autogenerated file created by the Stencil compiler. - * DO NOT MODIFY IT MANUALLY - */ -${docsDts} -declare const _default: JsonDocs; -export default _default; -`; - - const json = { - ...docsData, - components: docsData.components.map((cmp) => ({ - filePath: cmp.filePath, - - encapsulation: cmp.encapsulation, - tag: cmp.tag, - readme: cmp.readme, - docs: cmp.docs, - docsTags: cmp.docsTags, - usage: cmp.usage, - props: cmp.props, - methods: cmp.methods, - events: cmp.events, - listeners: cmp.listeners, - styles: cmp.styles, - slots: cmp.slots, - parts: cmp.parts, - states: cmp.customStates, - dependents: cmp.dependents, - dependencies: cmp.dependencies, - dependencyGraph: cmp.dependencyGraph, - deprecation: cmp.deprecation, - })), - }; - const jsonContent = JSON.stringify(json, null, 2); - await Promise.all( - jsonOutputTargets.map((jsonOutput) => { - return writeDocsOutput(compilerCtx, jsonOutput, jsonContent, typesContent); - }), - ); -}; - -export const writeDocsOutput = async ( - compilerCtx: d.CompilerCtx, - jsonOutput: d.OutputTargetDocsJson, - jsonContent: string, - typesContent: string, -) => { - return Promise.all([ - compilerCtx.fs.writeFile(jsonOutput.file, jsonContent), - jsonOutput.typesFile ? compilerCtx.fs.writeFile(jsonOutput.typesFile, typesContent) : (Promise.resolve() as any), - ]); -}; diff --git a/src/compiler/docs/readme/index.ts b/src/compiler/docs/readme/index.ts deleted file mode 100644 index db1b2e5797f..00000000000 --- a/src/compiler/docs/readme/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { isOutputTargetDocsReadme } from '@utils'; - -import type * as d from '../../../declarations'; -import { generateReadme } from './output-docs'; - -export const generateReadmeDocs = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - docsData: d.JsonDocs, - outputTargets: d.OutputTarget[], -) => { - const readmeOutputTargets = outputTargets.filter(isOutputTargetDocsReadme); - if (readmeOutputTargets.length === 0) { - return; - } - const strictCheck = readmeOutputTargets.some((o) => o.strict); - if (strictCheck) { - strictCheckDocs(config, docsData); - } - - await Promise.all( - docsData.components.map((cmpData) => { - return generateReadme(config, compilerCtx, readmeOutputTargets, cmpData, docsData.components); - }), - ); -}; - -export const strictCheckDocs = (config: d.ValidatedConfig, docsData: d.JsonDocs) => { - docsData.components.forEach((component) => { - component.props.forEach((prop) => { - if (!prop.docs && prop.deprecation === undefined) { - config.logger.warn(`Property "${prop.name}" of "${component.tag}" is not documented. ${component.filePath}`); - } - }); - component.methods.forEach((method) => { - if (!method.docs && method.deprecation === undefined) { - config.logger.warn(`Method "${method.name}" of "${component.tag}" is not documented. ${component.filePath}`); - } - }); - component.events.forEach((ev) => { - if (!ev.docs && ev.deprecation === undefined) { - config.logger.warn(`Event "${ev.event}" of "${component.tag}" is not documented. ${component.filePath}`); - } - }); - component.parts.forEach((ev) => { - if (ev.docs === '') { - config.logger.warn(`Part "${ev.name}" of "${component.tag}" is not documented. ${component.filePath}`); - } - }); - }); -}; diff --git a/src/compiler/docs/readme/markdown-custom-states.ts b/src/compiler/docs/readme/markdown-custom-states.ts deleted file mode 100644 index 9c2f98631dd..00000000000 --- a/src/compiler/docs/readme/markdown-custom-states.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type * as d from '../../../declarations'; -import { MarkdownTable } from './docs-util'; - -/** - * Converts a list of Custom States metadata to a table written in Markdown - * @param customStates the Custom States metadata to convert - * @returns a list of strings that make up the Markdown table - */ -export const customStatesToMarkdown = (customStates: d.JsonDocsCustomState[]): ReadonlyArray => { - const content: string[] = []; - if (customStates.length === 0) { - return content; - } - - content.push(`## Custom States`); - content.push(``); - - const table = new MarkdownTable(); - table.addHeader(['State', 'Initial Value', 'Description']); - - customStates.forEach((state) => { - table.addRow([`\`:state(${state.name})\``, state.initialValue ? '`true`' : '`false`', state.docs]); - }); - - content.push(...table.toMarkdown()); - content.push(``); - content.push(``); - - return content; -}; diff --git a/src/compiler/docs/readme/output-docs.ts b/src/compiler/docs/readme/output-docs.ts deleted file mode 100644 index 208271ddee8..00000000000 --- a/src/compiler/docs/readme/output-docs.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { join, normalizePath, relative } from '@utils'; - -import type * as d from '../../../declarations'; -import { AUTO_GENERATE_COMMENT } from '../constants'; -import { getUserReadmeContent } from '../generate-doc-data'; -import { stylesToMarkdown } from './markdown-css-props'; -import { customStatesToMarkdown } from './markdown-custom-states'; -import { depsToMarkdown } from './markdown-dependencies'; -import { eventsToMarkdown } from './markdown-events'; -import { methodsToMarkdown } from './markdown-methods'; -import { overviewToMarkdown } from './markdown-overview'; -import { partsToMarkdown } from './markdown-parts'; -import { propsToMarkdown } from './markdown-props'; -import { slotsToMarkdown } from './markdown-slots'; -import { usageToMarkdown } from './markdown-usage'; - -/** - * Generate a README for a given component and write it to disk. - * - * Typically the README is going to be a 'sibling' to the component's source - * code (i.e. written to the same directory) but the user may also configure a - * custom output directory by setting {@link d.OutputTargetDocsReadme.dir}. - * - * Output readme files also include {@link AUTO_GENERATE_COMMENT}, and any - * text located _above_ that comment is preserved when the new readme is written - * to disk. - * - * @param config a validated Stencil config - * @param compilerCtx the current compiler context - * @param readmeOutputs docs-readme output targets - * @param docsData documentation data for the component of interest - * @param cmps metadata for all the components in the project - */ -export const generateReadme = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - readmeOutputs: d.OutputTargetDocsReadme[], - docsData: d.JsonDocsComponent, - cmps: d.JsonDocsComponent[], -) => { - const isUpdate = !!docsData.readme; - const userContent = isUpdate ? docsData.readme : getDefaultReadme(docsData); - - await Promise.all( - readmeOutputs.map(async (readmeOutput) => { - if (readmeOutput.dir) { - const relativeReadmePath = relative(config.srcDir, docsData.readmePath); - const readmeOutputPath = join(readmeOutput.dir, relativeReadmePath); - - const currentReadmeContent = - readmeOutput.overwriteExisting === true - ? // Overwrite explicitly requested: always use the provided user content. - userContent - : normalizePath(readmeOutput.dir) !== normalizePath(config.srcDir) - ? (readmeOutput.overwriteExisting === 'if-missing' && - // Validate a file exists at the output path - (await compilerCtx.fs.access(readmeOutputPath))) || - // False and undefined case: follow the changes made in #5648 - (readmeOutput.overwriteExisting ?? false) === false - ? // Existing file found: The user set a custom `.dir` property, which is - // where we're going to write the updated README. We need to read the - // non-automatically generated content from that file and preserve that. - await getUserReadmeContent(compilerCtx, readmeOutputPath) - : // No existing file found: use the provided user content. - userContent - : // Default case: writing to srcDir, so use the provided user content. - userContent; - - // CSS Custom Properties preservation is now handled centrally in outputDocs - const readmeContent = generateMarkdown(currentReadmeContent, docsData, cmps, readmeOutput, config); - - const results = await compilerCtx.fs.writeFile(readmeOutputPath, readmeContent); - if (results.changedContent) { - if (isUpdate) { - config.logger.info(`updated readme docs: ${docsData.tag}`); - } else { - config.logger.info(`created readme docs: ${docsData.tag}`); - } - } - } - }), - ); -}; - -export const generateMarkdown = ( - userContent: string | undefined, - cmp: d.JsonDocsComponent, - cmps: d.JsonDocsComponent[], - readmeOutput: d.OutputTargetDocsReadme, - config?: d.ValidatedConfig, -) => { - //If the readmeOutput.dependencies is true or undefined the dependencies will be generated. - const dependencies = readmeOutput.dependencies !== false ? depsToMarkdown(cmp, cmps, config) : []; - - return [ - userContent || '', - AUTO_GENERATE_COMMENT, - '', - '', - ...getDocsDeprecation(cmp), - ...overviewToMarkdown(cmp.overview), - ...usageToMarkdown(cmp.usage), - ...propsToMarkdown(cmp.props), - ...eventsToMarkdown(cmp.events), - ...methodsToMarkdown(cmp.methods), - ...slotsToMarkdown(cmp.slots), - ...partsToMarkdown(cmp.parts), - ...customStatesToMarkdown(cmp.customStates), - ...stylesToMarkdown(cmp.styles), - ...dependencies, - `----------------------------------------------`, - '', - readmeOutput.footer, - '', - ].join('\n'); -}; - -const getDocsDeprecation = (cmp: d.JsonDocsComponent) => { - if (cmp.deprecation !== undefined) { - return [`> **[DEPRECATED]** ${cmp.deprecation}`, '']; - } - return []; -}; - -/** - * Get a minimal default README for a Stencil component - * - * @param docsData documentation data for the component of interest - * @returns a minimal README template for that component - */ -const getDefaultReadme = (docsData: d.JsonDocsComponent) => { - return [`# ${docsData.tag}`, '', '', ''].join('\n'); -}; - -/** - * Extract the existing CSS Custom Properties section from a README file. - * This is used to preserve CSS props documentation when running `stencil docs` - * without building styles. - * - * @param compilerCtx the current compiler context - * @param readmePath the path to the README file to read - * @returns array of CSS custom properties styles, or undefined if none found - */ -export const extractExistingCssProps = async ( - compilerCtx: d.CompilerCtx, - readmePath: string, -): Promise => { - try { - const existingContent = await compilerCtx.fs.readFile(readmePath); - - // Find the CSS Custom Properties section - const cssPropsSectionMatch = existingContent.match( - /## CSS Custom Properties\s*\n\s*\n([\s\S]*?)(?=\n##|\n-{4,}|$)/, - ); - if (!cssPropsSectionMatch) { - return undefined; - } - - const cssPropsSection = cssPropsSectionMatch[1]; - const styles: d.JsonDocsStyle[] = []; - - // Parse the markdown table to extract CSS custom properties - // Table format: - // | Name | Description | - // | ---- | ----------- | - // | `--prop-name` | Description text | - const lines = cssPropsSection.split('\n'); - let inTable = false; - - for (const line of lines) { - const trimmedLine = line.trim(); - - // Skip header and separator rows - if (trimmedLine.startsWith('| Name') || trimmedLine.startsWith('| ---')) { - inTable = true; - continue; - } - - // Parse table rows - if (inTable && trimmedLine.startsWith('|')) { - const parts = trimmedLine - .split('|') - .map((p) => p.trim()) - .filter((p) => p); - if (parts.length >= 2) { - // Extract the CSS variable name (remove backticks) - const name = parts[0].replace(/`/g, '').trim(); - const docs = parts[1].trim(); - - if (name.startsWith('--')) { - styles.push({ - name, - docs, - annotation: 'prop', - mode: undefined, - }); - } - } - } - } - - return styles.length > 0 ? styles : undefined; - } catch (e) { - return undefined; - } -}; diff --git a/src/compiler/docs/test/output-docs.spec.ts b/src/compiler/docs/test/output-docs.spec.ts deleted file mode 100644 index 898c5829d21..00000000000 --- a/src/compiler/docs/test/output-docs.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type * as d from '../../../declarations'; -import { generateMarkdown } from '../readme/output-docs'; - -describe('css-props to markdown', () => { - describe('generateMarkdown', () => { - const mockReadmeOutput: d.OutputTargetDocsReadme = { - type: 'docs-readme', - footer: '*Built with StencilJS*', - }; - - const mockComponent: d.JsonDocsComponent = { - tag: 'my-component', - filePath: 'src/components/my-component/my-component.tsx', - fileName: 'my-component.tsx', - dirPath: 'src/components/my-component', - readmePath: 'src/components/my-component/readme.md', - usagesDir: 'src/components/my-component/usage', - encapsulation: 'shadow', - docs: '', - docsTags: [], - usage: {}, - props: [], - methods: [], - events: [], - listeners: [], - styles: [], - slots: [], - parts: [], - dependents: [], - dependencies: [], - dependencyGraph: {}, - customStates: [], - readme: '', - }; - - it.each([ - { - name: 'component styles when available', - componentStyles: [ - { name: '--background', docs: 'Background color', annotation: 'prop' as const, mode: undefined }, - { name: '--color', docs: 'Text color', annotation: 'prop' as const, mode: undefined }, - ], - shouldContain: ['## CSS Custom Properties', '`--background`', 'Background color', '`--color`', 'Text color'], - shouldNotContain: [], - }, - { - name: 'preserved CSS props (already in component.styles)', - componentStyles: [ - { - name: '--bg', - docs: 'Defaults to var(--nano-color-blue-cerulean-1000);', - annotation: 'prop' as const, - mode: undefined, - }, - { name: '--text-color', docs: 'Text color of the component', annotation: 'prop' as const, mode: undefined }, - ], - shouldContain: ['## CSS Custom Properties', '`--bg`', 'Defaults to var(--nano-color-blue-cerulean-1000);'], - shouldNotContain: [], - }, - { - name: 'no CSS section when styles are empty', - componentStyles: [], - shouldContain: [], - shouldNotContain: ['## CSS Custom Properties'], - }, - { - name: 'updated component styles', - componentStyles: [ - { name: '--new-prop', docs: 'New property from build', annotation: 'prop' as const, mode: undefined }, - ], - shouldContain: ['`--new-prop`', 'New property from build'], - shouldNotContain: [], - }, - ])('should use $name', ({ componentStyles, shouldContain, shouldNotContain }) => { - const component: d.JsonDocsComponent = { - ...mockComponent, - styles: componentStyles, - }; - - const markdown = generateMarkdown('# my-component', component, [], mockReadmeOutput); - - shouldContain.forEach((expected) => { - expect(markdown).toContain(expected); - }); - - shouldNotContain.forEach((unexpected) => { - expect(markdown).not.toContain(unexpected); - }); - }); - - it('should escape special characters in CSS prop descriptions', () => { - const component: d.JsonDocsComponent = { - ...mockComponent, - styles: [ - { - name: '--bg', - docs: 'Defaults to var(--nano-color-blue-cerulean-1000); with | pipes', - annotation: 'prop', - mode: undefined, - }, - ], - }; - - const markdown = generateMarkdown('# my-component', component, [], mockReadmeOutput); - - // Pipe characters are escaped in markdown tables - expect(markdown).toContain('Defaults to var(--nano-color-blue-cerulean-1000); with \\| pipes'); - }); - }); -}); diff --git a/src/compiler/docs/test/tsconfig.json b/src/compiler/docs/test/tsconfig.json deleted file mode 100644 index 0ff0be83173..00000000000 --- a/src/compiler/docs/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/docs/vscode/index.ts b/src/compiler/docs/vscode/index.ts deleted file mode 100644 index 41a1794110f..00000000000 --- a/src/compiler/docs/vscode/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { isOutputTargetDocsVscode, join } from '@utils'; - -import type * as d from '../../../declarations'; -import { getNameText } from '../generate-doc-data'; - -/** - * Generate [custom data](https://github.com/microsoft/vscode-custom-data) to augment existing HTML types in VS Code. - * This function writes the custom data as a JSON file to disk, which can be used in VS Code to inform the IDE about - * custom elements generated by Stencil. - * - * The JSON generated by this function must conform to the - * [HTML custom data schema](https://github.com/microsoft/vscode-html-languageservice/blob/e7ae8a7170df5e721a13cee1b86e293b24eb3b20/docs/customData.schema.json). - * - * This function generates custom data for HTML only at this time (it does not generate custom data for CSS). - * - * @param compilerCtx the current compiler context - * @param docsData an intermediate representation documentation derived from compiled Stencil components - * @param outputTargets the output target(s) the associated with the current build - */ -export const generateVscodeDocs = async ( - compilerCtx: d.CompilerCtx, - docsData: d.JsonDocs, - outputTargets: d.OutputTarget[], -): Promise => { - const vsCodeOutputTargets = outputTargets.filter(isOutputTargetDocsVscode); - if (vsCodeOutputTargets.length === 0) { - return; - } - - await Promise.all( - vsCodeOutputTargets.map(async (outputTarget: d.OutputTargetDocsVscode): Promise => { - const json = { - /** - * the 'version' top-level field is required by the schema. changes to the JSON generated by Stencil must: - * - comply with v1.X of the schema _OR_ - * - increment this field as a part of updating the JSON generation. This should be considered a breaking change - * - * {@link https://github.com/microsoft/vscode-html-languageservice/blob/e7ae8a7170df5e721a13cee1b86e293b24eb3b20/src/htmlLanguageTypes.ts#L184} - */ - version: 1.1, - tags: docsData.components.map((cmp: d.JsonDocsComponent) => ({ - name: cmp.tag, - description: { - kind: 'markdown', - value: cmp.docs, - }, - attributes: cmp.props - .filter((p: d.JsonDocsProp): p is DocPropWithAttribute => p.attr !== undefined && p.attr.length > 0) - .map(serializeAttribute), - references: getReferences(cmp, outputTarget.sourceCodeBaseUrl), - })), - }; - - // fields in the custom data may have a value of `undefined`. calling `stringify` will remove such fields. - const jsonContent = JSON.stringify(json, null, 2); - await compilerCtx.fs.writeFile(outputTarget.file, jsonContent); - }), - ); -}; - -/** - * This type describes external references for a custom element. - * - * An internal representation of Microsoft/VS Code's [`IReference` type](https://github.com/microsoft/vscode-html-languageservice/blob/e7ae8a7170df5e721a13cee1b86e293b24eb3b20/src/htmlLanguageTypes.ts#L153). - */ -type TagReference = { - name: string; - url: string; -}; - -/** - * Generate a 'references' section for a component's documentation. - * @param cmp the Stencil component to generate a references section for - * @param repoBaseUrl an optional URL, that when provided, will add a reference to the source code for the component - * @returns the generated references section, or undefined if no references could be generated - */ -const getReferences = (cmp: d.JsonDocsComponent, repoBaseUrl: string | undefined): TagReference[] | undefined => { - // collect any `@reference` JSDoc tags on the component - const references = getNameText('reference', cmp.docsTags).map(([name, url]) => ({ name, url })); - - if (repoBaseUrl) { - references.push({ - name: 'Source code', - url: join(repoBaseUrl, cmp.filePath ?? ''), - }); - } - if (references.length > 0) { - return references; - } - return undefined; -}; - -/** - * A type that describes the attributes that can be used with a custom element. - * - * An internal representation of Microsoft/VS Code's [`IAttributeData` type](https://github.com/microsoft/vscode-html-languageservice/blob/e7ae8a7170df5e721a13cee1b86e293b24eb3b20/src/htmlLanguageTypes.ts#L165). - */ -type AttributeData = { - name: string; - description: string; - values?: { name: string }[]; -}; - -/** - * Utility that provides a type-safe way of making a key K on a type T required. - * - * This is preferable than using an intersection of `T & {K: someType}` as it ensures that: - * - the type of K will always match the type T[K] - * - it should error should K not exist in `keyof T` - */ -type WithRequired = T & { [P in K]-?: T[P] }; - -/** - * A `@Prop` documentation type with a required 'attr' field - */ -type DocPropWithAttribute = WithRequired; - -/** - * Serialize a component's class member decorated with `@Prop` to be written to disk - * @param prop the intermediate representation of the documentation to serialize - * @returns the serialized data - */ -const serializeAttribute = (prop: DocPropWithAttribute): AttributeData => { - const attribute: AttributeData = { - name: prop.attr, - description: prop.docs, - }; - const values = prop.values - .filter( - (jsonDocValue: d.JsonDocsValue): jsonDocValue is Required => - jsonDocValue.type === 'string' && jsonDocValue.value !== undefined, - ) - .map((jsonDocValue: Required) => ({ name: jsonDocValue.value })); - - if (values.length > 0) { - attribute.values = values; - } - return attribute; -}; diff --git a/src/compiler/entries/component-graph.ts b/src/compiler/entries/component-graph.ts deleted file mode 100644 index 41eee6df6c6..00000000000 --- a/src/compiler/entries/component-graph.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type * as d from '../../declarations'; -import { getScopeId } from '../style/scope-css'; - -export const generateModuleGraph = (cmps: d.ComponentCompilerMeta[], bundleModules: ReadonlyArray) => { - const cmpMap = new Map(); - cmps.forEach((cmp) => { - const bundle = bundleModules.find((b) => b.cmps.includes(cmp)); - if (bundle) { - // add default case for no mode - cmpMap.set(getScopeId(cmp.tagName), bundle.rollupResult.imports); - } - }); - - return cmpMap; -}; diff --git a/src/compiler/events.ts b/src/compiler/events.ts deleted file mode 100644 index 7cdb280d035..00000000000 --- a/src/compiler/events.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type * as d from '../declarations'; - -export const buildEvents = (): d.BuildEvents => { - const evCallbacks: EventCallback[] = []; - - const off = (callback: any) => { - const index = evCallbacks.findIndex((ev) => ev.callback === callback); - if (index > -1) { - evCallbacks.splice(index, 1); - return true; - } - return false; - }; - - const on = (arg0: any, arg1?: any): d.BuildOnEventRemove => { - if (typeof arg0 === 'function') { - const eventName: null = null; - const callback = arg0; - evCallbacks.push({ - eventName, - callback, - }); - return () => off(callback); - } else if (typeof arg0 === 'string' && typeof arg1 === 'function') { - const eventName = arg0.toLowerCase().trim(); - const callback = arg1; - - evCallbacks.push({ - eventName, - callback, - }); - - return () => off(callback); - } - return () => false; - }; - - const emit = (eventName: d.CompilerEventName, data: any) => { - const normalizedEventName = eventName.toLowerCase().trim(); - const callbacks = evCallbacks.slice(); - - for (const ev of callbacks) { - if (ev.eventName == null) { - try { - ev.callback(eventName, data); - } catch (e) { - console.error(e); - } - } else if (ev.eventName === normalizedEventName) { - try { - ev.callback(data); - } catch (e) { - console.error(e); - } - } - } - }; - - const unsubscribeAll = () => { - evCallbacks.length = 0; - }; - - return { - emit, - on, - unsubscribeAll, - }; -}; - -interface EventCallback { - eventName: string | null; - callback: Function; -} diff --git a/src/compiler/fs-watch/fs-watch-rebuild.ts b/src/compiler/fs-watch/fs-watch-rebuild.ts deleted file mode 100644 index d355d68919b..00000000000 --- a/src/compiler/fs-watch/fs-watch-rebuild.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { isOutputTargetDocsJson, isOutputTargetDocsVscode, isOutputTargetStats, isString, unique } from '@utils'; -import { basename } from 'path'; - -import type * as d from '../../declarations'; - -export const filesChanged = (buildCtx: d.BuildCtx) => { - // files changed include updated, added and deleted - return unique([...buildCtx.filesUpdated, ...buildCtx.filesAdded, ...buildCtx.filesDeleted]).sort(); -}; - -/** - * Unary helper function mapping string to string and wrapping `basename`, - * which normally takes two string arguments. This means it cannot be passed - * to `Array.prototype.map`, but this little helper can! - * - * @param filePath a filepath to check out - * @returns the basename for that filepath - */ -const unaryBasename = (filePath: string): string => basename(filePath); - -/** - * Get the file extension for a path - * - * @param filePath a path - * @returns the file extension (well, characters after the last `'.'`) or - * `null` if no extension exists. - */ -const getExt = (filePath: string): string | null => { - const fileParts = filePath.split('.'); - - return fileParts.length > 1 ? fileParts.pop()!.toLowerCase() : null; -}; - -/** - * Script extensions which we want to be able to recognize - */ -const SCRIPT_EXT = ['ts', 'tsx', 'js', 'jsx']; - -/** - * Helper to check if a filepath has a script extension - * - * @param filePath a file extension - * @returns whether the filepath has a script extension or not - */ -export const hasScriptExt = (filePath: string): boolean => { - const ext = getExt(filePath); - - return ext ? SCRIPT_EXT.includes(ext) : false; -}; - -const STYLE_EXT = ['css', 'scss', 'sass', 'pcss', 'styl', 'stylus', 'less']; - -/** - * Helper to check if a filepath has a style extension - * - * @param filePath a file extension to check - * @returns whether the filepath has a style extension or not - */ -export const hasStyleExt = (filePath: string): boolean => { - const ext = getExt(filePath); - - return ext ? STYLE_EXT.includes(ext) : false; -}; - -/** - * Get all scripts from a build context that were added - * - * @param buildCtx the build context - * @returns an array of filepaths that were added - */ -export const scriptsAdded = (buildCtx: d.BuildCtx): string[] => - buildCtx.filesAdded.filter(hasScriptExt).map(unaryBasename); - -/** - * Get all scripts from a build context that were deleted - * - * @param buildCtx the build context - * @returns an array of deleted filepaths - */ -export const scriptsDeleted = (buildCtx: d.BuildCtx): string[] => - buildCtx.filesDeleted.filter(hasScriptExt).map(unaryBasename); - -/** - * Check whether a build has script changes - * - * @param buildCtx the build context - * @returns whether or not there are script changes - */ -export const hasScriptChanges = (buildCtx: d.BuildCtx): boolean => buildCtx.filesChanged.some(hasScriptExt); - -/** - * Check whether a build has style changes - * - * @param buildCtx the build context - * @returns whether or not there are style changes - */ -export const hasStyleChanges = (buildCtx: d.BuildCtx): boolean => buildCtx.filesChanged.some(hasStyleExt); - -/** - * Check whether a build has html changes - * - * @param config the current config - * @param buildCtx the build context - * @returns whether or not HTML files were changed - */ -export const hasHtmlChanges = (config: d.ValidatedConfig, buildCtx: d.BuildCtx): boolean => { - const anyHtmlChanged = buildCtx.filesChanged.some((f) => f.toLowerCase().endsWith('.html')); - - if (anyHtmlChanged) { - // any *.html in any directory that changes counts and rebuilds - return true; - } - - const srcIndexHtmlChanged = buildCtx.filesChanged.some((fileChanged) => { - // the src index index.html file has changed - // this file name could be something other than index.html - return fileChanged === config.srcIndexHtml; - }); - - return srcIndexHtmlChanged; -}; - -export const updateCacheFromRebuild = (compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - buildCtx.filesChanged.forEach((filePath) => { - compilerCtx.fs.clearFileCache(filePath); - }); - - buildCtx.dirsAdded.forEach((dirAdded) => { - compilerCtx.fs.clearDirCache(dirAdded); - }); - - buildCtx.dirsDeleted.forEach((dirDeleted) => { - compilerCtx.fs.clearDirCache(dirDeleted); - }); -}; - -/** - * Checks if a path is ignored by the watch configuration - * - * @param config The validated config for the Stencil project - * @param path The path to check - * @returns Whether the path is ignored by the watch configuration - */ -export const isWatchIgnorePath = (config: d.ValidatedConfig, path: string) => { - if (!isString(path)) { - return false; - } - - const isWatchIgnore = (config.watchIgnoredRegex as RegExp[]).some((reg) => reg.test(path)); - if (isWatchIgnore) { - return true; - } - const outputTargets = config.outputTargets; - const ignoreFiles = [ - ...outputTargets.filter(isOutputTargetDocsJson).map((o) => o.file), - ...outputTargets.filter(isOutputTargetDocsJson).map((o) => o.typesFile), - ...outputTargets.filter(isOutputTargetStats).map((o) => o.file), - ...outputTargets.filter(isOutputTargetDocsVscode).map((o) => o.file), - ]; - if (ignoreFiles.includes(path)) { - return true; - } - - return false; -}; diff --git a/src/compiler/html/add-script-attr.ts b/src/compiler/html/add-script-attr.ts deleted file mode 100644 index 007056bf382..00000000000 --- a/src/compiler/html/add-script-attr.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { join } from '@utils'; - -import type * as d from '../../declarations'; -import { getAbsoluteBuildDir } from './html-utils'; - -export const addScriptDataAttribute = (config: d.ValidatedConfig, doc: Document, outputTarget: d.OutputTargetWww) => { - const resourcesUrl = getAbsoluteBuildDir(outputTarget); - const entryEsmFilename = `${config.fsNamespace}.esm.js`; - const entryNoModuleFilename = `${config.fsNamespace}.js`; - const expectedEsmSrc = join(resourcesUrl, entryEsmFilename); - const expectedNoModuleSrc = join(resourcesUrl, entryNoModuleFilename); - - const scripts = Array.from(doc.querySelectorAll('script')); - const scriptEsm = scripts.find((s) => s.getAttribute('src') === expectedEsmSrc); - const scriptNomodule = scripts.find((s) => s.getAttribute('src') === expectedNoModuleSrc); - - if (scriptEsm) { - scriptEsm.setAttribute('data-stencil', ''); - } - if (scriptNomodule) { - scriptNomodule.setAttribute('data-stencil', ''); - } -}; diff --git a/src/compiler/html/inject-module-preloads.ts b/src/compiler/html/inject-module-preloads.ts deleted file mode 100644 index 9d3d6e4eada..00000000000 --- a/src/compiler/html/inject-module-preloads.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { join } from '@utils'; - -import type * as d from '../../declarations'; -import { getAbsoluteBuildDir } from './html-utils'; - -export const optimizeCriticalPath = (doc: Document, criticalBundlers: string[], outputTarget: d.OutputTargetWww) => { - const buildDir = getAbsoluteBuildDir(outputTarget); - const paths = criticalBundlers.map((path) => join(buildDir, path)); - injectModulePreloads(doc, paths); -}; - -export const injectModulePreloads = (doc: Document, paths: string[]) => { - const existingLinks = (Array.from(doc.querySelectorAll('link[rel=modulepreload]')) as HTMLLinkElement[]).map((link) => - link.getAttribute('href'), - ); - - const addLinks = paths.filter((path) => !existingLinks.includes(path)).map((path) => createModulePreload(doc, path)); - - const head = doc.head; - const firstScript = head.querySelector('script'); - if (firstScript) { - for (const link of addLinks) { - head.insertBefore(link, firstScript); - } - } else { - for (const link of addLinks) { - head.appendChild(link); - } - } -}; - -const createModulePreload = (doc: Document, href: string) => { - const link = doc.createElement('link'); - link.setAttribute('rel', 'modulepreload'); - link.setAttribute('href', href); - return link; -}; diff --git a/src/compiler/html/test/tsconfig.json b/src/compiler/html/test/tsconfig.json deleted file mode 100644 index 0ff0be83173..00000000000 --- a/src/compiler/html/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/html/update-global-styles-link.ts b/src/compiler/html/update-global-styles-link.ts deleted file mode 100644 index 29d0aeb19db..00000000000 --- a/src/compiler/html/update-global-styles-link.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { join } from '@utils'; - -import type * as d from '../../declarations'; -import { getAbsoluteBuildDir } from './html-utils'; - -export const updateGlobalStylesLink = ( - config: d.ValidatedConfig, - doc: Document, - globalScriptFilename: string, - outputTarget: d.OutputTargetWww, -) => { - if (!globalScriptFilename) { - return; - } - const buildDir = getAbsoluteBuildDir(outputTarget); - const originalPath = join(buildDir, config.fsNamespace + '.css'); - const newPath = join(buildDir, globalScriptFilename); - if (originalPath === newPath) { - return; - } - - const replacer = new RegExp(escapeRegExp(originalPath) + '$'); - - Array.from(doc.querySelectorAll('link')).forEach((link) => { - const href = link.getAttribute('href'); - if (href) { - const newHref = href.replace(replacer, newPath); - if (newHref !== href) { - link.setAttribute('href', newHref); - } - } - }); -}; - -const escapeRegExp = (text: string) => text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); diff --git a/src/compiler/index.ts b/src/compiler/index.ts deleted file mode 100644 index d6af5da098b..00000000000 --- a/src/compiler/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import ts from 'typescript'; - -export { buildId, vermoji, version, versions } from '../version'; -export { createCompiler } from './compiler'; -export { loadConfig } from './config/load-config'; -export { optimizeCss } from './optimize/optimize-css'; -export { optimizeJs } from './optimize/optimize-js'; -export { createPrerenderer } from './prerender/prerender-main'; -export { FsWriteResults } from './sys/in-memory-fs'; -export { nodeRequire } from './sys/node-require'; -export { createSystem } from './sys/stencil-sys'; -export { transpile, transpileSync } from './transpile'; -export { createWorkerContext } from './worker/worker-thread'; -export { createWorkerMessageHandler } from './worker/worker-thread'; -export { ts }; -export { validateConfig } from './config/validate-config'; diff --git a/src/compiler/optimize/autoprefixer.ts b/src/compiler/optimize/autoprefixer.ts deleted file mode 100644 index e51aa7b2939..00000000000 --- a/src/compiler/optimize/autoprefixer.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Postcss } from 'postcss'; - -import type * as d from '../../declarations'; - -type CssProcessor = ReturnType; -let cssProcessor: CssProcessor; - -/** - * Autoprefix a CSS string, adding vendor prefixes to make sure that what - * is written in the CSS will render correctly in our range of supported browsers. - * This function uses PostCSS in combination with the Autoprefix plugin to - * automatically add vendor prefixes based on a list of browsers which we want - * to support. - * - * @param cssText the text to be prefixed - * @param opts an optional param with options for Autoprefixer - * @returns a Promise wrapping some prefixed CSS as well as diagnostics - */ -export const autoprefixCss = async (cssText: string, opts: boolean | null | d.AutoprefixerOptions) => { - const output: d.OptimizeCssOutput = { - output: cssText, - diagnostics: [], - }; - - try { - const autoprefixerOpts = opts != null && typeof opts === 'object' ? opts : DEFAULT_AUTOPREFIX_OPTIONS; - - const processor = getProcessor(autoprefixerOpts); - const result = await processor.process(cssText, { map: null }); - - result.warnings().forEach((warning: any) => { - output.diagnostics.push({ - header: `Autoprefix CSS: ${warning.plugin}`, - messageText: warning.text, - level: 'warn', - type: 'css', - lines: [], - }); - }); - - output.output = result.css; - } catch (e: any) { - const diagnostic: d.Diagnostic = { - header: `Autoprefix CSS`, - messageText: `CSS Error` + e, - level: `error`, - type: `css`, - lines: [], - }; - - if (typeof e.name === 'string') { - diagnostic.header = e.name; - } - - if (typeof e.reason === 'string') { - diagnostic.messageText = e.reason; - } - - if (typeof e.source === 'string' && typeof e.line === 'number') { - const lines = (e.source as string).replace(/\r/g, '\n').split('\n'); - - if (lines.length > 0) { - const addLine = (lineNumber: number) => { - const line = lines[lineNumber]; - if (typeof line === 'string') { - const printLine: d.PrintLine = { - lineIndex: -1, - lineNumber: -1, - text: line, - errorCharStart: -1, - errorLength: -1, - }; - diagnostic.lines = diagnostic.lines || []; - diagnostic.lines.push(printLine); - } - }; - - addLine(e.line - 3); - addLine(e.line - 2); - addLine(e.line - 1); - addLine(e.line); - addLine(e.line + 1); - addLine(e.line + 2); - addLine(e.line + 3); - } - } - - output.diagnostics.push(diagnostic); - } - - return output; -}; - -/** - * Get the processor for PostCSS and the Autoprefixer plugin - * - * @param autoprefixerOpts Options for Autoprefixer - * @returns postCSS with the Autoprefixer plugin applied - */ -const getProcessor = (autoprefixerOpts: d.AutoprefixerOptions): CssProcessor => { - const { postcss, autoprefixer } = require('../sys/node/autoprefixer.js'); - if (!cssProcessor) { - cssProcessor = postcss([autoprefixer(autoprefixerOpts)]); - } - return cssProcessor; -}; - -/** - * Default options for the Autoprefixer PostCSS plugin. See the documentation: - * https://github.com/postcss/autoprefixer#options for a complete list. - * - * This default option set will: - * - * - override the default browser list (`overrideBrowserslist`) - * - turn off the visual cascade (`cascade`) - * - disable auto-removing outdated prefixes (`remove`) - * - set `flexbox` to `"no-2009"`, which limits prefixing for flexbox to the - * final and IE 10 versions of the specification - */ -const DEFAULT_AUTOPREFIX_OPTIONS: d.AutoprefixerOptions = { - overrideBrowserslist: ['last 2 versions', 'iOS >= 9', 'Android >= 4.4', 'Explorer >= 11', 'ExplorerMobile >= 11'], - cascade: false, - remove: false, - flexbox: 'no-2009', -}; diff --git a/src/compiler/optimize/optimize-css.ts b/src/compiler/optimize/optimize-css.ts deleted file mode 100644 index 8a024c5eef3..00000000000 --- a/src/compiler/optimize/optimize-css.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { hasError } from '@utils'; - -import { OptimizeCssInput, OptimizeCssOutput } from '../../declarations'; -import { autoprefixCss } from './autoprefixer'; -import { minifyCss } from './minify-css'; - -/** - * Optimize a CSS file, optionally running an autoprefixer and a minifier - * depending on the options set on the input options argument. - * - * @param inputOpts input CSS options - * @returns a promise wrapping the optimized output - */ -export const optimizeCss = async (inputOpts: OptimizeCssInput): Promise => { - let result: OptimizeCssOutput = { - output: inputOpts.input, - diagnostics: [], - }; - if (inputOpts.autoprefixer !== false && inputOpts.autoprefixer !== null) { - result = await autoprefixCss(inputOpts.input, inputOpts.autoprefixer ?? null); - if (hasError(result.diagnostics)) { - return result; - } - } - if (inputOpts.minify !== false) { - result.output = await minifyCss({ - css: result.output, - resolveUrl: inputOpts.resolveUrl, - }); - } - return result; -}; diff --git a/src/compiler/optimize/optimize-module.ts b/src/compiler/optimize/optimize-module.ts deleted file mode 100644 index d985552896d..00000000000 --- a/src/compiler/optimize/optimize-module.ts +++ /dev/null @@ -1,261 +0,0 @@ -import sourceMapMerge from 'merge-source-map'; -import type { CompressOptions, MangleOptions, ManglePropertiesOptions, MinifyOptions, SourceMapOptions } from 'terser'; -import ts from 'typescript'; - -import type { CompilerCtx, OptimizeJsResult, SourceMap, SourceTarget, ValidatedConfig } from '../../declarations'; -import { minfyJsId } from '../../version'; -import { minifyJs } from './minify-js'; - -interface OptimizeModuleOptions { - input: string; - sourceMap?: SourceMap; - sourceTarget?: SourceTarget; - isCore?: boolean; - minify?: boolean; - inlineHelpers?: boolean; - modeName?: string; -} - -/** - * Begins the process of minifying a user's JavaScript - * @param config the Stencil configuration file that was provided as a part of the build step - * @param compilerCtx the current compiler context - * @param opts minification options that specify how the JavaScript ought to be minified - * @returns the minified JavaScript result - */ -export const optimizeModule = async ( - config: ValidatedConfig, - compilerCtx: CompilerCtx, - opts: OptimizeModuleOptions, -): Promise => { - if ((!opts.minify && opts.sourceTarget !== 'es5') || opts.input === '') { - return { - output: opts.input, - diagnostics: [], - sourceMap: opts.sourceMap, - }; - } - - const isDebug = config.logLevel === 'debug'; - const cacheKey = await compilerCtx.cache.createKey('optimizeModule', minfyJsId, opts, isDebug); - const cachedContent = await compilerCtx.cache.get(cacheKey); - if (cachedContent != null) { - const cachedMap = await compilerCtx.cache.get(cacheKey + 'Map'); - return { - output: cachedContent, - diagnostics: [], - sourceMap: cachedMap ? JSON.parse(cachedMap) : null, - }; - } - - let minifyOpts: MinifyOptions; - let code = opts.input; - if (opts.isCore) { - // IS_ESM_BUILD is replaced at build time so SystemJS and esm builds have diff values - // not using the BUILD conditional since rollup would input the same value - code = code.replace(/\/\* IS_ESM_BUILD \*\//g, '&& false /* IS_SYSTEM_JS_BUILD */'); - } - - if (opts.sourceTarget === 'es5' || opts.minify) { - minifyOpts = getTerserOptions(config, opts.sourceTarget, isDebug); - if (config.sourceMap) { - minifyOpts.sourceMap = { - content: - // We need to loosely check for a source map definition - // so we don't spread a `null`/`undefined` value into the object - // which results in invalid source maps during minification - opts.sourceMap != null - ? { - ...opts.sourceMap, - version: 3, - } - : undefined, - }; - } - - const compressOpts = minifyOpts.compress as CompressOptions; - const mangleOptions = minifyOpts.mangle as MangleOptions; - - if (opts.sourceTarget !== 'es5' && opts.isCore) { - if (!isDebug) { - compressOpts.passes = 2; - compressOpts.global_defs = { - supportsListenerOptions: true, - }; - compressOpts.pure_funcs = compressOpts.pure_funcs || []; - compressOpts.pure_funcs = ['getHostRef', ...compressOpts.pure_funcs]; - } - - mangleOptions.properties = { - debug: isDebug, - ...getTerserManglePropertiesConfig(), - }; - - compressOpts.inline = 1; - compressOpts.unsafe = true; - compressOpts.unsafe_undefined = true; - } - } - - const shouldTranspile = opts.sourceTarget === 'es5'; - const results = await compilerCtx.worker.prepareModule(code, minifyOpts, shouldTranspile, !!opts.inlineHelpers); - if ( - results != null && - typeof results.output === 'string' && - results.diagnostics.length === 0 && - compilerCtx != null - ) { - if (opts.isCore) { - results.output = results.output.replace(/disconnectedCallback\(\)\{\},/g, ''); - } - await compilerCtx.cache.put(cacheKey, results.output); - if (results.sourceMap) { - await compilerCtx.cache.put(cacheKey + 'Map', JSON.stringify(results.sourceMap)); - } - } - - return results; -}; - -/** - * Builds a configuration object to be used by Terser for the purposes of minifying a user's JavaScript - * @param config the Stencil configuration file that was provided as a part of the build step - * @param sourceTarget the version of JavaScript being targeted (e.g. ES2017) - * @param prettyOutput if true, set the necessary flags to beautify the output of terser - * @returns the minification options - */ -export const getTerserOptions = ( - config: ValidatedConfig, - sourceTarget: SourceTarget, - prettyOutput: boolean, -): MinifyOptions => { - const opts: MinifyOptions = { - ie8: false, - safari10: false, - format: {}, - sourceMap: config.sourceMap, - }; - - if (sourceTarget === 'es5') { - opts.ecma = opts.format.ecma = 5; - opts.compress = false; - opts.mangle = { - properties: getTerserManglePropertiesConfig(), - }; - } else { - opts.mangle = { - properties: getTerserManglePropertiesConfig(), - }; - opts.compress = { - pure_getters: true, - keep_fargs: false, - passes: 2, - }; - - opts.ecma = opts.format.ecma = opts.compress.ecma = 2018; - opts.toplevel = true; - opts.module = true; - opts.mangle.toplevel = true; - opts.compress.arrows = true; - opts.compress.module = true; - opts.compress.toplevel = true; - } - - if (prettyOutput) { - opts.mangle = { - keep_fnames: true, - properties: getTerserManglePropertiesConfig(), - }; - opts.compress = {}; - opts.compress.drop_console = false; - opts.compress.drop_debugger = false; - opts.compress.pure_funcs = []; - opts.format.beautify = true; - opts.format.indent_level = 2; - opts.format.comments = 'all'; - } - - return opts; -}; - -/** - * Get baseline configuration for the 'properties' option for terser's mangle - * configuration. - * - * @returns an object with our baseline property mangling configuration - */ -function getTerserManglePropertiesConfig(): ManglePropertiesOptions { - const options = { - regex: '^\\$.+\\$$', - // we need to reserve this name so that it can be accessed on `hostRef` - // at runtime - reserved: ['$hostElement$'], - } satisfies ManglePropertiesOptions; - - return options; -} - -/** - * This method is likely to be called by a worker on the compiler context, rather than directly. - * @param input the source code to minify - * @param minifyOpts options to be used by the minifier - * @param transpileToEs5 if true, use the TypeScript compiler to transpile the input to ES5 prior to minification - * @param inlineHelpers when true, emits less terse JavaScript by allowing global helpers created by the TypeScript - * compiler to be added directly to the transpiled source. Used only if `transpileToEs5` is true. - * @returns minified input, as JavaScript - */ -export const prepareModule = async ( - input: string, - minifyOpts: MinifyOptions, - transpileToEs5: boolean, - inlineHelpers: boolean, -): Promise => { - const results: OptimizeJsResult = { - output: input, - diagnostics: [], - sourceMap: null, - }; - - if (transpileToEs5) { - const tsResults = ts.transpileModule(input, { - fileName: 'module.ts', - compilerOptions: { - sourceMap: !!minifyOpts.sourceMap, - allowJs: true, - target: ts.ScriptTarget.ES5, - module: ts.ModuleKind.ESNext, - removeComments: false, - isolatedModules: true, - skipLibCheck: true, - noEmitHelpers: !inlineHelpers, - importHelpers: !inlineHelpers, - }, - reportDiagnostics: false, - }); - results.output = tsResults.outputText; - - if (tsResults.sourceMapText) { - // need to merge sourcemaps at this point - const mergeMap = sourceMapMerge( - (minifyOpts.sourceMap as SourceMapOptions)?.content as SourceMap, - JSON.parse(tsResults.sourceMapText), - ); - - if (mergeMap != null) { - minifyOpts.sourceMap = { - content: { - ...mergeMap, - sources: mergeMap.sources ?? [], - version: 3, - }, - }; - } - } - } - - if (minifyOpts) { - return minifyJs(results.output, minifyOpts); - } - - return results; -}; diff --git a/src/compiler/output-targets/copy/local-copy-tasks.ts b/src/compiler/output-targets/copy/local-copy-tasks.ts deleted file mode 100644 index 128f884ddb8..00000000000 --- a/src/compiler/output-targets/copy/local-copy-tasks.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { join } from '@utils'; -import { isAbsolute } from 'path'; - -import type * as d from '../../../declarations'; - -export const getSrcAbsPath = (config: d.ValidatedConfig, src: string) => { - if (isAbsolute(src)) { - return src; - } - return join(config.srcDir, src); -}; - -export const getDestAbsPath = (src: string, destAbsPath: string, destRelPath: string) => { - if (destRelPath) { - if (isAbsolute(destRelPath)) { - return destRelPath; - } else { - return join(destAbsPath, destRelPath); - } - } - - if (isAbsolute(src)) { - throw new Error(`copy task, "dest" property must exist if "src" property is an absolute path: ${src}`); - } - - return destAbsPath; -}; diff --git a/src/compiler/output-targets/copy/output-copy.ts b/src/compiler/output-targets/copy/output-copy.ts deleted file mode 100644 index 2cf7dd446c4..00000000000 --- a/src/compiler/output-targets/copy/output-copy.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { buildError, isGlob, isOutputTargetCopy, join, normalizePath } from '@utils'; -import { minimatch } from 'minimatch'; - -import type * as d from '../../../declarations'; -import { canSkipAssetsCopy, getComponentAssetsCopyTasks } from './assets-copy-tasks'; -import { getDestAbsPath, getSrcAbsPath } from './local-copy-tasks'; - -const DEFAULT_IGNORE = [ - '**/__mocks__/**', - '**/__fixtures__/**', - '**/dist/**', - '**/.{idea,git,cache,output,temp}/**', - '**/.ds_store', - '**/.gitignore', - '**/desktop.ini', - '**/thumbs.db', -]; - -export const outputCopy = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - const outputTargets = config.outputTargets.filter(isOutputTargetCopy); - if (outputTargets.length === 0) { - return; - } - - const changedFiles = [...buildCtx.filesUpdated, ...buildCtx.filesAdded, ...buildCtx.dirsAdded]; - const copyTasks: Required[] = []; - const needsCopyAssets = !canSkipAssetsCopy(compilerCtx, buildCtx.entryModules, buildCtx.filesChanged); - outputTargets.forEach((o) => { - if (needsCopyAssets && o.copyAssets) { - copyTasks.push(...getComponentAssetsCopyTasks(config, buildCtx, o.dir, o.copyAssets === 'collection')); - } - copyTasks.push(...getCopyTasks(config, buildCtx, o, changedFiles)); - }); - - if (copyTasks.length > 0) { - const timespan = buildCtx.createTimeSpan(`copy started`); - let copiedFiles = 0; - try { - const copyResults = await config.sys.copy(copyTasks, config.srcDir); - if (copyResults != null) { - buildCtx.diagnostics.push(...copyResults.diagnostics); - compilerCtx.fs.cancelDeleteDirectoriesFromDisk(copyResults.dirPaths); - compilerCtx.fs.cancelDeleteFilesFromDisk(copyResults.filePaths); - copiedFiles = copyResults.filePaths.length; - } - } catch (e) { - const err = buildError(buildCtx.diagnostics); - if (e instanceof Error) { - err.messageText = e.message; - } - } - timespan.finish(`copy finished (${copiedFiles} file${copiedFiles === 1 ? '' : 's'})`); - } -}; - -const getCopyTasks = ( - config: d.ValidatedConfig, - buildCtx: d.BuildCtx, - o: d.OutputTargetCopy, - changedFiles: string[], -) => { - if (!Array.isArray(o.copy)) { - return []; - } - const copyTasks = - !buildCtx.isRebuild || buildCtx.requiresFullBuild ? o.copy : filterCopyTasks(config, o.copy, changedFiles); - - return copyTasks.map((t) => transformToAbs(t, o.dir)); -}; - -const filterCopyTasks = (config: d.ValidatedConfig, tasks: d.CopyTask[], changedFiles: string[]) => { - if (Array.isArray(tasks)) { - return tasks.filter((copy) => { - let copySrc = copy.src; - if (isGlob(copySrc)) { - // test the glob - copySrc = join(config.srcDir, copySrc); - if (changedFiles.some(minimatch.filter(copySrc))) { - return true; - } - } else { - copySrc = normalizePath(getSrcAbsPath(config, copySrc + '/')); - if (changedFiles.some((f) => f.startsWith(copySrc))) { - return true; - } - } - return false; - }); - } - return []; -}; - -const transformToAbs = (copyTask: d.CopyTask, dest: string): Required => { - return { - src: copyTask.src, - dest: getDestAbsPath(copyTask.src, dest, copyTask.dest), - ignore: copyTask.ignore || DEFAULT_IGNORE, - keepDirStructure: - typeof copyTask.keepDirStructure === 'boolean' ? copyTask.keepDirStructure : copyTask.dest == null, - warn: copyTask.warn !== false, - }; -}; diff --git a/src/compiler/output-targets/dist-collection/index.ts b/src/compiler/output-targets/dist-collection/index.ts deleted file mode 100644 index 80061defb4b..00000000000 --- a/src/compiler/output-targets/dist-collection/index.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { - catchError, - COLLECTION_MANIFEST_FILE_NAME, - flatOne, - generatePreamble, - isOutputTargetDistCollection, - join, - normalizePath, - relative, - sortBy, -} from '@utils'; -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { typescriptVersion, version } from '../../../version'; -import { mapImportsToPathAliases } from '../../transformers/map-imports-to-path-aliases'; - -/** - * Main output target function for `dist-collection`. This function takes the compiled output from a - * {@link ts.Program}, runs each file through a transformer to transpile import path aliases, and then writes - * the output code and source maps to disk in the specified collection directory. - * - * @param config The validated Stencil config. - * @param compilerCtx The current compiler context. - * @param buildCtx The current build context. - * @param changedModuleFiles The changed modules returned from the TS compiler. - * @returns An empty promise. Resolved once all functions finish. - */ -export const outputCollection = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - changedModuleFiles: d.Module[], -): Promise => { - const outputTargets = config.outputTargets.filter(isOutputTargetDistCollection); - if (outputTargets.length === 0) { - return; - } - - const bundlingEventMessage = `generate collections${config.sourceMap ? ' + source maps' : ''}`; - const timespan = buildCtx.createTimeSpan(`${bundlingEventMessage} started`, true); - try { - await Promise.all( - changedModuleFiles.map(async (mod) => { - let code = mod.staticSourceFileText; - if (config.preamble) { - code = `${generatePreamble(config)}\n${code}`; - } - const mapCode = mod.sourceMapFileText; - - await Promise.all( - outputTargets.map(async (target) => { - const relPath = relative(config.srcDir, mod.jsFilePath); - const filePath = join(target.collectionDir, relPath); - - // Transpile the already transpiled modules to apply - // a transformer to convert aliased import paths to relative paths - // We run this even if the transformer will perform no action - // to avoid race conditions between multiple output targets that - // may be writing to the same location - const { outputText } = ts.transpileModule(code, { - fileName: mod.sourceFilePath, - compilerOptions: { - target: ts.ScriptTarget.Latest, - }, - transformers: { - after: [mapImportsToPathAliases(config, filePath, target)], - }, - }); - - await compilerCtx.fs.writeFile(filePath, outputText, { outputTargetType: target.type }); - - if (mod.sourceMapPath) { - const relativeSourceMapPath = relative(config.srcDir, mod.sourceMapPath); - const sourceMapOutputFilePath = join(target.collectionDir, relativeSourceMapPath); - await compilerCtx.fs.writeFile(sourceMapOutputFilePath, mapCode, { outputTargetType: target.type }); - } - }), - ); - }), - ); - - await writeCollectionManifests(config, compilerCtx, buildCtx, outputTargets); - } catch (e: any) { - catchError(buildCtx.diagnostics, e); - } - - timespan.finish(`${bundlingEventMessage} finished`); -}; - -const writeCollectionManifests = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTargets: d.OutputTargetDistCollection[], -) => { - const collectionData = JSON.stringify(serializeCollectionManifest(config, compilerCtx, buildCtx), null, 2); - return Promise.all(outputTargets.map((o) => writeCollectionManifest(compilerCtx, collectionData, o))); -}; - -// this maps the json data to our internal data structure -// mapping is so that the internal data structure "could" -// change, but the external user data will always use the same api -// over the top lame mapping functions is basically so we can loosely -// couple core component meta data between specific versions of the compiler -const writeCollectionManifest = async ( - compilerCtx: d.CompilerCtx, - collectionData: string, - outputTarget: d.OutputTargetDistCollection, -) => { - // get the absolute path to the directory where the collection will be saved - const { collectionDir } = outputTarget; - - // create an absolute file path to the actual collection json file - const collectionFilePath = join(collectionDir, COLLECTION_MANIFEST_FILE_NAME); - - // don't bother serializing/writing the collection if we're not creating a distribution - await compilerCtx.fs.writeFile(collectionFilePath, collectionData); -}; - -const serializeCollectionManifest = (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - // create the single collection we're going to fill up with data - const collectionManifest: d.CollectionManifest = { - entries: buildCtx.moduleFiles - .filter((mod) => !mod.isCollectionDependency && mod.cmps.length > 0) - .map((mod) => relative(config.srcDir, mod.jsFilePath)), - // Include mixin/abstract class modules that can be extended by consuming projects - // These are modules with Stencil static members but no @Component decorator - mixins: buildCtx.moduleFiles - .filter((mod) => !mod.isCollectionDependency && mod.hasExportableMixins && mod.cmps.length === 0) - .map((mod) => relative(config.srcDir, mod.jsFilePath)), - compiler: { - name: '@stencil/core', - version, - typescriptVersion, - }, - collections: serializeCollectionDependencies(compilerCtx), - bundles: config.bundles.map((b) => ({ - components: b.components.slice().sort(), - })), - }; - if (config.globalScript) { - const mod = compilerCtx.moduleMap.get(normalizePath(config.globalScript)); - if (mod) { - collectionManifest.global = relative(config.srcDir, mod.jsFilePath); - } - } - return collectionManifest; -}; - -const serializeCollectionDependencies = (compilerCtx: d.CompilerCtx): d.CollectionDependencyData[] => { - const collectionDeps = compilerCtx.collections.map((c) => ({ - name: c.collectionName, - tags: flatOne(c.moduleFiles.map((m) => m.cmps)) - .map((cmp) => cmp.tagName) - .sort(), - })); - - return sortBy(collectionDeps, (item) => item.name); -}; diff --git a/src/compiler/output-targets/dist-custom-elements/custom-elements-build-conditionals.ts b/src/compiler/output-targets/dist-custom-elements/custom-elements-build-conditionals.ts deleted file mode 100644 index bf1ad250b09..00000000000 --- a/src/compiler/output-targets/dist-custom-elements/custom-elements-build-conditionals.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { isOutputTargetHydrate } from '@utils'; - -import type * as d from '../../../declarations'; -import { getBuildFeatures, updateBuildConditionals } from '../../app-core/app-data'; -/** - * Get build conditions appropriate for the `dist-custom-elements` Output - * Target, including disabling lazy loading and hydration. - * - * @param config a validated user-supplied config - * @param cmps metadata about the components currently being compiled - * @returns build conditionals appropriate for the `dist-custom-elements` OT - */ -export const getCustomElementsBuildConditionals = ( - config: d.ValidatedConfig, - cmps: d.ComponentCompilerMeta[], -): d.BuildConditionals => { - // because custom elements bundling does not customize the build conditionals by default - // then the default in "import { BUILD, NAMESPACE } from '@stencil/core/internal/app-data'" - // needs to have the static build conditionals set for the custom elements build - const build = getBuildFeatures(cmps) as d.BuildConditionals; - const hasHydrateOutputTargets = config.outputTargets.some(isOutputTargetHydrate); - - build.lazyLoad = false; - build.hydrateClientSide = hasHydrateOutputTargets; - build.hydrateServerSide = false; - build.asyncQueue = config.taskQueue === 'congestionAsync'; - build.taskQueue = config.taskQueue !== 'immediate'; - build.initializeNextTick = config.extras.initializeNextTick; - - updateBuildConditionals(config, build); - build.devTools = false; - - return build; -}; diff --git a/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts b/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts deleted file mode 100644 index efe203767b6..00000000000 --- a/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { dashToPascalCase, isOutputTargetDistCustomElements, join, normalizePath, relative } from '@utils'; -import { dirname } from 'path'; - -import type * as d from '../../../declarations'; - -/** - * Entrypoint for generating types for one or more `dist-custom-elements` output targets defined in a Stencil project's - * configuration - * @param config the Stencil configuration associated with the project being compiled - * @param compilerCtx the current compiler context - * @param buildCtx the context associated with the current build - * @param typesDir the path to the directory where type declarations are saved - */ -export const generateCustomElementsTypes = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - typesDir: string, -): Promise => { - const outputTargets = config.outputTargets.filter(isOutputTargetDistCustomElements); - - await Promise.all( - outputTargets.map((outputTarget) => - generateCustomElementsTypesOutput(config, compilerCtx, buildCtx, typesDir, outputTarget), - ), - ); -}; - -/** - * Generates types for a single `dist-custom-elements` output target definition in a Stencil project's configuration - * - * @param config the Stencil configuration associated with the project being compiled - * @param compilerCtx the current compiler context - * @param buildCtx the context associated with the current build - * @param typesDir path to the directory where type declarations are saved - * @param outputTarget the output target for which types are being currently generated - */ -const generateCustomElementsTypesOutput = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - typesDir: string, - outputTarget: d.OutputTargetDistCustomElements, -) => { - const isBarrelExport = outputTarget.customElementsExportBehavior === 'single-export-module'; - const isBundleExport = outputTarget.customElementsExportBehavior === 'bundle'; - - // the path where we're going to write the typedef for the whole dist-custom-elements output - const customElementsDtsPath = join(outputTarget.dir!, 'index.d.ts'); - // the directory where types for the individual components are written - const componentsTypeDirectoryRelPath = relative(outputTarget.dir!, typesDir); - - const components = buildCtx.components.filter((m) => !m.isCollectionDependency); - - const componentsDtsRelPath = relDts(outputTarget.dir!, join(typesDir, 'components.d.ts')); - - // Check for user's index.ts to augment it with Stencil types - const usersIndexJsPath = join(config.srcDir, 'index.ts'); - const hasUserIndex = await compilerCtx.fs.access(usersIndexJsPath); - const userIndexRelPath = hasUserIndex ? normalizePath(dirname(componentsDtsRelPath)) : null; - - const code = [ - // To mirror the index.js file and only export the typedefs for the - // entities exported there, we will re-export the typedefs iff - // the `customElementsExportBehavior` is set to barrel component exports - ...(isBarrelExport - ? [ - `/* ${config.namespace} custom elements */`, - ...components.map((component) => { - const exportName = dashToPascalCase(component.tagName); - const importName = component.componentClassName; - - // typedefs for individual components can be found under paths like - // $TYPES_DIR/components/my-component/my-component.d.ts - // - // To construct this path we: - // - // - get the relative path to the component's source file from the source directory - // - join that relative path to the relative path from the `index.d.ts` file to the - // directory where typedefs are saved - const componentSourceRelPath = relative(config.srcDir, component.sourceFilePath).replace(/\.tsx$/, ''); - const componentDTSPath = join(componentsTypeDirectoryRelPath, componentSourceRelPath); - - const defineFunctionExportName = `defineCustomElement${exportName}`; - // Get the path to the sibling typedef file for the current component - // When we bundle the code to generate the component JS files it generates - // the JS and typedef files based on the component tag name. So, we can - // just use the tagname to create the relative path - const localComponentTypeDefFilePath = `./${component.tagName}`; - - return [ - `export { ${importName} as ${exportName} } from '${ - componentDTSPath.startsWith('.') ? componentDTSPath : './' + componentDTSPath - }';`, - // We need to alias each `defineCustomElement` function typedef to match the aliased name given to the - // function in the `index.js` - `export { defineCustomElement as ${defineFunctionExportName} } from '${localComponentTypeDefFilePath}';`, - ].join('\n'); - }), - ``, - ] - : []), - `/**`, - ` * Get the base path to where the assets can be found. Use "setAssetPath(path)"`, - ` * if the path needs to be customized.`, - ` */`, - `export declare const getAssetPath: (path: string) => string;`, - ``, - `/**`, - ` * Used to manually set the base path where assets can be found.`, - ` * If the script is used as "module", it's recommended to use "import.meta.url",`, - ` * such as "setAssetPath(import.meta.url)". Other options include`, - ` * "setAssetPath(document.currentScript.src)", or using a bundler's replace plugin to`, - ` * dynamically set the path at build time, such as "setAssetPath(process.env.ASSET_PATH)".`, - ` * But do note that this configuration depends on how your script is bundled, or lack of`, - ` * bundling, and where your assets can be loaded from. Additionally custom bundling`, - ` * will have to ensure the static assets are copied to its build directory.`, - ` */`, - `export declare const setAssetPath: (path: string) => void;`, - ``, - `/**`, - ` * Used to specify a nonce value that corresponds with an application's CSP.`, - ` * When set, the nonce will be added to all dynamically created script and style tags at runtime.`, - ` * Alternatively, the nonce value can be set on a meta tag in the DOM head`, - ` * () which`, - ` * will result in the same behavior.`, - ` */`, - `export declare const setNonce: (nonce: string) => void`, - ``, - `export interface SetPlatformOptions {`, - ` raf?: (c: FrameRequestCallback) => number;`, - ` ael?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;`, - ` rel?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;`, - `}`, - `export declare const setPlatformOptions: (opts: SetPlatformOptions) => void;`, - // Always export user's index.ts types if it exists (matching JS behavior) - ...(userIndexRelPath ? [``, `export * from '${userIndexRelPath}';`] : []), - ...(isBundleExport - ? [ - ``, - `/**`, - ` * Utility to define all custom elements within this package using the tag name provided in the component's source.`, - ` * When defining each custom element, it will also check it's safe to define by:`, - ` *`, - ` * 1. Ensuring the "customElements" registry is available in the global context (window).`, - ` * 2. Ensuring that the component tag name is not already defined.`, - ` *`, - ` * Use the standard [customElements.define()](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define)`, - ` * method instead to define custom elements individually, or to provide a different tag name.`, - ` */`, - `export declare const defineCustomElements: (opts?: any) => void;`, - ] - : []), - ]; - - // Export from components.d.ts for barrel exports if user doesn't have an index.ts - if (isBarrelExport && !userIndexRelPath) { - code.push(`export * from '${componentsDtsRelPath}';`); - } - - await compilerCtx.fs.writeFile(customElementsDtsPath, code.join('\n') + `\n`, { - outputTargetType: outputTarget.type, - }); - - await Promise.all( - components.map(async (cmp) => { - const dtsCode = generateCustomElementType(componentsDtsRelPath, cmp); - const fileName = `${cmp.tagName}.d.ts`; - const filePath = join(outputTarget.dir!, fileName); - await compilerCtx.fs.writeFile(filePath, dtsCode, { outputTargetType: outputTarget.type }); - }), - ); - - // Generate loader.d.ts if autoLoader is enabled - if (outputTarget.autoLoader) { - const loaderFileName = - typeof outputTarget.autoLoader === 'object' ? outputTarget.autoLoader.fileName || 'loader' : 'loader'; - const loaderDtsPath = join(outputTarget.dir!, `${loaderFileName}.d.ts`); - const loaderDtsCode = generateLoaderType(); - await compilerCtx.fs.writeFile(loaderDtsPath, loaderDtsCode, { outputTargetType: outputTarget.type }); - } -}; - -/** - * Generate a type declaration file for the auto-loader module - * @returns the contents of the type declaration file for the loader - */ -const generateLoaderType = (): string => { - return [ - `/**`, - ` * Start the auto-loader, scanning the DOM and watching for changes.`, - ` * The loader uses MutationObserver to detect when custom elements are added`, - ` * to the DOM and lazily loads their definitions.`, - ` * @param root - The root element to observe (default: document.body)`, - ` */`, - `export declare function start(root?: Element): void;`, - ``, - `/**`, - ` * Stop the auto-loader and disconnect the MutationObserver.`, - ` */`, - `export declare function stop(): void;`, - ``, - ].join('\n'); -}; - -/** - * Generate a type declaration file for a specific Stencil component - * @param componentsDtsRelPath the path to a root type declaration file from which commonly used entities can be - * referenced from in the newly generated file - * @param cmp the component to generate the type declaration file for - * @returns the contents of the type declaration file for the provided `cmp` - */ -const generateCustomElementType = (componentsDtsRelPath: string, cmp: d.ComponentCompilerMeta): string => { - const tagNameAsPascal = dashToPascalCase(cmp.tagName); - const o: string[] = [ - `import type { Components, JSX } from "${componentsDtsRelPath}";`, - ``, - `interface ${tagNameAsPascal} extends Components.${tagNameAsPascal}, HTMLElement {}`, - `export const ${tagNameAsPascal}: {`, - ` prototype: ${tagNameAsPascal};`, - ` new (): ${tagNameAsPascal};`, - `};`, - `/**`, - ` * Used to define this component and all nested components recursively.`, - ` */`, - `export const defineCustomElement: () => void;`, - ``, - ]; - - return o.join('\n'); -}; - -/** - * Determines the relative path between two provided paths. If a type declaration file extension is present on - * `dtsPath`, it will be removed from the computed relative path. - * @param fromPath the path from which to start at - * @param dtsPath the destination path - * @returns the relative path from the provided `fromPath` to the `dtsPath` - */ -const relDts = (fromPath: string, dtsPath: string): string => { - dtsPath = relative(fromPath, dtsPath); - - return normalizePath(dtsPath.replace('.d.ts', ''), true); -}; diff --git a/src/compiler/output-targets/dist-custom-elements/generate-loader-module.ts b/src/compiler/output-targets/dist-custom-elements/generate-loader-module.ts deleted file mode 100644 index b82e011a278..00000000000 --- a/src/compiler/output-targets/dist-custom-elements/generate-loader-module.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type * as d from '../../../declarations'; -import { STENCIL_INTERNAL_CLIENT_ID } from '../../bundle/entry-alias-ids'; - -/** - * Generate the auto-loader module content that will be bundled via Rollup. - * This loader uses MutationObserver to lazily load and define custom elements - * as they appear in the DOM. - * - * @param components - The list of components to include in the loader - * @param outputTarget - The output target configuration - * @returns The generated loader module source code - */ -export const generateLoaderModule = ( - components: d.ComponentCompilerMeta[], - outputTarget: d.OutputTargetDistCustomElements, -): string => { - const autoLoaderConfig = outputTarget.autoLoader; - const autoStart = typeof autoLoaderConfig === 'object' ? autoLoaderConfig.autoStart !== false : true; - - // Build component map: { 'my-button': './my-button.js', ... } - const componentMap = components.map((cmp) => ` '${cmp.tagName}': './${cmp.tagName}.js'`).join(',\n'); - - return ` -import { transformTag } from '${STENCIL_INTERNAL_CLIENT_ID}'; - -/** - * Component map built at compile time. - * Maps original tag names to their module paths. - */ -const components = { -${componentMap} -}; - -/** - * Set of tags that have already been loaded/registered. - * Prevents duplicate loading attempts. - */ -const defined = new Set(); - -/** - * MutationObserver instance for watching DOM changes. - */ -let observer; - -/** - * Build a lookup map using transformed tag names. - * This is called at runtime to account for any tag transformers - * that may have been set via setTagTransformer(). - */ -function getTransformedLookup() { - const lookup = {}; - for (const [tag, path] of Object.entries(components)) { - lookup[transformTag(tag)] = { originalTag: tag, path }; - } - return lookup; -} - -/** - * Scan a root element for undefined custom elements and load them. - * @param root - The root element to scan - * @param lookup - The transformed tag lookup map - */ -async function load(root, lookup) { - const rootTag = root instanceof Element ? root.tagName.toLowerCase() : ''; - const tags = [...root.querySelectorAll(':not(:defined)')] - .map(el => el.tagName.toLowerCase()) - .filter(tag => lookup[tag] && !defined.has(tag)); - - // Also check the root element itself - if (rootTag && lookup[rootTag] && !defined.has(rootTag)) { - tags.push(rootTag); - } - - // Load unique tags in parallel - await Promise.allSettled([...new Set(tags)].map(tag => register(tag, lookup))); -} - -/** - * Register a single component by importing its module. - * @param transformedTag - The transformed tag name (as it appears in the DOM) - * @param lookup - The transformed tag lookup map - */ -async function register(transformedTag, lookup) { - // Skip if already defined or being defined - if (defined.has(transformedTag) || customElements.get(transformedTag)) { - defined.add(transformedTag); - return; - } - - // Mark as being processed to prevent duplicate attempts - defined.add(transformedTag); - const { path } = lookup[transformedTag]; - - try { - const module = await import(path); - // Call defineCustomElement if exported (handles component + dependencies) - if (typeof module.defineCustomElement === 'function') { - module.defineCustomElement(); - } - } catch (e) { - console.error(\`[Stencil Loader] Failed to load <\${transformedTag}>\`, e); - // Remove from defined set to allow retry - defined.delete(transformedTag); - } -} - -/** - * Start the auto-loader, scanning the DOM and watching for changes. - * @param root - The root element to observe (default: document.body) - */ -export function start(root = document.body) { - const lookup = getTransformedLookup(); - - // Disconnect any existing observer - if (observer) { - observer.disconnect(); - } - - // Create MutationObserver to watch for new elements - observer = new MutationObserver(mutations => { - for (const { addedNodes } of mutations) { - for (const node of addedNodes) { - if (node.nodeType === Node.ELEMENT_NODE) { - load(node, lookup); - } - } - } - }); - - // Initial scan of existing DOM - load(root, lookup); - - // Start observing for new elements - observer.observe(root, { subtree: true, childList: true }); -} - -/** - * Stop the auto-loader and disconnect the MutationObserver. - */ -export function stop() { - if (observer) { - observer.disconnect(); - observer = null; - } -} -${autoStart ? '\n// Auto-start the loader\nstart();' : ''} -`.trim(); -}; diff --git a/src/compiler/output-targets/dist-custom-elements/index.ts b/src/compiler/output-targets/dist-custom-elements/index.ts deleted file mode 100644 index 2fae740f8fe..00000000000 --- a/src/compiler/output-targets/dist-custom-elements/index.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { - catchError, - dashToPascalCase, - generatePreamble, - getSourceMappingUrlForEndOfFile, - hasError, - isBoolean, - isOutputTargetDistCustomElements, - isString, - join, - rollupToStencilSourceMap, -} from '@utils'; -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import type { BundleOptions } from '../../bundle/bundle-interface'; -import { bundleOutput } from '../../bundle/bundle-output'; -import { STENCIL_APP_GLOBALS_ID, STENCIL_INTERNAL_CLIENT_ID, USER_INDEX_ENTRY_ID } from '../../bundle/entry-alias-ids'; -import { optimizeModule } from '../../optimize/optimize-module'; -import { addDefineCustomElementFunctions } from '../../transformers/component-native/add-define-custom-element-function'; -import { proxyCustomElement } from '../../transformers/component-native/proxy-custom-element-function'; -import { nativeComponentTransform } from '../../transformers/component-native/tranform-to-native-component'; -import { removeCollectionImports } from '../../transformers/remove-collection-imports'; -import { rewriteAliasedSourceFileImportPaths } from '../../transformers/rewrite-aliased-paths'; -import { updateStencilCoreImports } from '../../transformers/update-stencil-core-import'; -import { getCustomElementsBuildConditionals } from './custom-elements-build-conditionals'; -import { generateLoaderModule } from './generate-loader-module'; -import { addTagTransform } from '../../transformers/add-tag-transform'; - -/** - * Main output target function for `dist-custom-elements`. This function just - * does some organizational work to call the other functions in this module, - * which do actual work of generating the rollup configuration, creating an - * entry chunk, running, the build, etc. - * - * @param config the validated compiler configuration we're using - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @returns an empty Promise which won't resolve until the work is done! - */ -export const outputCustomElements = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -): Promise => { - if (!config.buildDist) { - return; - } - - const outputTargets = config.outputTargets.filter(isOutputTargetDistCustomElements); - if (outputTargets.length === 0) { - return; - } - - const bundlingEventMessage = `generate custom elements${config.sourceMap ? ' + source maps' : ''}`; - const timespan = buildCtx.createTimeSpan(`${bundlingEventMessage} started`); - - await Promise.all(outputTargets.map((target) => bundleCustomElements(config, compilerCtx, buildCtx, target))); - - timespan.finish(`${bundlingEventMessage} finished`); -}; - -/** - * Get bundle options for our current build and compiler context which we'll use - * to generate a Rollup build and so on. - * - * @param config a validated Stencil configuration object - * @param buildCtx the current build context - * @param compilerCtx the current compiler context - * @param outputTarget the outputTarget we're currently dealing with - * @returns bundle options suitable for generating a rollup configuration - */ -export const getBundleOptions = ( - config: d.ValidatedConfig, - buildCtx: d.BuildCtx, - compilerCtx: d.CompilerCtx, - outputTarget: d.OutputTargetDistCustomElements, -): BundleOptions => ({ - id: 'customElements', - platform: 'client', - conditionals: getCustomElementsBuildConditionals(config, buildCtx.components), - customBeforeTransformers: getCustomBeforeTransformers( - config, - compilerCtx, - buildCtx.components, - outputTarget, - buildCtx, - ), - externalRuntime: !!outputTarget.externalRuntime, - inlineWorkers: true, - inputs: { - // Here we prefix our index chunk with '\0' to tell Rollup that we're - // going to be using virtual modules with this module. A leading '\0' - // prevents other plugins from messing with the module. We generate a - // string for the index chunk below in the `loader` property. - // - // @see {@link https://rollupjs.org/guide/en/#conventions} for more info. - index: '\0core', - }, - loader: {}, - preserveEntrySignatures: 'allow-extension', -}); - -/** - * Get bundle options for rollup, run the rollup build, optionally minify the - * output, and write files to disk. - * - * @param config the validated Stencil configuration we're using - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param outputTarget the outputTarget we're currently dealing with - * @returns an empty promise - */ -export const bundleCustomElements = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTarget: d.OutputTargetDistCustomElements, -) => { - try { - const bundleOpts = getBundleOptions(config, buildCtx, compilerCtx, outputTarget); - - addCustomElementInputs(buildCtx, bundleOpts, outputTarget); - - const build = await bundleOutput(config, compilerCtx, buildCtx, bundleOpts); - - if (build) { - const rollupOutput = await build.generate({ - banner: generatePreamble(config), - format: 'esm', - sourcemap: config.sourceMap, - chunkFileNames: outputTarget.externalRuntime || !config.hashFileNames ? '[name].js' : 'p-[hash].js', - entryFileNames: '[name].js', - hoistTransitiveImports: false, - }); - - // the output target should have been validated at this point - as a result, we expect this field - // to have been backfilled if it wasn't provided - const outputTargetDir: string = outputTarget.dir!; - - // besides, if it isn't here we do a diagnostic and an early return - if (!isString(outputTargetDir)) { - buildCtx.diagnostics.push({ - level: 'error', - type: 'build', - messageText: 'dist-custom-elements output target provided with no output target directory!', - lines: [], - }); - return; - } - - const minify = isBoolean(outputTarget.minify) ? outputTarget.minify : config.minifyJs; - const files = rollupOutput.output.map(async (bundle) => { - if (bundle.type === 'chunk') { - let code = bundle.code; - let sourceMap = bundle.map ? rollupToStencilSourceMap(bundle.map) : undefined; - - const optimizeResults = await optimizeModule(config, compilerCtx, { - input: code, - isCore: bundle.isEntry, - minify, - sourceMap, - }); - buildCtx.diagnostics.push(...optimizeResults.diagnostics); - if (!hasError(optimizeResults.diagnostics) && typeof optimizeResults.output === 'string') { - code = optimizeResults.output; - } - if (optimizeResults.sourceMap) { - sourceMap = optimizeResults.sourceMap; - code = code + getSourceMappingUrlForEndOfFile(bundle.fileName); - await compilerCtx.fs.writeFile(join(outputTargetDir, bundle.fileName + '.map'), JSON.stringify(sourceMap), { - outputTargetType: outputTarget.type, - }); - } - await compilerCtx.fs.writeFile(join(outputTargetDir, bundle.fileName), code, { - outputTargetType: outputTarget.type, - }); - } - }); - await Promise.all(files); - } - } catch (e: any) { - catchError(buildCtx.diagnostics, e); - } -}; - -/** - * Create the virtual modules/input modules for the `dist-custom-elements` output target. - * @param buildCtx the context for the current build - * @param bundleOpts the bundle options to store the virtual modules under. acts as an output parameter - * @param outputTarget the configuration for the custom element output target - */ -export const addCustomElementInputs = ( - buildCtx: d.BuildCtx, - bundleOpts: BundleOptions, - outputTarget: d.OutputTargetDistCustomElements, -): void => { - const components = buildCtx.components; - // An array to store the imports of these modules that we're going to add to our entry chunk - const indexImports: string[] = []; - // An array to store the export declarations that we're going to add to our entry chunk - const indexExports: string[] = []; - // An array to store the exported component names that will be used for the `defineCustomElements` - // function on the `bundle` export behavior option - const exportNames: string[] = []; - - components.forEach((cmp) => { - const exp: string[] = []; - const exportName = dashToPascalCase(cmp.tagName); - const importName = cmp.componentClassName; - const importAs = `$Cmp${exportName}`; - const coreKey = `\0${exportName}`; - - if (cmp.isPlain) { - exp.push(`export { ${importName} as ${exportName} } from '${cmp.sourceFilePath}';`); - indexExports.push(`export { ${exportName} } from '${coreKey}';`); - } else { - // the `importName` may collide with the `exportName`, alias it just in case it does with `importAs` - exp.push( - `import { ${importName} as ${importAs}, defineCustomElement as cmpDefCustomEle } from '${cmp.sourceFilePath}';`, - ); - exp.push(`export const ${exportName} = ${importAs};`); - exp.push(`export const defineCustomElement = cmpDefCustomEle;`); - - // Here we push an export (with a rename for `defineCustomElement`) for - // this component onto our array which references the `coreKey` (prefixed - // with `\0`). We have to do this so that our import is referencing the - // correct virtual module, if we instead referenced, for instance, - // `cmp.sourceFilePath`, we would end up with duplicated modules in our - // output. - indexExports.push( - `export { ${exportName}, defineCustomElement as defineCustomElement${exportName} } from '${coreKey}';`, - ); - } - - indexImports.push(`import { ${exportName} } from '${coreKey}';`); - exportNames.push(exportName); - - bundleOpts.inputs[cmp.tagName] = coreKey; - bundleOpts.loader![coreKey] = exp.join('\n'); - }); - - // Generate the contents of the entry file to be created by the bundler - bundleOpts.loader!['\0core'] = generateEntryPoint(outputTarget, indexImports, indexExports, exportNames); - - // Generate auto-loader module if enabled - if (outputTarget.autoLoader) { - const loaderFileName = - typeof outputTarget.autoLoader === 'object' ? outputTarget.autoLoader.fileName || 'loader' : 'loader'; - - bundleOpts.inputs[loaderFileName] = '\0loader'; - bundleOpts.loader!['\0loader'] = generateLoaderModule(components, outputTarget); - } -}; - -/** - * Generate the entrypoint (`index.ts` file) contents for the `dist-custom-elements` output target - * @param outputTarget the output target's configuration - * @param cmpImports The import declarations for local component modules. - * @param cmpExports The export declarations for local component modules. - * @param cmpNames The exported component names (could be aliased) from local component modules. - * @returns the stringified contents to be placed in the entrypoint - */ -export const generateEntryPoint = ( - outputTarget: d.OutputTargetDistCustomElements, - cmpImports: string[] = [], - cmpExports: string[] = [], - cmpNames: string[] = [], -): string => { - const body: string[] = []; - const imports: string[] = []; - const exports: string[] = []; - - // Exports that are always present - exports.push( - `export { getAssetPath, setAssetPath, setNonce, setPlatformOptions, render } from '${STENCIL_INTERNAL_CLIENT_ID}';`, - `export * from '${USER_INDEX_ENTRY_ID}';`, - ); - - // Content related to global scripts - if (outputTarget.includeGlobalScripts !== false) { - imports.push(`import { globalScripts } from '${STENCIL_APP_GLOBALS_ID}';`); - body.push(`globalScripts();`); - } - - // Content related to the `bundle` export behavior - if (outputTarget.customElementsExportBehavior === 'bundle') { - imports.push(`import { transformTag } from '${STENCIL_INTERNAL_CLIENT_ID}';`); - imports.push(...cmpImports); - body.push( - 'export const defineCustomElements = (opts) => {', - " if (typeof customElements !== 'undefined') {", - ' [', - ...cmpNames.map((cmp) => ` ${cmp},`), - ' ].forEach(cmp => {', - ' if (!customElements.get(transformTag(cmp.is))) {', - ' customElements.define(transformTag(cmp.is), cmp, opts);', - ' }', - ' });', - ' }', - '};', - ); - } - - // Content related to the `single-export-module` export behavior - if (outputTarget.customElementsExportBehavior === 'single-export-module') { - exports.push(...cmpExports); - } - - // Generate the contents of the file based on the parts - // defined above. This keeps the file structure consistent as - // new export behaviors may be added - let content = ''; - - // Add imports to file content - content += imports.length ? imports.join('\n') + '\n' : ''; - // Add exports to file content - content += exports.length ? exports.join('\n') + '\n' : ''; - // Add body to file content - content += body.length ? '\n' + body.join('\n') + '\n' : ''; - - return content; -}; - -/** - * Get the series of custom transformers, specific to the needs of the - * `dist-custom-elements` output target, that will be applied to a Stencil - * project's source code during the TypeScript transpilation process - * - * @param config the configuration for the Stencil project - * @param compilerCtx the current compiler context - * @param components the components that will be compiled as a part of the current build - * @param outputTarget the output target configuration - * @returns a list of transformers to use in the transpilation process - */ -const getCustomBeforeTransformers = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - components: d.ComponentCompilerMeta[], - outputTarget: d.OutputTargetDistCustomElements, - buildCtx: d.BuildCtx, -): ts.TransformerFactory[] => { - const transformOpts: d.TransformOptions = { - coreImportPath: STENCIL_INTERNAL_CLIENT_ID, - componentExport: null, - componentMetadata: null, - currentDirectory: config.sys.getCurrentDirectory(), - proxy: null, - style: 'static', - styleImportData: 'queryparams', - }; - const customBeforeTransformers = [ - addDefineCustomElementFunctions(compilerCtx, components, outputTarget), - updateStencilCoreImports(transformOpts.coreImportPath), - ]; - - if (config.transformAliasedImportPaths) { - customBeforeTransformers.push(rewriteAliasedSourceFileImportPaths); - } - - if (buildCtx.config.extras.additionalTagTransformers) { - customBeforeTransformers.push(addTagTransform(compilerCtx, buildCtx)); - } - - customBeforeTransformers.push( - nativeComponentTransform(compilerCtx, transformOpts, buildCtx), - proxyCustomElement(compilerCtx, transformOpts), - removeCollectionImports(compilerCtx), - ); - return customBeforeTransformers; -}; diff --git a/src/compiler/output-targets/dist-custom-elements/test/dist-custom-elements.spec.ts b/src/compiler/output-targets/dist-custom-elements/test/dist-custom-elements.spec.ts deleted file mode 100644 index 2f54d584ef6..00000000000 --- a/src/compiler/output-targets/dist-custom-elements/test/dist-custom-elements.spec.ts +++ /dev/null @@ -1,166 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockBuildCtx, mockCompilerCtx, mockValidatedConfig } from '@stencil/core/testing'; -import path from 'path'; - -import { BundleOptions } from '../../../bundle/bundle-interface'; -import * as bundleOutputMod from '../../../bundle/bundle-output'; -import * as optimizeModuleMod from '../../../optimize/optimize-module'; -import { stubComponentCompilerMeta } from '../../../types/tests/ComponentCompilerMeta.stub'; -import { addCustomElementInputs, bundleCustomElements } from '../index'; - -describe('dist-custom-elements', () => { - it('should export plain component', () => { - const cmpMeta = stubComponentCompilerMeta({ isPlain: true, sourceFilePath: './foo/bar.tsx', tagName: 'my-tag' }); - const buildCtx = mockBuildCtx(); - buildCtx.components = [cmpMeta]; - const bundleOpts: BundleOptions = { - id: 'customElements', - platform: 'client', - inputs: {}, - loader: {}, - }; - const outputTarget: d.OutputTargetDistCustomElements = { - type: 'dist-custom-elements', - customElementsExportBehavior: 'single-export-module', - }; - addCustomElementInputs(buildCtx, bundleOpts, outputTarget); - expect(bundleOpts.loader['\x00MyTag']).toContain("export { StubCmp as MyTag } from './foo/bar.tsx';"); - expect(bundleOpts.loader['\x00core']).toContain(`export { MyTag } from '\x00MyTag';\n`); - }); - - it('should export component with a defineCustomElement function', () => { - const cmpMeta = stubComponentCompilerMeta({ sourceFilePath: './foo/bar.tsx', tagName: 'my-tag' }); - const buildCtx = mockBuildCtx(); - buildCtx.components = [cmpMeta]; - const bundleOpts: BundleOptions = { - id: 'customElements', - platform: 'client', - inputs: {}, - loader: {}, - }; - const outputTarget: d.OutputTargetDistCustomElements = { - type: 'dist-custom-elements', - customElementsExportBehavior: 'single-export-module', - }; - addCustomElementInputs(buildCtx, bundleOpts, outputTarget); - expect(bundleOpts.loader['\x00MyTag']).toContain('export const defineCustomElement = cmpDefCustomEle;'); - expect(bundleOpts.loader['\x00MyTag']).toContain( - "import { StubCmp as $CmpMyTag, defineCustomElement as cmpDefCustomEle } from './foo/bar.tsx';", - ); - expect(bundleOpts.loader['\x00core']).toContain( - `export { MyTag, defineCustomElement as defineCustomElementMyTag } from '\x00MyTag';\n`, - ); - }); - - describe('minification', () => { - let bundleOutputSpy: jest.SpyInstance; - let optimizeModuleSpy: jest.SpyInstance; - let mockRollupBuild: any; - - beforeEach(() => { - // Mock the rollup build output - mockRollupBuild = { - generate: jest.fn().mockResolvedValue({ - output: [ - { - type: 'chunk', - fileName: 'index.js', - code: 'export const test = "unminified code";', - isEntry: true, - map: null, - }, - ], - }), - }; - - // Spy on bundleOutput to return our mock build - bundleOutputSpy = jest.spyOn(bundleOutputMod, 'bundleOutput'); - bundleOutputSpy.mockResolvedValue(mockRollupBuild); - - // Spy on optimizeModule to verify it's called with correct minify parameter - optimizeModuleSpy = jest.spyOn(optimizeModuleMod, 'optimizeModule'); - optimizeModuleSpy.mockResolvedValue({ - output: 'const test="minified";', - diagnostics: [], - sourceMap: undefined, - }); - }); - - afterEach(() => { - bundleOutputSpy.mockRestore(); - optimizeModuleSpy.mockRestore(); - }); - - it('should pass minify=true to optimizeModule when outputTarget.minify is true', async () => { - const config = mockValidatedConfig({ minifyJs: false }); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - buildCtx.components = [stubComponentCompilerMeta()]; - - const outputTarget: d.OutputTargetDistCustomElements = { - type: 'dist-custom-elements', - dir: path.join(config.rootDir, 'dist'), - customElementsExportBehavior: 'single-export-module', - minify: true, - }; - - await bundleCustomElements(config, compilerCtx, buildCtx, outputTarget); - - expect(optimizeModuleSpy).toHaveBeenCalledWith( - config, - compilerCtx, - expect.objectContaining({ - minify: true, - }), - ); - }); - - it('should pass minify=false to optimizeModule when outputTarget.minify is false', async () => { - const config = mockValidatedConfig({ minifyJs: true }); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - buildCtx.components = [stubComponentCompilerMeta()]; - - const outputTarget: d.OutputTargetDistCustomElements = { - type: 'dist-custom-elements', - dir: path.join(config.rootDir, 'dist'), - customElementsExportBehavior: 'single-export-module', - minify: false, - }; - - await bundleCustomElements(config, compilerCtx, buildCtx, outputTarget); - - expect(optimizeModuleSpy).toHaveBeenCalledWith( - config, - compilerCtx, - expect.objectContaining({ - minify: false, - }), - ); - }); - - it('should fall back to config.minifyJs when outputTarget.minify is undefined', async () => { - const config = mockValidatedConfig({ minifyJs: true }); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - buildCtx.components = [stubComponentCompilerMeta()]; - - const outputTarget: d.OutputTargetDistCustomElements = { - type: 'dist-custom-elements', - dir: path.join(config.rootDir, 'dist'), - customElementsExportBehavior: 'single-export-module', - // minify is undefined, should use config.minifyJs - }; - - await bundleCustomElements(config, compilerCtx, buildCtx, outputTarget); - - expect(optimizeModuleSpy).toHaveBeenCalledWith( - config, - compilerCtx, - expect.objectContaining({ - minify: true, // from config.minifyJs - }), - ); - }); - }); -}); diff --git a/src/compiler/output-targets/dist-hydrate-script/bundle-hydrate-factory.ts b/src/compiler/output-targets/dist-hydrate-script/bundle-hydrate-factory.ts deleted file mode 100644 index 8fe36847557..00000000000 --- a/src/compiler/output-targets/dist-hydrate-script/bundle-hydrate-factory.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { loadRollupDiagnostics } from '@utils'; -import * as ts from 'typescript'; - -import type * as d from '../../../declarations'; -import type { BundleOptions } from '../../bundle/bundle-interface'; -import { bundleOutput } from '../../bundle/bundle-output'; -import { STENCIL_INTERNAL_HYDRATE_ID } from '../../bundle/entry-alias-ids'; -import { hydrateComponentTransform } from '../../transformers/component-hydrate/tranform-to-hydrate-component'; -import { removeCollectionImports } from '../../transformers/remove-collection-imports'; -import { addTagTransform } from '../../transformers/add-tag-transform'; -import { rewriteAliasedSourceFileImportPaths } from '../../transformers/rewrite-aliased-paths'; -import { updateStencilCoreImports } from '../../transformers/update-stencil-core-import'; -import { getHydrateBuildConditionals } from './hydrate-build-conditionals'; - -/** - * Marshall some Rollup options for the hydrate factory and then pass it to our - * {@link bundleOutput} helper - * - * @param config a validated Stencil configuration - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param appFactoryEntryCode an entry code for the app factory - * @returns a promise wrapping a rollup build object - */ -export const bundleHydrateFactory = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - appFactoryEntryCode: string, -) => { - try { - const bundleOpts: BundleOptions = { - id: 'hydrate', - platform: 'hydrate', - conditionals: getHydrateBuildConditionals(config, buildCtx.components), - customBeforeTransformers: getCustomBeforeTransformers(config, compilerCtx, buildCtx), - inlineDynamicImports: true, - inputs: { - '@app-factory-entry': '@app-factory-entry', - }, - loader: { - '@app-factory-entry': appFactoryEntryCode, - }, - }; - - const rollupBuild = await bundleOutput(config, compilerCtx, buildCtx, bundleOpts); - return rollupBuild; - } catch (e: any) { - if (!buildCtx.hasError) { - // TODO(STENCIL-353): Implement a type guard that balances using our own copy of Rollup types (which are - // breakable) and type safety (so that the error variable may be something other than `any`) - loadRollupDiagnostics(config, compilerCtx, buildCtx, e); - } - } - return undefined; -}; - -/** - * Generate a collection of transformations that are to be applied as a part of the `before` step in the TypeScript - * compilation process. - # - * @param config the Stencil configuration associated with the current build - * @param compilerCtx the current compiler context - * @returns a collection of transformations that should be applied to the source code, intended for the `before` part - * of the pipeline - */ -const getCustomBeforeTransformers = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx?: d.BuildCtx, -): ts.TransformerFactory[] => { - const transformOpts: d.TransformOptions = { - coreImportPath: STENCIL_INTERNAL_HYDRATE_ID, - componentExport: null, - componentMetadata: null, - currentDirectory: config.sys.getCurrentDirectory(), - proxy: null, - style: 'static', - styleImportData: 'queryparams', - }; - const customBeforeTransformers = [updateStencilCoreImports(transformOpts.coreImportPath)]; - - if (config.transformAliasedImportPaths) { - customBeforeTransformers.push(rewriteAliasedSourceFileImportPaths); - } - - if (buildCtx.config.extras.additionalTagTransformers) { - customBeforeTransformers.push(addTagTransform(compilerCtx, buildCtx)); - } - - customBeforeTransformers.push( - hydrateComponentTransform(compilerCtx, transformOpts, buildCtx), - removeCollectionImports(compilerCtx), - ); - return customBeforeTransformers; -}; diff --git a/src/compiler/output-targets/dist-hydrate-script/generate-hydrate-app.ts b/src/compiler/output-targets/dist-hydrate-script/generate-hydrate-app.ts deleted file mode 100644 index 6187ab01f9e..00000000000 --- a/src/compiler/output-targets/dist-hydrate-script/generate-hydrate-app.ts +++ /dev/null @@ -1,159 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { catchError, createOnWarnFn, generatePreamble, join, loadRollupDiagnostics } from '@utils'; -import MagicString from 'magic-string'; -import { RollupOptions } from 'rollup'; -import { rollup, type RollupBuild } from 'rollup'; - -import { - STENCIL_APP_DATA_ID, - STENCIL_HYDRATE_FACTORY_ID, - STENCIL_INTERNAL_HYDRATE_ID, - STENCIL_MOCK_DOC_ID, -} from '../../bundle/entry-alias-ids'; -import { bundleHydrateFactory } from './bundle-hydrate-factory'; -import { - HYDRATE_FACTORY_INTRO, - HYDRATE_FACTORY_OUTRO, - MODE_RESOLUTION_CHAIN_DECLARATION, -} from './hydrate-factory-closure'; -import { updateToHydrateComponents } from './update-to-hydrate-components'; -import { writeHydrateOutputs } from './write-hydrate-outputs'; - -const buildHydrateAppFor = async ( - format: 'esm' | 'cjs', - rollupBuild: RollupBuild, - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTargets: d.OutputTargetHydrate[], -) => { - const file = format === 'esm' ? 'index.mjs' : 'index.js'; - const rollupOutput = await rollupBuild.generate({ - banner: generatePreamble(config), - format, - file, - }); - - await writeHydrateOutputs(config, compilerCtx, buildCtx, outputTargets, rollupOutput); -}; - -/** - * Generate and build the hydrate app and then write it to disk - * - * @param config a validated Stencil configuration - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param outputTargets the output targets for the current build - */ -export const generateHydrateApp = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTargets: d.OutputTargetHydrate[], -) => { - try { - const packageDir = join(config.sys.getCompilerExecutingPath(), '..', '..'); - const input = join(packageDir, 'internal', 'hydrate', 'runner.js'); - const mockDoc = join(packageDir, 'mock-doc', 'index.js'); - const appData = join(packageDir, 'internal', 'app-data', 'index.js'); - - const rollupOptions: RollupOptions = { - ...config.rollupConfig.inputOptions, - external: ['stream'], - - input, - plugins: [ - { - name: 'hydrateAppPlugin', - resolveId(id) { - // Handle both @hydrate-factory (TypeScript alias) and full path - if (id === STENCIL_HYDRATE_FACTORY_ID || id === '@hydrate-factory') { - return STENCIL_HYDRATE_FACTORY_ID; - } - if (id === STENCIL_MOCK_DOC_ID) { - return mockDoc; - } - if (id === STENCIL_APP_DATA_ID) { - return appData; - } - return null; - }, - load(id) { - if (id === STENCIL_HYDRATE_FACTORY_ID) { - return generateHydrateFactory(config, compilerCtx, buildCtx); - } - return null; - }, - transform(code) { - /** - * Remove the modeResolutionChain variable from the generated code. - * This variable is redefined in `HYDRATE_FACTORY_INTRO` to ensure we can - * use it within the hydrate and global runtime. - */ - return code.replace(`var ${MODE_RESOLUTION_CHAIN_DECLARATION}`, ''); - }, - }, - ], - treeshake: false, - onwarn: createOnWarnFn(buildCtx.diagnostics), - }; - - const rollupAppBuild = await rollup(rollupOptions); - await Promise.all([ - buildHydrateAppFor('cjs', rollupAppBuild, config, compilerCtx, buildCtx, outputTargets), - buildHydrateAppFor('esm', rollupAppBuild, config, compilerCtx, buildCtx, outputTargets), - ]); - } catch (e: any) { - if (!buildCtx.hasError) { - // TODO(STENCIL-353): Implement a type guard that balances using our own copy of Rollup types (which are - // breakable) and type safety (so that the error variable may be something other than `any`) - loadRollupDiagnostics(config, compilerCtx, buildCtx, e); - } - } -}; - -const generateHydrateFactory = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - if (!buildCtx.hasError) { - try { - const appFactoryEntryCode = await generateHydrateFactoryEntry(buildCtx); - - const rollupFactoryBuild = await bundleHydrateFactory(config, compilerCtx, buildCtx, appFactoryEntryCode); - if (rollupFactoryBuild != null) { - const rollupOutput = await rollupFactoryBuild.generate({ - format: 'cjs', - esModule: false, - strict: false, - intro: HYDRATE_FACTORY_INTRO, - outro: HYDRATE_FACTORY_OUTRO, - inlineDynamicImports: true, - }); - - if (!buildCtx.hasError && rollupOutput != null && Array.isArray(rollupOutput.output)) { - return rollupOutput.output[0].code; - } - } - } catch (e: any) { - catchError(buildCtx.diagnostics, e); - } - } - return ''; -}; - -const generateHydrateFactoryEntry = async (buildCtx: d.BuildCtx) => { - const cmps = buildCtx.components; - const hydrateCmps = await updateToHydrateComponents(cmps); - const s = new MagicString(''); - - s.append(`import { hydrateApp, registerComponents, styles } from '${STENCIL_INTERNAL_HYDRATE_ID}';\n`); - - hydrateCmps.forEach((cmpData) => s.append(cmpData.importLine + '\n')); - - s.append(`registerComponents([\n`); - hydrateCmps.forEach((cmpData) => { - s.append(` ${cmpData.uniqueComponentClassName},\n`); - }); - s.append(`]);\n`); - s.append(`export { hydrateApp }\n`); - - return s.toString(); -}; diff --git a/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts b/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts deleted file mode 100644 index 99484b9c3c0..00000000000 --- a/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type * as d from '../../../declarations'; -import { getBuildFeatures, updateBuildConditionals } from '../../app-core/app-data'; - -/** - * Get the `BUILD` conditionals for the hydrate build based on the current - * project - * - * @param config a validated Stencil configuration - * @param cmps component metadata - * @returns a populated build conditional object - */ -export const getHydrateBuildConditionals = (config: d.ValidatedConfig, cmps: d.ComponentCompilerMeta[]) => { - const build = getBuildFeatures(cmps) as d.BuildConditionals; - // we need to make sure that things like the hydratedClass and flag are - // set for the hydrate build - updateBuildConditionals(config, build); - - build.slotRelocation = true; - build.lazyLoad = true; - build.hydrateServerSide = true; - build.hydrateClientSide = true; - build.isDebug = false; - build.isDev = false; - build.isTesting = false; - build.devTools = false; - build.lifecycleDOMEvents = false; - build.profile = false; - build.hotModuleReplacement = false; - build.updatable = true; - build.member = true; - build.constructableCSS = false; - build.asyncLoading = true; - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - build.appendChildSlotFix = false; - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - build.slotChildNodesFix = false; - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - build.experimentalSlotFixes = false; - // TODO(STENCIL-1086): remove this option when it's the default behavior - build.experimentalScopedSlotChanges = false; - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - build.cloneNodeFix = false; - build.cssAnnotations = true; - // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field - build.shadowDomShim = true; - // TODO(STENCIL-1305): remove this option - build.scriptDataOpts = false; - build.attachStyles = true; - - return build; -}; diff --git a/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts b/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts deleted file mode 100644 index 2b60a094402..00000000000 --- a/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts +++ /dev/null @@ -1,149 +0,0 @@ -export const HYDRATE_APP_CLOSURE_START = `/*hydrateAppClosure start*/`; - -export const MODE_RESOLUTION_CHAIN_DECLARATION = `modeResolutionChain = [];`; - -/** - * This is the entry point for the hydrate factory. - * - * __Note:__ the `modeResolutionChain` will be uncommented in the - * `src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts` file. This enables us to use - * one module resolution chain across hydrate and core runtime. - */ -export const HYDRATE_FACTORY_INTRO = ` -// const ${MODE_RESOLUTION_CHAIN_DECLARATION} - -export function hydrateFactory($stencilWindow, $stencilHydrateOpts, $stencilHydrateResults, $stencilAfterHydrate, $stencilHydrateResolve) { - var globalThis = $stencilWindow; - var self = $stencilWindow; - var top = $stencilWindow; - var parent = $stencilWindow; - - var addEventListener = $stencilWindow.addEventListener.bind($stencilWindow); - var alert = $stencilWindow.alert.bind($stencilWindow); - var blur = $stencilWindow.blur.bind($stencilWindow); - var cancelAnimationFrame = $stencilWindow.cancelAnimationFrame.bind($stencilWindow); - var cancelIdleCallback = $stencilWindow.cancelIdleCallback.bind($stencilWindow); - var clearInterval = $stencilWindow.clearInterval.bind($stencilWindow); - var clearTimeout = $stencilWindow.clearTimeout.bind($stencilWindow); - var close = () => {}; - var confirm = $stencilWindow.confirm.bind($stencilWindow); - var dispatchEvent = $stencilWindow.dispatchEvent.bind($stencilWindow); - var focus = $stencilWindow.focus.bind($stencilWindow); - var getComputedStyle = $stencilWindow.getComputedStyle.bind($stencilWindow); - var matchMedia = $stencilWindow.matchMedia.bind($stencilWindow); - var open = $stencilWindow.open.bind($stencilWindow); - var prompt = $stencilWindow.prompt.bind($stencilWindow); - var removeEventListener = $stencilWindow.removeEventListener.bind($stencilWindow); - var requestAnimationFrame = $stencilWindow.requestAnimationFrame.bind($stencilWindow); - var requestIdleCallback = $stencilWindow.requestIdleCallback.bind($stencilWindow); - var setInterval = $stencilWindow.setInterval.bind($stencilWindow); - var setTimeout = $stencilWindow.setTimeout.bind($stencilWindow); - - // Tag transform functions are provided from the outer scope - // This ensures the factory uses the same instance as the runner - var setTagTransformer = $stencilTagTransform.setTagTransformer; - var transformTag = $stencilTagTransform.transformTag; - - var CharacterData = $stencilWindow.CharacterData; - var CSS = $stencilWindow.CSS; - var CustomEvent = $stencilWindow.CustomEvent; - var CSSStyleSheet = $stencilWindow.CSSStyleSheet; - var Document = $stencilWindow.Document; - var ShadowRoot = $stencilWindow.ShadowRoot; - var DocumentFragment = $stencilWindow.DocumentFragment; - var DocumentType = $stencilWindow.DocumentType; - var DOMTokenList = $stencilWindow.DOMTokenList; - var Element = $stencilWindow.Element; - var Event = $stencilWindow.Event; - var HTMLAnchorElement = $stencilWindow.HTMLAnchorElement; - var HTMLBaseElement = $stencilWindow.HTMLBaseElement; - var HTMLButtonElement = $stencilWindow.HTMLButtonElement; - var HTMLCanvasElement = $stencilWindow.HTMLCanvasElement; - var HTMLElement = $stencilWindow.HTMLElement; - var HTMLFormElement = $stencilWindow.HTMLFormElement; - var HTMLImageElement = $stencilWindow.HTMLImageElement; - var HTMLInputElement = $stencilWindow.HTMLInputElement; - var HTMLLinkElement = $stencilWindow.HTMLLinkElement; - var HTMLMetaElement = $stencilWindow.HTMLMetaElement; - var HTMLScriptElement = $stencilWindow.HTMLScriptElement; - var HTMLStyleElement = $stencilWindow.HTMLStyleElement; - var HTMLTemplateElement = $stencilWindow.HTMLTemplateElement; - var HTMLTitleElement = $stencilWindow.HTMLTitleElement; - var IntersectionObserver = $stencilWindow.IntersectionObserver; - var ResizeObserver = $stencilWindow.ResizeObserver; - var KeyboardEvent = $stencilWindow.KeyboardEvent; - var MouseEvent = $stencilWindow.MouseEvent; - var Node = $stencilWindow.Node; - var NodeList = $stencilWindow.NodeList; - var URL = $stencilWindow.URL; - - var console = $stencilWindow.console; - var customElements = $stencilWindow.customElements; - var history = $stencilWindow.history; - var localStorage = $stencilWindow.localStorage; - var location = $stencilWindow.location; - var navigator = $stencilWindow.navigator; - var performance = $stencilWindow.performance; - var sessionStorage = $stencilWindow.sessionStorage; - - var devicePixelRatio = $stencilWindow.devicePixelRatio; - var innerHeight = $stencilWindow.innerHeight; - var innerWidth = $stencilWindow.innerWidth; - var origin = $stencilWindow.origin; - var pageXOffset = $stencilWindow.pageXOffset; - var pageYOffset = $stencilWindow.pageYOffset; - var screen = $stencilWindow.screen; - var screenLeft = $stencilWindow.screenLeft; - var screenTop = $stencilWindow.screenTop; - var screenX = $stencilWindow.screenX; - var screenY = $stencilWindow.screenY; - var scrollX = $stencilWindow.scrollX; - var scrollY = $stencilWindow.scrollY; - var exports = {}; - - var fetch, FetchError, Headers, Request, Response; - - if (typeof $stencilWindow.fetch === 'function') { - fetch = $stencilWindow.fetch; - } else { - fetch = $stencilWindow.fetch = function() { throw new Error('fetch() is not implemented'); }; - } - - if (typeof $stencilWindow.FetchError === 'function') { - FetchError = $stencilWindow.FetchError; - } else { - FetchError = $stencilWindow.FetchError = class FetchError { constructor() { throw new Error('FetchError is not implemented'); } }; - } - - if (typeof $stencilWindow.Headers === 'function') { - Headers = $stencilWindow.Headers; - } else { - Headers = $stencilWindow.Headers = class Headers { constructor() { throw new Error('Headers is not implemented'); } }; - } - - if (typeof $stencilWindow.Request === 'function') { - Request = $stencilWindow.Request; - } else { - Request = $stencilWindow.Request = class Request { constructor() { throw new Error('Request is not implemented'); } }; - } - - if (typeof $stencilWindow.Response === 'function') { - Response = $stencilWindow.Response; - } else { - Response = $stencilWindow.Response = class Response { constructor() { throw new Error('Response is not implemented'); } }; - } - - function hydrateAppClosure($stencilWindow) { - const window = $stencilWindow; - const document = $stencilWindow.document; - ${HYDRATE_APP_CLOSURE_START} -`; - -export const HYDRATE_FACTORY_OUTRO = ` - /*hydrateAppClosure end*/ - hydrateApp(window, $stencilHydrateOpts, $stencilHydrateResults, $stencilAfterHydrate, $stencilHydrateResolve); - } - - hydrateAppClosure($stencilWindow); -} -`; diff --git a/src/compiler/output-targets/dist-hydrate-script/index.ts b/src/compiler/output-targets/dist-hydrate-script/index.ts deleted file mode 100644 index a51d6593dad..00000000000 --- a/src/compiler/output-targets/dist-hydrate-script/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { isOutputTargetHydrate } from '@utils'; - -import type * as d from '../../../declarations'; -import { generateHydrateApp } from './generate-hydrate-app'; - -export const outputHydrateScript = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -) => { - const hydrateOutputTargets = config.outputTargets.filter(isOutputTargetHydrate); - if (hydrateOutputTargets.length > 0) { - const timespan = buildCtx.createTimeSpan(`generate hydrate app started`); - - await generateHydrateApp(config, compilerCtx, buildCtx, hydrateOutputTargets); - - timespan.finish(`generate hydrate app finished`); - } -}; diff --git a/src/compiler/output-targets/dist-hydrate-script/relocate-hydrate-context.ts b/src/compiler/output-targets/dist-hydrate-script/relocate-hydrate-context.ts deleted file mode 100644 index 7dea71091df..00000000000 --- a/src/compiler/output-targets/dist-hydrate-script/relocate-hydrate-context.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type * as d from '../../../declarations'; -import { getGlobalScriptData } from '../../bundle/app-data-plugin'; -import { HYDRATE_APP_CLOSURE_START } from './hydrate-factory-closure'; - -export const relocateHydrateContextConst = (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, code: string) => { - const globalScripts = getGlobalScriptData(config, compilerCtx); - if (globalScripts.length > 0) { - const startCode = code.indexOf('/*hydrate context start*/'); - if (startCode > -1) { - const endCode = code.indexOf('/*hydrate context end*/') + '/*hydrate context end*/'.length; - const hydrateContextCode = code.substring(startCode, endCode); - code = code.replace(hydrateContextCode, ''); - return code.replace(HYDRATE_APP_CLOSURE_START, HYDRATE_APP_CLOSURE_START + '\n ' + hydrateContextCode); - } - } - return code; -}; diff --git a/src/compiler/output-targets/dist-hydrate-script/test/dist-hydrate-script.spec.ts b/src/compiler/output-targets/dist-hydrate-script/test/dist-hydrate-script.spec.ts deleted file mode 100644 index 8dfdbf235e3..00000000000 --- a/src/compiler/output-targets/dist-hydrate-script/test/dist-hydrate-script.spec.ts +++ /dev/null @@ -1,211 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockBuildCtx, mockCompilerCtx, mockValidatedConfig } from '@stencil/core/testing'; -import path from 'path'; - -import { validateHydrateScript } from '../../../config/outputs/validate-hydrate-script'; -import * as optimizeModuleMod from '../../../optimize/optimize-module'; -import { writeHydrateOutputs } from '../write-hydrate-outputs'; - -describe('dist-hydrate-script', () => { - describe('minification', () => { - let optimizeModuleSpy: jest.SpyInstance; - let mockFs: any; - - beforeEach(() => { - // Spy on optimizeModule to verify it's called with correct minify parameter - optimizeModuleSpy = jest.spyOn(optimizeModuleMod, 'optimizeModule'); - optimizeModuleSpy.mockResolvedValue({ - output: 'const minified="code";', - diagnostics: [], - sourceMap: undefined, - }); - }); - - afterEach(() => { - optimizeModuleSpy.mockRestore(); - }); - - it('should call optimizeModule when outputTarget.minify is true', async () => { - const config = mockValidatedConfig(); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - - // Mock filesystem operations - mockFs = compilerCtx.fs; - mockFs.readFile = jest.fn().mockResolvedValue('{"name":"test"}'); - mockFs.writeFile = jest.fn().mockResolvedValue(undefined); - mockFs.copyFile = jest.fn().mockResolvedValue(undefined); - - const outputTarget: d.OutputTargetHydrate = { - type: 'dist-hydrate-script', - dir: path.join(config.rootDir, 'dist', 'hydrate'), - minify: true, - }; - - const rollupOutput = { - output: [ - { - type: 'chunk' as const, - fileName: 'index.js', - code: 'export const test = "unminified code";', - isEntry: true, - }, - ], - }; - - await writeHydrateOutputs(config, compilerCtx, buildCtx, [outputTarget], rollupOutput as any); - - expect(optimizeModuleSpy).toHaveBeenCalledWith( - config, - compilerCtx, - expect.objectContaining({ - minify: true, - }), - ); - }); - - it('should not call optimizeModule when outputTarget.minify is false', async () => { - const config = mockValidatedConfig(); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - - // Mock filesystem operations - mockFs = compilerCtx.fs; - mockFs.readFile = jest.fn().mockResolvedValue('{"name":"test"}'); - mockFs.writeFile = jest.fn().mockResolvedValue(undefined); - mockFs.copyFile = jest.fn().mockResolvedValue(undefined); - - const outputTarget: d.OutputTargetHydrate = { - type: 'dist-hydrate-script', - dir: path.join(config.rootDir, 'dist', 'hydrate'), - minify: false, - }; - - const rollupOutput = { - output: [ - { - type: 'chunk' as const, - fileName: 'index.js', - code: 'export const test = "unminified code";', - isEntry: true, - }, - ], - }; - - await writeHydrateOutputs(config, compilerCtx, buildCtx, [outputTarget], rollupOutput as any); - - expect(optimizeModuleSpy).not.toHaveBeenCalled(); - }); - - it('should not call optimizeModule when outputTarget.minify is undefined', async () => { - const config = mockValidatedConfig(); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - - // Mock filesystem operations - mockFs = compilerCtx.fs; - mockFs.readFile = jest.fn().mockResolvedValue('{"name":"test"}'); - mockFs.writeFile = jest.fn().mockResolvedValue(undefined); - mockFs.copyFile = jest.fn().mockResolvedValue(undefined); - - const outputTarget: d.OutputTargetHydrate = { - type: 'dist-hydrate-script', - dir: path.join(config.rootDir, 'dist', 'hydrate'), - // minify is undefined - }; - - const rollupOutput = { - output: [ - { - type: 'chunk' as const, - fileName: 'index.js', - code: 'export const test = "unminified code";', - isEntry: true, - }, - ], - }; - - await writeHydrateOutputs(config, compilerCtx, buildCtx, [outputTarget], rollupOutput as any); - - expect(optimizeModuleSpy).not.toHaveBeenCalled(); - }); - }); - - describe('generatePackageJson', () => { - it('should skip writing package.json when generatePackageJson is false', async () => { - const config = mockValidatedConfig(); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - - const mockFs = compilerCtx.fs; - mockFs.readFile = jest.fn().mockResolvedValue('{"name":"test"}'); - mockFs.writeFile = jest.fn().mockResolvedValue(undefined); - mockFs.copyFile = jest.fn().mockResolvedValue(undefined); - - const outputTarget: d.OutputTargetHydrate = { - type: 'dist-hydrate-script', - dir: path.join(config.rootDir, 'dist', 'hydrate'), - generatePackageJson: false, - }; - - const rollupOutput = { - output: [ - { - type: 'chunk' as const, - fileName: 'index.js', - code: 'export const test = "unminified code";', - isEntry: true, - }, - ], - }; - - const [validatedOutputTarget] = validateHydrateScript(config, [outputTarget]); - - await writeHydrateOutputs(config, compilerCtx, buildCtx, [validatedOutputTarget], rollupOutput as any); - - expect(mockFs.copyFile).toHaveBeenCalled(); - expect(mockFs.writeFile).not.toHaveBeenCalledWith( - expect.stringMatching(/dist[\\/]+hydrate[\\/]+package\.json$/), - expect.any(String), - ); - }); - - it('should write package.json by default after validation', async () => { - const config = mockValidatedConfig(); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - - const mockFs = compilerCtx.fs; - mockFs.readFile = jest.fn().mockResolvedValue('{"name":"test"}'); - mockFs.writeFile = jest.fn().mockResolvedValue(undefined); - mockFs.copyFile = jest.fn().mockResolvedValue(undefined); - - const outputTarget: d.OutputTargetHydrate = { - type: 'dist-hydrate-script', - dir: path.join(config.rootDir, 'dist', 'hydrate'), - // generatePackageJson is undefined, should default to true after validation - }; - - const rollupOutput = { - output: [ - { - type: 'chunk' as const, - fileName: 'index.js', - code: 'export const test = "unminified code";', - isEntry: true, - }, - ], - }; - - const [validatedOutputTarget] = validateHydrateScript(config, [outputTarget]); - - await writeHydrateOutputs(config, compilerCtx, buildCtx, [validatedOutputTarget], rollupOutput as any); - - expect(mockFs.copyFile).toHaveBeenCalled(); - expect(mockFs.writeFile).toHaveBeenCalledWith( - expect.stringMatching(/dist[\\/]+hydrate[\\/]+package\.json$/), - expect.stringContaining('"name"'), - ); - }); - }); -}); diff --git a/src/compiler/output-targets/dist-hydrate-script/update-to-hydrate-components.ts b/src/compiler/output-targets/dist-hydrate-script/update-to-hydrate-components.ts deleted file mode 100644 index e6dc7532354..00000000000 --- a/src/compiler/output-targets/dist-hydrate-script/update-to-hydrate-components.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { dashToPascalCase, sortBy, toTitleCase } from '@utils'; - -import type * as d from '../../../declarations'; - -export const updateToHydrateComponents = async (cmps: d.ComponentCompilerMeta[]) => { - const hydrateCmps = await Promise.all(cmps.map(updateToHydrateComponent)); - return sortBy(hydrateCmps, (c) => c.cmp.componentClassName); -}; - -const updateToHydrateComponent = async (cmp: d.ComponentCompilerMeta) => { - const cmpData: d.ComponentCompilerData = { - filePath: cmp.sourceFilePath, - exportLine: ``, - cmp: cmp, - uniqueComponentClassName: ``, - importLine: ``, - }; - - const pascalCasedClassName = dashToPascalCase(toTitleCase(cmp.tagName)); - - if (cmp.componentClassName !== pascalCasedClassName) { - cmpData.uniqueComponentClassName = pascalCasedClassName; - cmpData.importLine = `import { ${cmp.componentClassName} as ${cmpData.uniqueComponentClassName} } from '${cmpData.filePath}';`; - } else { - cmpData.uniqueComponentClassName = cmp.componentClassName; - cmpData.importLine = `import { ${cmpData.uniqueComponentClassName} } from '${cmpData.filePath}';`; - } - - return cmpData; -}; diff --git a/src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts b/src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts deleted file mode 100644 index 27acbb8b54b..00000000000 --- a/src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { hasError, join } from '@utils'; -import { basename } from 'path'; -import type { RollupOutput } from 'rollup'; - -import type * as d from '../../../declarations'; -import { optimizeModule } from '../../optimize/optimize-module'; -import { MODE_RESOLUTION_CHAIN_DECLARATION } from './hydrate-factory-closure'; -import { relocateHydrateContextConst } from './relocate-hydrate-context'; - -export const writeHydrateOutputs = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTargets: d.OutputTargetHydrate[], - rollupOutput: RollupOutput, -) => { - return Promise.all( - outputTargets.map((outputTarget) => { - return writeHydrateOutput(config, compilerCtx, buildCtx, outputTarget, rollupOutput); - }), - ); -}; - -const writeHydrateOutput = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTarget: d.OutputTargetHydrate, - rollupOutput: RollupOutput, -) => { - const hydratePackageName = await getHydratePackageName(config, compilerCtx); - - const hydrateAppDirPath = outputTarget.dir; - if (!hydrateAppDirPath) { - throw new Error(`outputTarget config missing the "dir" property`); - } - - const hydrateCoreIndexPath = join(hydrateAppDirPath, 'index.js'); - const hydrateCoreIndexPathESM = join(hydrateAppDirPath, 'index.mjs'); - const hydrateCoreIndexDtsFilePath = join(hydrateAppDirPath, 'index.d.ts'); - - const writeOperations: Promise[] = [copyHydrateRunnerDts(config, compilerCtx, hydrateAppDirPath)]; - - if (outputTarget.generatePackageJson) { - const pkgJsonPath = join(hydrateAppDirPath, 'package.json'); - const pkgJsonCode = getHydratePackageJson( - config, - hydrateCoreIndexPath, - hydrateCoreIndexPathESM, - hydrateCoreIndexDtsFilePath, - hydratePackageName, - ); - - writeOperations.push(compilerCtx.fs.writeFile(pkgJsonPath, pkgJsonCode)); - } - - await Promise.all(writeOperations); - - // always remember a path to the hydrate app that the prerendering may need later on - buildCtx.hydrateAppFilePath = hydrateCoreIndexPath; - const minify = outputTarget.minify === true; - - await Promise.all( - rollupOutput.output.map(async (output) => { - if (output.type === 'chunk') { - let code = relocateHydrateContextConst(config, compilerCtx, output.code); - - /** - * Enable the line where we define `modeResolutionChain` for the hydrate module. - */ - code = code.replace( - `// const ${MODE_RESOLUTION_CHAIN_DECLARATION}`, - `const ${MODE_RESOLUTION_CHAIN_DECLARATION}`, - ); - - /** - * Inject the $stencilTagTransform variable definition. - * This variable is referenced by the factory closure (HYDRATE_FACTORY_INTRO) - * and must be defined at module scope to be accessible within the factory. - * We inject it after the tag transform functions are defined/exported. - */ - const tagTransformFunctionPattern = /function (setTagTransformer|transformTag)\(/; - const match = code.match(tagTransformFunctionPattern); - if (match) { - // Find where setTagTransformer and transformTag functions are defined - // and inject the $stencilTagTransform variable after them - const injectCode = `\n// Tag transform state object for factory closure\nvar $stencilTagTransform = { setTagTransformer: setTagTransformer, transformTag: transformTag };\n`; - - // Find the last occurrence of tag transform function definitions - const lastTransformTagIndex = code.lastIndexOf('function transformTag('); - const lastSetTagTransformerIndex = code.lastIndexOf('function setTagTransformer('); - const injectionPoint = Math.max(lastTransformTagIndex, lastSetTagTransformerIndex); - - if (injectionPoint !== -1) { - // Find the end of that function (closing brace) - let braceCount = 0; - let foundStart = false; - let injectionIndex = injectionPoint; - - for (let i = injectionPoint; i < code.length; i++) { - if (code[i] === '{') { - foundStart = true; - braceCount++; - } else if (code[i] === '}') { - braceCount--; - if (foundStart && braceCount === 0) { - injectionIndex = i + 1; - break; - } - } - } - - code = code.slice(0, injectionIndex) + injectCode + code.slice(injectionIndex); - } - } - - if (minify) { - const optimizeResults = await optimizeModule(config, compilerCtx, { - input: code, - isCore: output.isEntry, - minify, - }); - - buildCtx.diagnostics.push(...optimizeResults.diagnostics); - if (!hasError(optimizeResults.diagnostics) && typeof optimizeResults.output === 'string') { - code = optimizeResults.output; - } - } - - const filePath = join(hydrateAppDirPath, output.fileName); - await compilerCtx.fs.writeFile(filePath, code, { immediateWrite: true }); - } - }), - ); -}; - -const getHydratePackageJson = ( - config: d.ValidatedConfig, - hydrateAppFilePathCJS: string, - hydrateAppFilePathESM: string, - hydrateDtsFilePath: string, - hydratePackageName: string, -) => { - const pkg: d.PackageJsonData = { - name: hydratePackageName, - description: `${config.namespace} component hydration app.`, - main: basename(hydrateAppFilePathCJS), - types: basename(hydrateDtsFilePath), - exports: { - '.': { - require: `./${basename(hydrateAppFilePathCJS)}`, - import: `./${basename(hydrateAppFilePathESM)}`, - }, - }, - }; - return JSON.stringify(pkg, null, 2); -}; - -const getHydratePackageName = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx) => { - const directoryName = basename(config.rootDir); - try { - const rootPkgFilePath = join(config.rootDir, 'package.json'); - const pkgStr = await compilerCtx.fs.readFile(rootPkgFilePath); - const pkgData = JSON.parse(pkgStr) as d.PackageJsonData; - const scope = pkgData.name || directoryName; - return `${scope}/hydrate`; - } catch (e) {} - - return `${config.fsNamespace || directoryName}/hydrate`; -}; - -const copyHydrateRunnerDts = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - hydrateAppDirPath: string, -) => { - const packageDir = join(config.sys.getCompilerExecutingPath(), '..', '..'); - const srcHydrateDir = join(packageDir, 'internal', 'hydrate', 'runner.d.ts'); - - const runnerDtsDestPath = join(hydrateAppDirPath, 'index.d.ts'); - - await compilerCtx.fs.copyFile(srcHydrateDir, runnerDtsDestPath); -}; diff --git a/src/compiler/output-targets/dist-lazy/generate-cjs.ts b/src/compiler/output-targets/dist-lazy/generate-cjs.ts deleted file mode 100644 index 9a94baf62aa..00000000000 --- a/src/compiler/output-targets/dist-lazy/generate-cjs.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { generatePreamble, join, relativeImport } from '@utils'; -import type { OutputOptions, RollupBuild } from 'rollup'; - -import type * as d from '../../../declarations'; -import { generateRollupOutput } from '../../app-core/bundle-app-core'; -import { generateLazyModules } from './generate-lazy-module'; -import { lazyBundleIdPlugin } from './lazy-bundleid-plugin'; - -export const generateCjs = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - rollupBuild: RollupBuild, - outputTargets: d.OutputTargetDistLazy[], -): Promise => { - const cjsOutputs = outputTargets.filter((o) => !!o.cjsDir); - - if (cjsOutputs.length > 0) { - const outputTargetType = cjsOutputs[0].type; - const esmOpts: OutputOptions = { - banner: generatePreamble(config), - format: 'cjs', - entryFileNames: '[name].cjs.js', - assetFileNames: '[name]-[hash][extname]', - sourcemap: config.sourceMap, - plugins: [lazyBundleIdPlugin(buildCtx, config, false, '.cjs')], - }; - - if (!!config.extras.experimentalImportInjection || !!config.extras.enableImportInjection) { - esmOpts.interop = 'auto'; - esmOpts.dynamicImportInCjs = false; - } - - const results = await generateRollupOutput(rollupBuild, esmOpts, config, buildCtx.entryModules); - if (results != null) { - const destinations = cjsOutputs - .map((o) => o.cjsDir) - .filter((cjsDir): cjsDir is string => typeof cjsDir === 'string'); - - buildCtx.commonJsComponentBundle = await generateLazyModules( - config, - compilerCtx, - buildCtx, - outputTargetType, - destinations, - results, - 'es2017', - false, - ); - - await generateShortcuts(compilerCtx, results, cjsOutputs); - } - } - - return { name: 'cjs', buildCtx }; -}; - -const generateShortcuts = ( - compilerCtx: d.CompilerCtx, - rollupResult: d.RollupResult[], - outputTargets: d.OutputTargetDistLazy[], -): Promise => { - const indexFilename = rollupResult.find((r) => r.type === 'chunk' && r.isIndex).fileName; - return Promise.all( - outputTargets.map(async (o) => { - if (o.cjsIndexFile) { - const entryPointPath = join(o.cjsDir, indexFilename); - const relativePath = relativeImport(o.cjsIndexFile, entryPointPath); - const shortcutContent = `module.exports = require('${relativePath}');\n`; - await compilerCtx.fs.writeFile(o.cjsIndexFile, shortcutContent, { outputTargetType: o.type }); - } - }), - ); -}; diff --git a/src/compiler/output-targets/dist-lazy/generate-esm-browser.ts b/src/compiler/output-targets/dist-lazy/generate-esm-browser.ts deleted file mode 100644 index a0dc4d8e770..00000000000 --- a/src/compiler/output-targets/dist-lazy/generate-esm-browser.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { generatePreamble } from '@utils'; -import type { OutputOptions, RollupBuild } from 'rollup'; - -import type * as d from '../../../declarations'; -import { generateRollupOutput } from '../../app-core/bundle-app-core'; -import { generateLazyModules } from './generate-lazy-module'; -import { lazyBundleIdPlugin } from './lazy-bundleid-plugin'; - -export const generateEsmBrowser = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - rollupBuild: RollupBuild, - outputTargets: d.OutputTargetDistLazy[], -): Promise => { - const esmOutputs = outputTargets.filter((o) => !!o.esmDir && !!o.isBrowserBuild); - if (esmOutputs.length) { - const outputTargetType = esmOutputs[0].type; - const esmOpts: OutputOptions = { - banner: generatePreamble(config), - format: 'es', - entryFileNames: '[name].esm.js', - chunkFileNames: config.hashFileNames ? 'p-[hash].js' : '[name]-[hash].js', - assetFileNames: config.hashFileNames ? 'p-[hash][extname]' : '[name]-[hash][extname]', - sourcemap: config.sourceMap, - plugins: [lazyBundleIdPlugin(buildCtx, config, config.hashFileNames, '', true)], - }; - - const output = await generateRollupOutput(rollupBuild, esmOpts, config, buildCtx.entryModules); - - if (output != null) { - const es2017destinations = esmOutputs - .map((o) => o.esmDir) - .filter((esmDir): esmDir is string => typeof esmDir === 'string'); - buildCtx.esmBrowserComponentBundle = await generateLazyModules( - config, - compilerCtx, - buildCtx, - outputTargetType, - es2017destinations, - output, - 'es2017', - true, - ); - } - } - - return { name: 'esm-browser', buildCtx }; -}; diff --git a/src/compiler/output-targets/dist-lazy/generate-esm.ts b/src/compiler/output-targets/dist-lazy/generate-esm.ts deleted file mode 100644 index fdf9920bced..00000000000 --- a/src/compiler/output-targets/dist-lazy/generate-esm.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { generatePreamble, join, relativeImport } from '@utils'; -import type { OutputOptions, RollupBuild } from 'rollup'; - -import type * as d from '../../../declarations'; -import type { RollupResult } from '../../../declarations'; -import { generateRollupOutput } from '../../app-core/bundle-app-core'; -import { generateLazyModules } from './generate-lazy-module'; -import { lazyBundleIdPlugin } from './lazy-bundleid-plugin'; - -export const generateEsm = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - rollupBuild: RollupBuild, - outputTargets: d.OutputTargetDistLazy[], -): Promise => { - const esmEs5Outputs = config.buildEs5 ? outputTargets.filter((o) => !!o.esmEs5Dir && !o.isBrowserBuild) : []; - const esmOutputs = outputTargets.filter((o) => !!o.esmDir && !o.isBrowserBuild); - if (esmOutputs.length + esmEs5Outputs.length > 0) { - const esmOpts: OutputOptions = { - banner: generatePreamble(config), - format: 'es', - entryFileNames: '[name].js', - assetFileNames: '[name]-[hash][extname]', - sourcemap: config.sourceMap, - plugins: [lazyBundleIdPlugin(buildCtx, config, false, '')], - }; - const outputTargetType = esmOutputs[0].type; - const output = await generateRollupOutput(rollupBuild, esmOpts, config, buildCtx.entryModules); - - if (output != null) { - const es2017destinations = esmOutputs - .map((o) => o.esmDir) - .filter((esmDir): esmDir is string => typeof esmDir === 'string'); - buildCtx.esmComponentBundle = await generateLazyModules( - config, - compilerCtx, - buildCtx, - outputTargetType, - es2017destinations, - output, - 'es2017', - false, - ); - - const es5destinations = esmEs5Outputs - .map((o) => o.esmEs5Dir) - .filter((esmEs5Dir): esmEs5Dir is string => typeof esmEs5Dir === 'string'); - buildCtx.es5ComponentBundle = await generateLazyModules( - config, - compilerCtx, - buildCtx, - outputTargetType, - es5destinations, - output, - 'es5', - false, - ); - - if (config.buildEs5) { - await copyPolyfills(config, compilerCtx, esmOutputs); - } - await generateShortcuts(config, compilerCtx, outputTargets, output); - } - } - - return { name: 'esm', buildCtx }; -}; - -/** - * Copy polyfills from `$INSTALL_DIR/internal/client/polyfills` to the lazy - * loader output directory where $INSTALL_DIR is the directory in which the - * `@stencil/core` package is installed. - * - * @param config a validated Stencil configuration - * @param compilerCtx the current compiler context - * @param outputTargets dist-lazy output targets - */ -const copyPolyfills = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - outputTargets: d.OutputTargetDistLazy[], -): Promise => { - const destinations = outputTargets - .filter((o) => o.polyfills) - .map((o) => o.esmDir) - .filter((esmDir): esmDir is string => typeof esmDir === 'string'); - if (destinations.length === 0) { - return; - } - - const src = join(config.sys.getCompilerExecutingPath(), '..', '..', 'internal', 'client', 'polyfills'); - const files = await compilerCtx.fs.readdir(src); - - await Promise.all( - destinations.map((dest) => { - return Promise.all( - files.map((f) => { - return compilerCtx.fs.copyFile(f.absPath, join(dest, 'polyfills', f.relPath)); - }), - ); - }), - ); -}; - -const generateShortcuts = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - outputTargets: d.OutputTargetDistLazy[], - rollupResult: RollupResult[], -): Promise => { - const indexFilename = rollupResult.find((r) => r.type === 'chunk' && r.isIndex).fileName; - - return Promise.all( - outputTargets.map(async (o) => { - if (o.esmDir && o.esmIndexFile) { - const entryPointPath = - config.buildEs5 && o.esmEs5Dir ? join(o.esmEs5Dir, indexFilename) : join(o.esmDir, indexFilename); - - const relativePath = relativeImport(o.esmIndexFile, entryPointPath); - const shortcutContent = `export * from '${relativePath}';`; - await compilerCtx.fs.writeFile(o.esmIndexFile, shortcutContent, { outputTargetType: o.type }); - } - }), - ); -}; diff --git a/src/compiler/output-targets/dist-lazy/generate-lazy-module.ts b/src/compiler/output-targets/dist-lazy/generate-lazy-module.ts deleted file mode 100644 index fd869ca4868..00000000000 --- a/src/compiler/output-targets/dist-lazy/generate-lazy-module.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { - formatComponentRuntimeMeta, - getSourceMappingUrlForEndOfFile, - hasDependency, - join, - rollupToStencilSourceMap, - stringifyRuntimeData, -} from '@utils'; -import type { SourceMap as RollupSourceMap } from 'rollup'; - -import type * as d from '../../../declarations'; -import { optimizeModule } from '../../optimize/optimize-module'; -import { writeLazyModule } from './write-lazy-entry-module'; - -export const generateLazyModules = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTargetType: string, - destinations: string[], - results: d.RollupResult[], - sourceTarget: d.SourceTarget, - isBrowserBuild: boolean, -): Promise => { - if (!Array.isArray(destinations) || destinations.length === 0) { - return []; - } - const shouldMinify = !!(config.minifyJs && isBrowserBuild); - const rollupResults = results.filter((r): r is d.RollupChunkResult => r.type === 'chunk'); - const entryComponentsResults = rollupResults.filter((rollupResult) => rollupResult.isComponent); - const chunkResults = rollupResults.filter((rollupResult) => !rollupResult.isComponent && !rollupResult.isEntry); - - const bundleModules = await Promise.all( - entryComponentsResults.map((rollupResult) => { - return generateLazyEntryModule( - config, - compilerCtx, - buildCtx, - rollupResult, - outputTargetType, - destinations, - sourceTarget, - shouldMinify, - isBrowserBuild, - ); - }), - ); - - if ((!!config.extras.experimentalImportInjection || !!config.extras.enableImportInjection) && !isBrowserBuild) { - addStaticImports(rollupResults, bundleModules); - } - - await Promise.all( - chunkResults.map((rollupResult) => { - return writeLazyChunk( - config, - compilerCtx, - buildCtx, - rollupResult, - outputTargetType, - destinations, - sourceTarget, - shouldMinify, - isBrowserBuild, - ); - }), - ); - - const lazyRuntimeData = formatLazyBundlesRuntimeMeta(bundleModules); - const entryResults = rollupResults.filter((rollupResult) => !rollupResult.isComponent && rollupResult.isEntry); - await Promise.all( - entryResults.map((rollupResult) => { - return writeLazyEntry( - config, - compilerCtx, - buildCtx, - rollupResult, - outputTargetType, - destinations, - lazyRuntimeData, - sourceTarget, - shouldMinify, - isBrowserBuild, - ); - }), - ); - - await Promise.all( - results - .filter((r): r is d.RollupAssetResult => r.type === 'asset') - .map((r: d.RollupAssetResult) => { - return Promise.all( - destinations.map((dest) => { - return compilerCtx.fs.writeFile(join(dest, r.fileName), r.content); - }), - ); - }), - ); - - return bundleModules; -}; - -/** - * Add imports for each bundle to Stencil's lazy loader. Some bundlers that are built atop of Rollup strictly impose - * the limitations that are laid out in https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations. - * This function injects an explicit import statement for each bundle that can be lazily loaded. - * @param rollupChunkResults the results of running Rollup across a Stencil project - * @param bundleModules lazy-loadable modules that can be resolved at runtime - */ -const addStaticImports = (rollupChunkResults: d.RollupChunkResult[], bundleModules: d.BundleModule[]): void => { - rollupChunkResults.filter(isStencilCoreResult).forEach((index: d.RollupChunkResult) => { - const generateCjs = isCjsFormat(index) ? generateCaseClauseCjs : generateCaseClause; - index.code = index.code.replace( - '/*!__STENCIL_STATIC_IMPORT_SWITCH__*/', - ` - if (!hmrVersionId || !BUILD.hotModuleReplacement) { - const processMod = importedModule => { - cmpModules.set(bundleId, importedModule); - return importedModule[exportName]; - } - switch(bundleId) { - ${bundleModules.map((mod) => generateCjs(mod.output.bundleId)).join('')} - } - }`, - ); - }); -}; - -/** - * Determine if a Rollup output chunk contains Stencil runtime code - * @param rollupChunkResult the rollup chunk output to test - * @returns true if the output chunk contains Stencil runtime code, false otherwise - */ -const isStencilCoreResult = (rollupChunkResult: d.RollupChunkResult): boolean => { - return ( - rollupChunkResult.isCore && - rollupChunkResult.entryKey === 'index' && - (rollupChunkResult.moduleFormat === 'es' || - rollupChunkResult.moduleFormat === 'esm' || - isCjsFormat(rollupChunkResult)) - ); -}; - -/** - * Helper function to determine if a Rollup chunk has a commonjs module format - * @param rollupChunkResult the Rollup result to test - * @returns true if the Rollup chunk has a commonjs module format, false otherwise - */ -const isCjsFormat = (rollupChunkResult: d.RollupChunkResult): boolean => { - return rollupChunkResult.moduleFormat === 'cjs' || rollupChunkResult.moduleFormat === 'commonjs'; -}; - -/** - * Generate a 'case' clause to be used within a `switch` statement. The case clause generated will key-off the provided - * bundle ID for a component, and load a file (tied to that ID) at runtime. - * @param bundleId the name of the bundle to load - * @returns the case clause that will load the component's file at runtime - */ -const generateCaseClause = (bundleId: string): string => { - return ` - case '${bundleId}': - return import( - /* webpackMode: "lazy" */ - './${bundleId}.entry.js').then(processMod, consoleError);`; -}; - -/** - * Generate a 'case' clause to be used within a `switch` statement. The case clause generated will key-off the provided - * bundle ID for a component, and load a CommonJS file (tied to that ID) at runtime. - * @param bundleId the name of the bundle to load - * @returns the case clause that will load the component's file at runtime - */ -const generateCaseClauseCjs = (bundleId: string): string => { - return ` - case '${bundleId}': - return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require( - /* webpackMode: "lazy" */ - './${bundleId}.entry.js')); }).then(processMod, consoleError);`; -}; - -const generateLazyEntryModule = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - rollupResult: d.RollupChunkResult, - outputTargetType: string, - destinations: string[], - sourceTarget: d.SourceTarget, - shouldMinify: boolean, - isBrowserBuild: boolean, -): Promise => { - const entryModule = buildCtx.entryModules.find((entryModule) => entryModule.entryKey === rollupResult.entryKey); - - const { code, sourceMap } = await convertChunk( - config, - compilerCtx, - buildCtx, - sourceTarget, - shouldMinify, - false, - isBrowserBuild, - rollupResult.code, - rollupResult.map, - ); - - const output = await writeLazyModule(compilerCtx, outputTargetType, destinations, code, sourceMap, rollupResult); - - return { - rollupResult, - entryKey: rollupResult.entryKey, - cmps: entryModule.cmps, - output, - }; -}; - -const writeLazyChunk = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - rollupResult: d.RollupChunkResult, - outputTargetType: string, - destinations: string[], - sourceTarget: d.SourceTarget, - shouldMinify: boolean, - isBrowserBuild: boolean, -) => { - const { code, sourceMap } = await convertChunk( - config, - compilerCtx, - buildCtx, - sourceTarget, - shouldMinify, - rollupResult.isCore, - isBrowserBuild, - rollupResult.code, - rollupResult.map, - ); - - await Promise.all( - destinations.map((dst) => { - const filePath = join(dst, rollupResult.fileName); - let fileCode = code; - const writes: Promise[] = []; - if (sourceMap) { - fileCode = code + getSourceMappingUrlForEndOfFile(rollupResult.fileName); - writes.push(compilerCtx.fs.writeFile(filePath + '.map', JSON.stringify(sourceMap), { outputTargetType })); - } - writes.push(compilerCtx.fs.writeFile(filePath, fileCode, { outputTargetType })); - return Promise.all(writes); - }), - ); -}; - -const writeLazyEntry = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - rollupResult: d.RollupChunkResult, - outputTargetType: string, - destinations: string[], - lazyRuntimeData: string, - sourceTarget: d.SourceTarget, - shouldMinify: boolean, - isBrowserBuild: boolean, -): Promise => { - if (isBrowserBuild && ['loader'].includes(rollupResult.entryKey)) { - return; - } - const inputCode = rollupResult.code.replace(`[/*!__STENCIL_LAZY_DATA__*/]`, `${lazyRuntimeData}`); - const { code, sourceMap } = await convertChunk( - config, - compilerCtx, - buildCtx, - sourceTarget, - shouldMinify, - false, - isBrowserBuild, - inputCode, - rollupResult.map, - ); - - await Promise.all( - destinations.map((dst) => { - const filePath = join(dst, rollupResult.fileName); - let fileCode = code; - const writes: Promise[] = []; - if (sourceMap) { - fileCode = code + getSourceMappingUrlForEndOfFile(rollupResult.fileName); - writes.push(compilerCtx.fs.writeFile(filePath + '.map', JSON.stringify(sourceMap), { outputTargetType })); - } - writes.push(compilerCtx.fs.writeFile(filePath, fileCode, { outputTargetType })); - return Promise.all(writes); - }), - ); -}; - -/** - * Sorts, formats, and stringifies the bundles for a lazy build of a Stencil project. - * - * @param bundleModules The modules for the Stencil lazy build emitted from Rollup. - * @returns A stringified representation of the lazy bundles. - */ -const formatLazyBundlesRuntimeMeta = (bundleModules: d.BundleModule[]): string => { - const sortedBundles = bundleModules.slice().sort(sortBundleModules); - const lazyBundles = sortedBundles.map(formatLazyRuntimeBundle); - return stringifyRuntimeData(lazyBundles); -}; - -/** - * Formats a bundle module into a tuple of bundle ID and component metadata for use at runtime. - * - * @param bundleModule The bundle module to format. - * @returns A tuple of bundle ID and component metadata. - */ -const formatLazyRuntimeBundle = (bundleModule: d.BundleModule): d.LazyBundleRuntimeData => { - const bundleId = bundleModule.output.bundleId; - const bundleCmps = bundleModule.cmps.slice().sort(sortBundleComponents); - return [bundleId, bundleCmps.map((cmp) => formatComponentRuntimeMeta(cmp, true))]; -}; - -/** - * Sorts bundle modules by the number of dependents, dependencies, and containing component tags. - * Dependencies/dependents may also include components that are statically slotted into other components. - * The order of the bundle modules is important because it determines the order in which the bundles are loaded - * and subsequently the order that their respective components are defined and connected (i.e. via the `connectedCallback`) - * at runtime. - * - * This must be a valid {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#comparefn | compareFn} - * - * @param a The first argument to compare. - * @param b The second argument to compare. - * @returns A number indicating whether the first argument is less than/greater than/equal to the second argument. - */ -export const sortBundleModules = (a: d.BundleModule, b: d.BundleModule): -1 | 1 | 0 => { - const aDependents = a.cmps.reduce((dependents, cmp) => { - dependents.push(...cmp.dependents); - return dependents; - }, [] as string[]); - const bDependents = b.cmps.reduce((dependents, cmp) => { - dependents.push(...cmp.dependents); - return dependents; - }, [] as string[]); - - if (a.cmps.some((cmp) => bDependents.includes(cmp.tagName))) return 1; - if (b.cmps.some((cmp) => aDependents.includes(cmp.tagName))) return -1; - - const aDependencies = a.cmps.reduce((dependencies, cmp) => { - dependencies.push(...cmp.dependencies); - return dependencies; - }, [] as string[]); - const bDependencies = b.cmps.reduce((dependencies, cmp) => { - dependencies.push(...cmp.dependencies); - return dependencies; - }, [] as string[]); - - if (a.cmps.some((cmp) => bDependencies.includes(cmp.tagName))) return -1; - if (b.cmps.some((cmp) => aDependencies.includes(cmp.tagName))) return 1; - - if (aDependents.length < bDependents.length) return -1; - if (aDependents.length > bDependents.length) return 1; - - if (aDependencies.length > bDependencies.length) return -1; - if (aDependencies.length < bDependencies.length) return 1; - - const aTags = a.cmps.map((cmp) => cmp.tagName); - const bTags = b.cmps.map((cmp) => cmp.tagName); - - if (aTags.length > bTags.length) return -1; - if (aTags.length < bTags.length) return 1; - - const aTagsStr = aTags.sort().join('.'); - const bTagsStr = bTags.sort().join('.'); - - if (aTagsStr < bTagsStr) return -1; - if (aTagsStr > bTagsStr) return 1; - - return 0; -}; - -export const sortBundleComponents = (a: d.ComponentCompilerMeta, b: d.ComponentCompilerMeta): -1 | 1 | 0 => { - // - // - // - // - // - - // cmp-c is a dependency of cmp-a and cmp-b - // cmp-c is a directDependency of cmp-b - // cmp-a is a dependant of cmp-b and cmp-c - // cmp-a is a directDependant of cmp-b - - if (a.directDependents.includes(b.tagName)) return 1; - if (b.directDependents.includes(a.tagName)) return -1; - - if (a.directDependencies.includes(b.tagName)) return 1; - if (b.directDependencies.includes(a.tagName)) return -1; - - if (a.dependents.includes(b.tagName)) return 1; - if (b.dependents.includes(a.tagName)) return -1; - - if (a.dependencies.includes(b.tagName)) return 1; - if (b.dependencies.includes(a.tagName)) return -1; - - if (a.dependents.length < b.dependents.length) return -1; - if (a.dependents.length > b.dependents.length) return 1; - - if (a.dependencies.length > b.dependencies.length) return -1; - if (a.dependencies.length < b.dependencies.length) return 1; - - if (a.tagName < b.tagName) return -1; - if (a.tagName > b.tagName) return 1; - - return 0; -}; - -const convertChunk = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - sourceTarget: d.SourceTarget, - shouldMinify: boolean, - isCore: boolean, - isBrowserBuild: boolean, - code: string, - rollupSrcMap: RollupSourceMap, -) => { - let sourceMap = rollupToStencilSourceMap(rollupSrcMap); - const inlineHelpers = isBrowserBuild || !hasDependency(buildCtx, 'tslib'); - const optimizeResults = await optimizeModule(config, compilerCtx, { - input: code, - sourceMap, - isCore, - sourceTarget, - inlineHelpers, - minify: shouldMinify, - }); - buildCtx.diagnostics.push(...optimizeResults.diagnostics); - - if (typeof optimizeResults.output === 'string') { - code = optimizeResults.output; - } - - if (optimizeResults.sourceMap) { - sourceMap = optimizeResults.sourceMap; - } - return { code, sourceMap }; -}; diff --git a/src/compiler/output-targets/dist-lazy/generate-system.ts b/src/compiler/output-targets/dist-lazy/generate-system.ts deleted file mode 100644 index 785f7ff19fd..00000000000 --- a/src/compiler/output-targets/dist-lazy/generate-system.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { generatePreamble, join, relativeImport } from '@utils'; -import type { OutputOptions, RollupBuild } from 'rollup'; - -import type * as d from '../../../declarations'; -import { getAppBrowserCorePolyfills } from '../../app-core/app-polyfills'; -import { generateRollupOutput } from '../../app-core/bundle-app-core'; -import { generateLazyModules } from './generate-lazy-module'; -import { lazyBundleIdPlugin } from './lazy-bundleid-plugin'; - -export const generateSystem = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - rollupBuild: RollupBuild, - outputTargets: d.OutputTargetDistLazy[], -): Promise => { - const systemOutputs = outputTargets.filter((o) => !!o.systemDir); - - if (systemOutputs.length > 0) { - const esmOpts: OutputOptions = { - banner: generatePreamble(config), - format: 'system', - entryFileNames: config.hashFileNames ? 'p-[hash].system.js' : '[name].system.js', - chunkFileNames: config.hashFileNames ? 'p-[hash].system.js' : '[name]-[hash].system.js', - assetFileNames: config.hashFileNames ? 'p-[hash][extname]' : '[name]-[hash][extname]', - sourcemap: config.sourceMap, - plugins: [lazyBundleIdPlugin(buildCtx, config, config.hashFileNames, '.system', true)], - }; - const results = await generateRollupOutput(rollupBuild, esmOpts, config, buildCtx.entryModules); - if (results != null) { - const destinations = systemOutputs - .map((o) => o.esmDir) - .filter((esmDir): esmDir is string => typeof esmDir === 'string'); - buildCtx.systemComponentBundle = await generateLazyModules( - config, - compilerCtx, - buildCtx, - outputTargets[0].type, - destinations, - results, - 'es5', - true, - ); - - await generateSystemLoaders(config, compilerCtx, results, systemOutputs); - } - } - - return { name: 'system', buildCtx }; -}; - -const generateSystemLoaders = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - rollupResult: d.RollupResult[], - systemOutputs: d.OutputTargetDistLazy[], -): Promise => { - const loaderFilename = rollupResult.find((r) => r.type === 'chunk' && r.isBrowserLoader).fileName; - - return Promise.all(systemOutputs.map((o) => writeSystemLoader(config, compilerCtx, loaderFilename, o))); -}; - -const writeSystemLoader = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - loaderFilename: string, - outputTarget: d.OutputTargetDistLazy, -): Promise => { - if (outputTarget.systemLoaderFile) { - const entryPointPath = join(outputTarget.systemDir, loaderFilename); - const relativePath = relativeImport(outputTarget.systemLoaderFile, entryPointPath); - const loaderContent = await getSystemLoader(config, compilerCtx, relativePath, !!outputTarget.polyfills); - await compilerCtx.fs.writeFile(outputTarget.systemLoaderFile, loaderContent, { - outputTargetType: outputTarget.type, - }); - } -}; - -const getSystemLoader = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - corePath: string, - includePolyfills: boolean, -): Promise => { - const polyfills = includePolyfills - ? await getAppBrowserCorePolyfills(config, compilerCtx) - : '/* polyfills excluded */'; - return ` -'use strict'; -(function () { - var currentScript = document.currentScript; - - // Safari 10 support type="module" but still download and executes the nomodule script - if (!currentScript || !currentScript.hasAttribute('nomodule') || !('onbeforeload' in currentScript)) { - - ${polyfills} - - // Figure out currentScript (for IE11, since it does not support currentScript) - var regex = /\\/${config.fsNamespace}(\\.esm)?\\.js($|\\?|#)/; - var scriptElm = currentScript || Array.from(document.querySelectorAll('script')).find(function(s) { - return regex.test(s.src) || s.getAttribute('data-stencil-namespace') === "${config.fsNamespace}"; - }); - - var resourcesUrl = scriptElm ? scriptElm.getAttribute('data-resources-url') || scriptElm.src : ''; - var start = function() { - // if src is not present then origin is "null", and new URL() throws TypeError: Failed to construct 'URL': Invalid base URL - var url = new URL('${corePath}', new URL(resourcesUrl, window.location.origin !== 'null' ? window.location.origin : undefined)); - System.import(url.href); - }; - - start(); - - // Note: using .call(window) here because the self-executing function needs - // to be scoped to the window object for the ES6Promise polyfill to work - } -}).call(window); -`; -}; diff --git a/src/compiler/output-targets/dist-lazy/lazy-build-conditionals.ts b/src/compiler/output-targets/dist-lazy/lazy-build-conditionals.ts deleted file mode 100644 index 09d483bdb9a..00000000000 --- a/src/compiler/output-targets/dist-lazy/lazy-build-conditionals.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { isOutputTargetHydrate } from '@utils'; - -import type * as d from '../../../declarations'; -import { getBuildFeatures, updateBuildConditionals } from '../../app-core/app-data'; - -export const getLazyBuildConditionals = ( - config: d.ValidatedConfig, - cmps: d.ComponentCompilerMeta[], -): d.BuildConditionals => { - const build = getBuildFeatures(cmps) as d.BuildConditionals; - - build.lazyLoad = true; - build.hydrateServerSide = false; - // TODO: deprecated in favour of `setTagTransformer` and `transformTag`. Remove in 5.0 - build.transformTagName = config.extras.tagNameTransform; - build.asyncQueue = config.taskQueue === 'congestionAsync'; - build.taskQueue = config.taskQueue !== 'immediate'; - build.initializeNextTick = config.extras.initializeNextTick; - - const hasHydrateOutputTargets = config.outputTargets.some(isOutputTargetHydrate); - build.hydrateClientSide = hasHydrateOutputTargets; - - updateBuildConditionals(config, build); - - return build; -}; diff --git a/src/compiler/output-targets/dist-lazy/lazy-component-plugin.ts b/src/compiler/output-targets/dist-lazy/lazy-component-plugin.ts deleted file mode 100644 index 8048f7709d5..00000000000 --- a/src/compiler/output-targets/dist-lazy/lazy-component-plugin.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { normalizePath } from '@utils'; -import type { Plugin } from 'rollup'; - -import type * as d from '../../../declarations'; - -export const lazyComponentPlugin = (buildCtx: d.BuildCtx): Plugin => { - const entrys = new Map(); - - const plugin: Plugin = { - name: 'lazyComponentPlugin', - - resolveId(importee) { - const entryModule = buildCtx.entryModules.find((entryModule) => entryModule.entryKey === importee); - if (entryModule) { - entrys.set(importee, entryModule); - return importee; - } - - return null; - }, - - load(id) { - const entryModule = entrys.get(id); - if (entryModule) { - return entryModule.cmps.map(createComponentExport).join('\n'); - } - return null; - }, - }; - - return plugin; -}; - -const createComponentExport = (cmp: d.ComponentCompilerMeta): string => { - const originalClassName = cmp.componentClassName; - const underscoredClassName = cmp.tagName.replace(/-/g, '_'); - const filePath = normalizePath(cmp.sourceFilePath); - return `export { ${originalClassName} as ${underscoredClassName} } from '${filePath}';`; -}; diff --git a/src/compiler/output-targets/dist-lazy/lazy-output.ts b/src/compiler/output-targets/dist-lazy/lazy-output.ts deleted file mode 100644 index 46a0b32961c..00000000000 --- a/src/compiler/output-targets/dist-lazy/lazy-output.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { catchError, isOutputTargetDist, isOutputTargetDistLazy, sortBy } from '@utils'; -import MagicString from 'magic-string'; -import * as ts from 'typescript'; - -import type * as d from '../../../declarations'; -import type { BundleOptions } from '../../bundle/bundle-interface'; -import { bundleOutput } from '../../bundle/bundle-output'; -import { - LAZY_BROWSER_ENTRY_ID, - LAZY_EXTERNAL_ENTRY_ID, - STENCIL_APP_GLOBALS_ID, - STENCIL_CORE_ID, - STENCIL_INTERNAL_CLIENT_PATCH_BROWSER_ID, - USER_INDEX_ENTRY_ID, -} from '../../bundle/entry-alias-ids'; -import { generateComponentBundles } from '../../entries/component-bundles'; -import { generateModuleGraph } from '../../entries/component-graph'; -import { lazyComponentTransform } from '../../transformers/component-lazy/transform-lazy-component'; -import { removeCollectionImports } from '../../transformers/remove-collection-imports'; -import { rewriteAliasedSourceFileImportPaths } from '../../transformers/rewrite-aliased-paths'; -import { updateStencilCoreImports } from '../../transformers/update-stencil-core-import'; -import { generateCjs } from './generate-cjs'; -import { generateEsm } from './generate-esm'; -import { generateEsmBrowser } from './generate-esm-browser'; -import { generateSystem } from './generate-system'; -import { getLazyBuildConditionals } from './lazy-build-conditionals'; -import { addTagTransform } from '../../transformers/add-tag-transform'; - -export const outputLazy = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -): Promise => { - const outputTargets = config.outputTargets.filter(isOutputTargetDistLazy); - if (outputTargets.length === 0) { - return; - } - - const bundleEventMessage = `generate lazy${config.sourceMap ? ' + source maps' : ''}`; - const timespan = buildCtx.createTimeSpan(`${bundleEventMessage} started`); - - try { - const bundleOpts: BundleOptions = { - id: 'lazy', - platform: 'client', - conditionals: getLazyBuildConditionals(config, buildCtx.components), - customBeforeTransformers: getCustomBeforeTransformers(config, compilerCtx, buildCtx), - inlineWorkers: config.outputTargets.some(isOutputTargetDist), - inputs: { - [config.fsNamespace]: LAZY_BROWSER_ENTRY_ID, - loader: LAZY_EXTERNAL_ENTRY_ID, - index: USER_INDEX_ENTRY_ID, - }, - loader: { - [LAZY_EXTERNAL_ENTRY_ID]: getLazyEntry(false), - [LAZY_BROWSER_ENTRY_ID]: getLazyEntry(true), - }, - }; - - // we've got the compiler context filled with app modules and collection dependency modules - // figure out how all these components should be connected - generateEntryModules(config, buildCtx); - buildCtx.entryModules.forEach((entryModule) => { - bundleOpts.inputs[entryModule.entryKey] = entryModule.entryKey; - }); - - const rollupBuild = await bundleOutput(config, compilerCtx, buildCtx, bundleOpts); - if (rollupBuild != null) { - const results: d.UpdatedLazyBuildCtx[] = await Promise.all([ - generateEsmBrowser(config, compilerCtx, buildCtx, rollupBuild, outputTargets), - generateEsm(config, compilerCtx, buildCtx, rollupBuild, outputTargets), - generateSystem(config, compilerCtx, buildCtx, rollupBuild, outputTargets), - generateCjs(config, compilerCtx, buildCtx, rollupBuild, outputTargets), - ]); - - results.forEach((result) => { - if (result.name === 'cjs') { - buildCtx.commonJsComponentBundle = result.buildCtx.commonJsComponentBundle; - } else if (result.name === 'system') { - buildCtx.systemComponentBundle = result.buildCtx.systemComponentBundle; - } else if (result.name === 'esm') { - buildCtx.esmComponentBundle = result.buildCtx.esmComponentBundle; - buildCtx.es5ComponentBundle = result.buildCtx.es5ComponentBundle; - } else if (result.name === 'esm-browser') { - buildCtx.esmBrowserComponentBundle = result.buildCtx.esmBrowserComponentBundle; - buildCtx.buildResults = result.buildCtx.buildResults; - buildCtx.components = result.buildCtx.components; - } - }); - - if (buildCtx.esmBrowserComponentBundle != null) { - buildCtx.componentGraph = generateModuleGraph(buildCtx.components, buildCtx.esmBrowserComponentBundle); - } - } - } catch (e: any) { - catchError(buildCtx.diagnostics, e); - } - - timespan.finish(`${bundleEventMessage} finished`); -}; - -/** - * Generate a collection of transformations that are to be applied as a part of the `before` step in the TypeScript - * compilation process. - # - * @param config the Stencil configuration associated with the current build - * @param compilerCtx the current compiler context - * @returns a collection of transformations that should be applied to the source code, intended for the `before` part - * of the pipeline - */ -const getCustomBeforeTransformers = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx?: d.BuildCtx, -): ts.TransformerFactory[] => { - const transformOpts: d.TransformOptions = { - coreImportPath: STENCIL_CORE_ID, - componentExport: 'lazy', - componentMetadata: null, - currentDirectory: config.sys.getCurrentDirectory(), - proxy: null, - style: 'static', - styleImportData: 'queryparams', - }; - const customBeforeTransformers = [updateStencilCoreImports(transformOpts.coreImportPath)]; - - if (config.transformAliasedImportPaths) { - customBeforeTransformers.push(rewriteAliasedSourceFileImportPaths); - } - - if (buildCtx.config.extras.additionalTagTransformers) { - customBeforeTransformers.push(addTagTransform(compilerCtx, buildCtx)); - } - - customBeforeTransformers.push( - lazyComponentTransform(compilerCtx, transformOpts, buildCtx), - removeCollectionImports(compilerCtx), - ); - return customBeforeTransformers; -}; - -/** - * Generate entry modules to be used by the build process by determining how - * modules and components are connected - * - * **Note**: this function mutates the {@link d.BuildCtx} object that is - * passed in to it, assigning the generated entry modules to the `entryModules` - * property - * - * @param config the Stencil configuration file that was provided as a part of the build step - * @param buildCtx the current build context - */ -function generateEntryModules(config: d.ValidatedConfig, buildCtx: d.BuildCtx): void { - // figure out how modules and components connect - try { - const bundles = generateComponentBundles(config, buildCtx); - buildCtx.entryModules = bundles.map(createEntryModule); - } catch (e: any) { - catchError(buildCtx.diagnostics, e); - } - - buildCtx.debug(`generateEntryModules, ${buildCtx.entryModules.length} entryModules`); -} - -/** - * Generates an entry module to be used during the bundling process - * @param cmps the component metadata to create a single entry module from - * @returns the entry module generated - */ -function createEntryModule(cmps: d.ComponentCompilerMeta[]): d.EntryModule { - // generate a unique entry key based on the components within this entry module - cmps = sortBy(cmps, (c) => c.tagName); - const entryKey = cmps.map((c) => c.tagName).join('.') + '.entry'; - - return { - cmps, - entryKey, - }; -} - -const getLazyEntry = (isBrowser: boolean): string => { - const s = new MagicString(``); - s.append(`export { setNonce } from '${STENCIL_CORE_ID}';\n`); - s.append(`import { bootstrapLazy } from '${STENCIL_CORE_ID}';\n`); - - if (isBrowser) { - s.append(`import { patchBrowser } from '${STENCIL_INTERNAL_CLIENT_PATCH_BROWSER_ID}';\n`); - s.append(`import { globalScripts } from '${STENCIL_APP_GLOBALS_ID}';\n`); - s.append(`patchBrowser().then(async (options) => {\n`); - s.append(` await globalScripts();\n`); - s.append(` return bootstrapLazy([/*!__STENCIL_LAZY_DATA__*/], options);\n`); - s.append(`});\n`); - } else { - s.append(`import { globalScripts } from '${STENCIL_APP_GLOBALS_ID}';\n`); - s.append(`export const defineCustomElements = async (win, options) => {\n`); - s.append(` if (typeof window === 'undefined') return undefined;\n`); - s.append(` await globalScripts();\n`); - s.append(` return bootstrapLazy([/*!__STENCIL_LAZY_DATA__*/], options);\n`); - s.append(`};\n`); - } - - return s.toString(); -}; diff --git a/src/compiler/output-targets/dist-lazy/write-lazy-entry-module.ts b/src/compiler/output-targets/dist-lazy/write-lazy-entry-module.ts deleted file mode 100644 index 46864eca74c..00000000000 --- a/src/compiler/output-targets/dist-lazy/write-lazy-entry-module.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { getSourceMappingUrlForEndOfFile, join } from '@utils'; - -import type * as d from '../../../declarations'; - -export const writeLazyModule = async ( - compilerCtx: d.CompilerCtx, - outputTargetType: string, - destinations: string[], - code: string, - sourceMap: d.SourceMap, - rollupResult?: d.RollupChunkResult, -): Promise => { - // code = replaceStylePlaceholders(entryModule.cmps, modeName, code); - - const fileName = rollupResult.fileName; - const bundleId = fileName.replace('.entry.js', ''); - - if (sourceMap) { - code = code + getSourceMappingUrlForEndOfFile(fileName); - } - - await Promise.all( - destinations.map((dst) => { - const jsPath = join(dst, fileName); - const mapPath = jsPath + '.map'; - const writes: Promise[] = [compilerCtx.fs.writeFile(jsPath, code, { outputTargetType })]; - if (!!sourceMap) { - writes.push(compilerCtx.fs.writeFile(mapPath, JSON.stringify(sourceMap), { outputTargetType })); - } - return Promise.all(writes); - }), - ); - - return { - bundleId, - fileName, - code, - }; -}; diff --git a/src/compiler/output-targets/empty-dir.ts b/src/compiler/output-targets/empty-dir.ts deleted file mode 100644 index 25a5da21f4f..00000000000 --- a/src/compiler/output-targets/empty-dir.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - isOutputTargetDist, - isOutputTargetDistCustomElements, - isOutputTargetDistLazy, - isOutputTargetDistLazyLoader, - isOutputTargetHydrate, - isOutputTargetWww, - isString, -} from '@utils'; - -import type * as d from '../../declarations'; - -type OutputTargetEmptiable = - | d.OutputTargetDist - | d.OutputTargetWww - | d.OutputTargetDistLazyLoader - | d.OutputTargetHydrate; - -const isEmptable = (o: d.OutputTarget): o is OutputTargetEmptiable => - isOutputTargetDist(o) || - isOutputTargetDistCustomElements(o) || - isOutputTargetWww(o) || - isOutputTargetDistLazy(o) || - isOutputTargetDistLazyLoader(o) || - isOutputTargetHydrate(o); - -export const emptyOutputTargets = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -) => { - if (buildCtx.isRebuild) { - return; - } - const cleanDirs = config.outputTargets - .filter(isEmptable) - .filter((o) => o.empty === true) - .map((o) => o.dir || (o as any).esmDir) - .filter(isString); - - if (cleanDirs.length === 0) { - return; - } - - const timeSpan = buildCtx.createTimeSpan(`cleaning ${cleanDirs.length} dirs`, true); - await compilerCtx.fs.emptyDirs(cleanDirs); - - timeSpan.finish('cleaning dirs finished'); -}; diff --git a/src/compiler/output-targets/index.ts b/src/compiler/output-targets/index.ts deleted file mode 100644 index 2ef5aa5b622..00000000000 --- a/src/compiler/output-targets/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { RollupCache } from 'rollup'; - -import type * as d from '../../declarations'; -import { outputCopy } from './copy/output-copy'; -import { outputCollection } from './dist-collection'; -import { outputCustomElements } from './dist-custom-elements'; -import { outputHydrateScript } from './dist-hydrate-script'; -import { outputLazy } from './dist-lazy/lazy-output'; -import { outputCustom } from './output-custom'; -import { outputDocs } from './output-docs'; -import { outputLazyLoader } from './output-lazy-loader'; -import { outputTypes } from './output-types'; -import { outputWww } from './output-www'; - -export const generateOutputTargets = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -) => { - const timeSpan = buildCtx.createTimeSpan('generate outputs started', true); - - const changedModuleFiles = Array.from(compilerCtx.changedModules) - .map((filename) => compilerCtx.moduleMap.get(filename)) - .filter((mod) => mod && !mod.isCollectionDependency); - - compilerCtx.changedModules.clear(); - - invalidateRollupCaches(compilerCtx); - - await Promise.all([ - outputCollection(config, compilerCtx, buildCtx, changedModuleFiles), - outputCustomElements(config, compilerCtx, buildCtx), - outputHydrateScript(config, compilerCtx, buildCtx), - outputLazyLoader(config, compilerCtx), - outputLazy(config, compilerCtx, buildCtx), - ]); - - await Promise.all([ - // the user may want to copy compiled assets which requires above tasks to - // have finished first - outputCopy(config, compilerCtx, buildCtx), - - // the www output target depends on the output of the lazy output target - // since it attempts to inline the lazy build entry point into `index.html` - // so we want to ensure that the lazy OT has already completed and written - // all of its files before the www OT runs. - outputWww(config, compilerCtx, buildCtx), - - // must run after all the other outputs - // since it validates files were created - outputDocs(config, compilerCtx, buildCtx), - outputTypes(config, compilerCtx, buildCtx), - outputCustom(config, compilerCtx, buildCtx), - ]); - - timeSpan.finish('generate outputs finished'); -}; - -const invalidateRollupCaches = (compilerCtx: d.CompilerCtx) => { - const invalidatedIds = compilerCtx.changedFiles; - compilerCtx.rollupCache.forEach((cache: RollupCache) => { - cache.modules.forEach((mod) => { - if (mod.transformDependencies.some((id) => invalidatedIds.has(id))) { - mod.originalCode = null; - } - }); - }); -}; diff --git a/src/compiler/output-targets/output-custom.ts b/src/compiler/output-targets/output-custom.ts deleted file mode 100644 index a7a83aab57a..00000000000 --- a/src/compiler/output-targets/output-custom.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { catchError, isOutputTargetCustom } from '@utils'; - -import type * as d from '../../declarations'; -import { generateDocData } from '../docs/generate-doc-data'; - -export const outputCustom = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - if (config._isTesting) { - return; - } - - const task = config.watch ? 'always' : 'onBuildOnly'; - const customOutputTargets = config.outputTargets - .filter(isOutputTargetCustom) - .filter((o) => (o.taskShouldRun === undefined ? true : o.taskShouldRun === task)); - - if (customOutputTargets.length === 0) { - return; - } - const docsData = await generateDocData(config, compilerCtx, buildCtx); - - await Promise.all( - customOutputTargets.map(async (o) => { - const timespan = buildCtx.createTimeSpan(`generating ${o.name} started`); - try { - await o.generator(config, compilerCtx, buildCtx, docsData); - } catch (e: any) { - catchError(buildCtx.diagnostics, e); - } - timespan.finish(`generate ${o.name} finished`); - }), - ); -}; diff --git a/src/compiler/output-targets/output-docs.ts b/src/compiler/output-targets/output-docs.ts deleted file mode 100644 index c03b9bd531d..00000000000 --- a/src/compiler/output-targets/output-docs.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - isOutputTargetDocsCustom, - isOutputTargetDocsCustomElementsManifest, - isOutputTargetDocsJson, - isOutputTargetDocsReadme, - isOutputTargetDocsVscode, - join, - normalizePath, -} from '@utils'; - -import type * as d from '../../declarations'; -import { generateCustomDocs } from '../docs/custom'; -import { generateCustomElementsManifestDocs } from '../docs/cem'; -import { generateDocData } from '../docs/generate-doc-data'; -import { generateJsonDocs } from '../docs/json'; -import { generateReadmeDocs } from '../docs/readme'; -import { extractExistingCssProps } from '../docs/readme/output-docs'; -import { generateVscodeDocs } from '../docs/vscode'; - -/** - * Generate documentation-related output targets - * @param config the configuration associated with the current Stencil task run - * @param compilerCtx the current compiler context - * @param buildCtx the build context for the current Stencil task run - */ -export const outputDocs = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -): Promise => { - if (!config.buildDocs) { - return; - } - const docsOutputTargets = config.outputTargets.filter( - (o) => - isOutputTargetDocsReadme(o) || - isOutputTargetDocsJson(o) || - isOutputTargetDocsCustom(o) || - isOutputTargetDocsVscode(o) || - isOutputTargetDocsCustomElementsManifest(o), - ); - - if (docsOutputTargets.length === 0) { - return; - } - - // ensure all the styles are built first, which parses all the css docs - await buildCtx.stylesPromise; - - const docsData = await generateDocData(config, compilerCtx, buildCtx); - - // If we're in docs-only mode (not a full build), preserve CSS Custom Properties - // from existing README files for components with empty styles. - // We detect docs-only mode by checking if ALL output targets are docs targets. - const isDocsOnlyMode = config.outputTargets.every( - (target) => - target.type === 'docs-readme' || - target.type === 'docs-json' || - target.type === 'docs-custom' || - target.type === 'docs-vscode' || - target.type === 'docs-custom-elements-manifest', - ); - - if (isDocsOnlyMode) { - // Preserve CSS props for components with empty styles - await Promise.all( - docsData.components.map(async (component) => { - if (component.styles.length === 0) { - // Find the README output target to get the correct path - const readmeTarget = docsOutputTargets.find(isOutputTargetDocsReadme) as d.OutputTargetDocsReadme | undefined; - const readmeDir = readmeTarget?.dir || config.srcDir; - const readmePath = - normalizePath(readmeDir) === normalizePath(config.srcDir) - ? component.readmePath - : join(readmeDir, component.readmePath.replace(config.srcDir, '')); - - const existingCssProps = await extractExistingCssProps(compilerCtx, readmePath); - if (existingCssProps) { - // Update component styles with preserved props - component.styles = existingCssProps; - } - } - }), - ); - } - - await Promise.all([ - generateReadmeDocs(config, compilerCtx, docsData, docsOutputTargets), - generateJsonDocs(config, compilerCtx, docsData, docsOutputTargets), - generateVscodeDocs(compilerCtx, docsData, docsOutputTargets), - generateCustomDocs(config, docsData, docsOutputTargets), - generateCustomElementsManifestDocs(compilerCtx, docsData, docsOutputTargets), - ]); -}; diff --git a/src/compiler/output-targets/output-lazy-loader.ts b/src/compiler/output-targets/output-lazy-loader.ts deleted file mode 100644 index 208600d18a3..00000000000 --- a/src/compiler/output-targets/output-lazy-loader.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { generatePreamble, isOutputTargetDistLazyLoader, join, relative, relativeImport } from '@utils'; - -import type * as d from '../../declarations'; -import { getClientPolyfill } from '../app-core/app-polyfills'; - -export const outputLazyLoader = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx) => { - const outputTargets = config.outputTargets.filter(isOutputTargetDistLazyLoader); - if (outputTargets.length === 0) { - return; - } - - await Promise.all(outputTargets.map((o) => generateLoader(config, compilerCtx, o))); -}; - -const generateLoader = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - outputTarget: d.OutputTargetDistLazyLoader, -) => { - const loaderPath = outputTarget.dir; - const es2017Dir = outputTarget.esmDir; - const es5Dir = outputTarget.esmEs5Dir || es2017Dir; - const cjsDir = outputTarget.cjsDir; - - if (!loaderPath || !es2017Dir || !cjsDir) { - return; - } - - const es5HtmlElement = await getClientPolyfill(config, compilerCtx, 'es5-html-element.js'); - const polyfillsEntryPoint = join(es2017Dir, 'polyfills/index.js'); - const polyfillsExport = `export * from '${relative(loaderPath, polyfillsEntryPoint)}';`; - - const es5EntryPoint = join(es5Dir, 'loader.js'); - const indexContent = filterAndJoin([ - generatePreamble(config), - es5HtmlElement, - config.buildEs5 ? polyfillsExport : null, - `export * from '${relative(loaderPath, es5EntryPoint)}';`, - ]); - - const es2017EntryPoint = join(es2017Dir, 'loader.js'); - const indexES2017Content = filterAndJoin([ - generatePreamble(config), - config.buildEs5 ? polyfillsExport : null, - `export * from '${relative(loaderPath, es2017EntryPoint)}';`, - ]); - - const cjsEntryPoint = join(cjsDir, 'loader.cjs.js'); - const indexCjsContent = filterAndJoin([ - generatePreamble(config), - `module.exports = require('${relative(loaderPath, cjsEntryPoint)}');`, - config.buildEs5 ? `module.exports.applyPolyfills = function() { return Promise.resolve() };` : null, - ]); - - const indexDtsPath = join(loaderPath, 'index.d.ts'); - - await Promise.all([ - compilerCtx.fs.writeFile(join(loaderPath, 'index.d.ts'), generateIndexDts(indexDtsPath, outputTarget.componentDts)), - compilerCtx.fs.writeFile(join(loaderPath, 'index.js'), indexContent), - compilerCtx.fs.writeFile(join(loaderPath, 'index.cjs.js'), indexCjsContent), - compilerCtx.fs.writeFile(join(loaderPath, 'cdn.js'), indexCjsContent), - compilerCtx.fs.writeFile(join(loaderPath, 'index.es2017.js'), indexES2017Content), - ]); -}; - -const generateIndexDts = (indexDtsPath: string, componentsDtsPath: string) => { - return `export * from '${relativeImport(indexDtsPath, componentsDtsPath, '.d.ts')}'; -export interface CustomElementsDefineOptions { - exclude?: string[]; - resourcesUrl?: string; - syncQueue?: boolean; - jmp?: (c: Function) => any; - raf?: (c: FrameRequestCallback) => number; - ael?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void; - rel?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void; -} -export declare function defineCustomElements(win?: Window, opts?: CustomElementsDefineOptions): void; -/** - * @deprecated - */ -export declare function applyPolyfills(): Promise; - -/** - * Used to specify a nonce value that corresponds with an application's CSP. - * When set, the nonce will be added to all dynamically created script and style tags at runtime. - * Alternatively, the nonce value can be set on a meta tag in the DOM head - * () which - * will result in the same behavior. - */ -export declare function setNonce(nonce: string): void; -`; -}; - -/** - * Given an array of 'parts' which can be assembled into a string 1) filter - * out any parts that are `null` and 2) join the remaining strings into a single - * output string - * - * @param parts an array of parts to filter and join - * @returns the joined string - */ -function filterAndJoin(parts: (string | null)[]): string { - return parts - .filter((part) => part !== null) - .join('\n') - .trim(); -} diff --git a/src/compiler/output-targets/output-service-workers.ts b/src/compiler/output-targets/output-service-workers.ts deleted file mode 100644 index 619fb138ec2..00000000000 --- a/src/compiler/output-targets/output-service-workers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { isOutputTargetWww } from '@utils'; - -import type * as d from '../../declarations'; -import { generateServiceWorker } from '../service-worker/generate-sw'; - -/** - * Entrypoint to creating a service worker for every `www` output target - * @param config the Stencil configuration used for the build - * @param buildCtx the build context associated with the build to mark as done - */ -export const outputServiceWorkers = async (config: d.ValidatedConfig, buildCtx: d.BuildCtx): Promise => { - const wwwServiceOutputs = config.outputTargets - .filter(isOutputTargetWww) - .filter((o) => typeof o.indexHtml === 'string' && !!o.serviceWorker); - - if (wwwServiceOutputs.length === 0 || config.sys.lazyRequire == null) { - return; - } - - // let's make sure they have what we need from workbox installed - const diagnostics = await config.sys.lazyRequire.ensure(config.rootDir, ['workbox-build']); - if (diagnostics.length > 0) { - buildCtx.diagnostics.push(...diagnostics); - } else { - // we've ensured workbox is installed, so let's require it now - const workbox: d.Workbox = config.sys.lazyRequire.require(config.rootDir, 'workbox-build'); - - await Promise.all( - wwwServiceOutputs.map((outputTarget) => generateServiceWorker(config, buildCtx, workbox, outputTarget)), - ); - } -}; diff --git a/src/compiler/output-targets/output-types.ts b/src/compiler/output-targets/output-types.ts deleted file mode 100644 index 49a5697a702..00000000000 --- a/src/compiler/output-targets/output-types.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { isOutputTargetDistTypes } from '@utils'; - -import type * as d from '../../declarations'; -import { generateTypes } from '../types/generate-types'; - -/** - * Entrypoint for generating types for all output targets - * @param config the Stencil configuration associated with the project being compiled - * @param compilerCtx the current compiler context - * @param buildCtx the context associated with the current build - */ -export const outputTypes = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -): Promise => { - const outputTargets = config.outputTargets.filter(isOutputTargetDistTypes); - if (outputTargets.length === 0) { - return; - } - - const timespan = buildCtx.createTimeSpan(`generate types started`, true); - - await Promise.all(outputTargets.map((outputsTarget) => generateTypes(config, compilerCtx, buildCtx, outputsTarget))); - - timespan.finish(`generate types finished`); -}; diff --git a/src/compiler/output-targets/output-www.ts b/src/compiler/output-targets/output-www.ts deleted file mode 100644 index 424d1a8dd74..00000000000 --- a/src/compiler/output-targets/output-www.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { cloneDocument, serializeNodeToHtml } from '@stencil/core/mock-doc'; -import { catchError, flatOne, isOutputTargetWww, join, relative, unique } from '@utils'; - -import type * as d from '../../declarations'; -import { generateEs5DisabledMessage } from '../app-core/app-es5-disabled'; -import { addScriptDataAttribute } from '../html/add-script-attr'; -import { getAbsoluteBuildDir } from '../html/html-utils'; -import { optimizeCriticalPath } from '../html/inject-module-preloads'; -import { updateIndexHtmlServiceWorker } from '../html/inject-sw-script'; -import { optimizeEsmImport } from '../html/inline-esm-import'; -import { inlineStyleSheets } from '../html/inline-style-sheets'; -import { updateGlobalStylesLink } from '../html/update-global-styles-link'; -import { getUsedComponents } from '../html/used-components'; -import { generateHashedCopy } from '../output-targets/copy/hashed-copy'; -import { INDEX_ORG } from '../service-worker/generate-sw'; -import { getScopeId } from '../style/scope-css'; - -/** - * Run a {@link d.OutputTargetWww} build. This involves generating `index.html` - * for the build which imports the output of the lazy build and also generating - * a host configuration record. - * - * @param config the current user-supplied config - * @param compilerCtx a compiler context - * @param buildCtx a build context - */ -export const outputWww = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -): Promise => { - const outputTargets = config.outputTargets.filter(isOutputTargetWww); - if (outputTargets.length === 0) { - return; - } - - const timespan = buildCtx.createTimeSpan(`generate www started`, true); - const criticalBundles = getCriticalPath(buildCtx); - - await Promise.all( - outputTargets.map((outputTarget) => generateWww(config, compilerCtx, buildCtx, criticalBundles, outputTarget)), - ); - - timespan.finish(`generate www finished`); -}; - -/** - * Derive the 'critical path' for our HTML content, which is a list of the - * bundles that it will need to render correctly. - * - * @param buildCtx the current build context - * @returns a list of bundles that need to be pulled in - */ -const getCriticalPath = (buildCtx: d.BuildCtx) => { - const componentGraph = buildCtx.componentGraph; - if (!buildCtx.indexDoc || !componentGraph) { - return []; - } - return unique( - flatOne( - getUsedComponents(buildCtx.indexDoc, buildCtx.components) - .map((tagName) => getScopeId(tagName)) - .map((scopeId) => buildCtx.componentGraph.get(scopeId) || []), - ), - ).sort(); -}; - -/** - * Process a single www output target, generating an `index.html` file and a - * host config (and writing both to disk) - * - * @param config the current user-supplied config - * @param compilerCtx a compiler context - * @param buildCtx a build context - * @param criticalPath a list of critical bundles - * @param outputTarget the www output target of interest - */ -const generateWww = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - criticalPath: string[], - outputTarget: d.OutputTargetWww, -): Promise => { - if (!config.buildEs5) { - await generateEs5DisabledMessage(config, compilerCtx, outputTarget); - } - - // Copy global styles into the build directory - // Process - if (buildCtx.indexDoc && outputTarget.indexHtml) { - await generateIndexHtml(config, compilerCtx, buildCtx, criticalPath, outputTarget); - } - await generateHostConfig(compilerCtx, outputTarget); -}; - -/** - * Generate a host configuration for a given www OT and write it to disk - * - * @param compilerCtx a compiler context - * @param outputTarget a www OT - * @returns a promise wrapping fs write results - */ -const generateHostConfig = (compilerCtx: d.CompilerCtx, outputTarget: d.OutputTargetWww) => { - const buildDir = getAbsoluteBuildDir(outputTarget); - const hostConfigPath = join(outputTarget.appDir, 'host.config.json'); - const hostConfigContent = JSON.stringify( - { - hosting: { - headers: [ - { - source: join(buildDir, '/p-*'), - headers: [ - { - key: 'Cache-Control', - value: 'max-age=31556952, s-maxage=31556952, immutable', - }, - ], - }, - ], - }, - }, - null, - ' ', - ); - - return compilerCtx.fs.writeFile(hostConfigPath, hostConfigContent, { outputTargetType: outputTarget.type }); -}; - -/** - * Attempt to generate `index.html` content for a www output target and, if all - * goes well, write it to disk. As part of creating the content several - * optimizations (mainly inlining content and adding module preloads) are - * attempted. - * - * @param config the current user-supplied Stencil configuration - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param criticalPath a list of bundles for which we should add module preloads - * @param outputTarget the www output target of interest - */ -const generateIndexHtml = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - criticalPath: string[], - outputTarget: d.OutputTargetWww, -) => { - if (compilerCtx.hasSuccessfulBuild && !buildCtx.hasHtmlChanges) { - // no need to rebuild index.html if there were no app file changes - return; - } - - // get the source index html content - try { - const doc = cloneDocument(buildCtx.indexDoc); - addScriptDataAttribute(config, doc, outputTarget); - - // validateHtml(config, buildCtx, doc); - await updateIndexHtmlServiceWorker(config, buildCtx, doc, outputTarget); - if (!config.watch && !config.devMode) { - const globalStylesFilename = await generateHashedCopy( - config, - compilerCtx, - join(outputTarget.buildDir, `${config.fsNamespace}.css`), - ); - const scriptFound = await optimizeEsmImport(config, compilerCtx, doc, outputTarget); - await inlineStyleSheets(compilerCtx, doc, MAX_CSS_INLINE_SIZE, outputTarget); - updateGlobalStylesLink(config, doc, globalStylesFilename, outputTarget); - if (scriptFound) { - optimizeCriticalPath(doc, criticalPath, outputTarget); - } - } - - const indexContent = serializeNodeToHtml(doc); - await compilerCtx.fs.writeFile(outputTarget.indexHtml, indexContent, { outputTargetType: outputTarget.type }); - - if (outputTarget.serviceWorker && config.flags.prerender) { - await compilerCtx.fs.writeFile(join(outputTarget.appDir, INDEX_ORG), indexContent, { - outputTargetType: outputTarget.type, - }); - } - - buildCtx.debug(`generateIndexHtml, write: ${relative(config.rootDir, outputTarget.indexHtml)}`); - } catch (e: any) { - catchError(buildCtx.diagnostics, e); - } -}; - -const MAX_CSS_INLINE_SIZE = 3 * 1024; diff --git a/src/compiler/output-targets/readme.md b/src/compiler/output-targets/readme.md deleted file mode 100644 index 5d2a0abe32a..00000000000 --- a/src/compiler/output-targets/readme.md +++ /dev/null @@ -1,115 +0,0 @@ -# Output Targets - -Stencil is able to generate components into various formats so they can be best integrated into the many different apps types, no matter what framework or bundler is used. - - -## Output Target Terms - -`script`: A prebuilt, stand-alone webapp already built from the components. These are already built to be loaded by just a script tag, no additional builds or bundling required. Both the `www` and `dist` output target types save an "app" into their directories. When saving the webapp into the `dist/` directory, it can be easily packaged up and used with a service like `unpkg.com`. See https://www.npmjs.com/package/@ionic/core - -`collection`: Source files transpiled down to simple JavaScript, and all component metadata placed on the component class as static getters. When one Stencil distribution imports another, it will use these files when generating its own distribution. What's important is that the source code of a `collection` is future proof, meaning no matter what version of Stencil it can import and understand the component metadata. - -`host`: The actual "host" element sitting in the webpage's DOM. - -`lazy-loaded`: A lazy-loaded webapp creates all the proxied host custom elements up front, but only downloads the component implementation on-demand. Lazy-loaded components work by having a proxied "host" custom element, and lazy-loads the component class and css, and rather than the host element having the "instance", such as a traditional custom element, the instance is of the lazy-loaded component class. If a Stencil library has a low number of components, then having them all packaged into a single-file would be best. But for a very large library of components, such as Ionic, it'd be best to have them lazy-loaded instead. Part of the configuration can decide when to make a library either lazy-loaded or single-file. - -`module`: Component code meant to be imported by other bundlers in order for them to be integrated within other apps. - -`native`: Lazy-loaded components split the host custom element and the component implementation apart. A "native" component is a traditional custom element in that the instance and host element are the same. - -`custom-element`: Individual custom elements packaged up into stand-alone, self-contained code. Each component imports shared runtime from `@stencil/core`. Opposite of lazy-loaded components that define themselves and load on demand, the custom elements builds must be imported and defined by the consumer, and any lazy-loaded depends on the consumer's bundling methods. - - -## Output Target Types - -### `www` - -- Default output target when not configured. -- Generates a stand-alone `app` into the `www/` directory. -- Depending on the number of components and configuration, the app may be lazy-loaded of single-file. - - -### `dist` - -- Generates `modules` to be imported by other bundlers, such as `dist/esm/` and `dist/esm-es5/` (when enabling buildEs5 config). -- Generates an `app` at the root of the `dist/` directory. It's the same stand-alone webapp as the `www` type, but located in dist so it's easy to package up and shared. -- Generates a `collection` into the `dist/collection/` directory to be used by other projects. - - -### `angular` - -- Generates a wrapper Angular component proxy. -- Web components themselves work fine within Angular, but you loose out on many of Angular's features, such as types or `@ViewChild`. In order for a Stencil project to fit right into the Angular ecosystem, this output target generates thin wrapper that can be imported by Angular. - - -### `dist-hydrate-script` - -- Used by NodeJS to do Static Site Generation (SSG) and/or Server Side Rendering (SSR). -- Used by Stencil prerendering commands. -- Formats the components so that the server can generate new global window environments that are scoped to each rendering, rather than having global information bleed between each URL rendered. - - -## Output Folder Structure Defaults - -``` -- dist/ - - - cjs/ (bundler ready, cjs modules) - - index.cjs.js - - loader.cjs.js - - - collection/ (metadata when this is lazy-loaded dependency) - - my-cmp/ - - my-cmp.js (esm) - - my-cmp.css - - collection-manifest.json - - global.js - - - custom-elements (bundler ready custom elements, esm only) - - index.js (esm) - - index.d.ts - - - esm (bundler ready, esm modules, es2017 source) - - index.js - - loader.js - - - esm-es5 (buildEs5, bundler ready, esm modules, es5 source) - - index.js - - loader.js - - - loader (bundler entry for lazy builds) - - cdn.js - - index.js - - index.cjs.js - - index.d.ts - - index.es2017.js - - package.json (to import loader package, such as myapp/loader) - - - myapp (browser ready script, named from stencil config namespace) - - myapp.css - - myapp.esm.js - - myapp.js (buildEs5 entry, systemjs modules, es5 source) - - myapp.system.js (buildEs5, systemjs modules, es5 source) - - - types (dts files for each component) - - my-cmp/ - -my-cmp.d.ts - - - index.cjs.js (dist cjs entry) - - index.js (dist esm entry) - -- hydrate - - index.js (NodeJS ready hydrate script, cjs module) - - index.d.ts (types for hydrate API) - - package.json (to import hydrate package, such as myapp/hydrate) - -- www/ (www output target) - - build/ - - myapp.esm.js (browser ready esm modern script) - - myapp.js (buildEs5, browser ready systemjs modules, es5 script) - - - index.html (optimized html from src/index.html) - -- package.json (top-level package.json is not auto-updated) -- stencil.config.ts -``` \ No newline at end of file diff --git a/src/compiler/output-targets/test/build-conditionals.spec.ts b/src/compiler/output-targets/test/build-conditionals.spec.ts deleted file mode 100644 index df54976bf11..00000000000 --- a/src/compiler/output-targets/test/build-conditionals.spec.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; - -import type * as d from '../../../declarations'; -import { validateConfig } from '../../config/validate-config'; -import { getCustomElementsBuildConditionals } from '../dist-custom-elements/custom-elements-build-conditionals'; -import { getHydrateBuildConditionals } from '../dist-hydrate-script/hydrate-build-conditionals'; -import { getLazyBuildConditionals } from '../dist-lazy/lazy-build-conditionals'; - -describe('build-conditionals', () => { - let userConfig: d.Config; - let cmps: d.ComponentCompilerMeta[]; - - beforeEach(() => { - userConfig = mockConfig(); - cmps = []; - }); - - describe('getCustomElementsBuildConditionals', () => { - it('default', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getCustomElementsBuildConditionals(config, cmps); - expect(bc).toMatchObject({ - lazyLoad: false, - hydrateClientSide: false, - hydrateServerSide: false, - }); - }); - - it('taskQueue async', () => { - userConfig.taskQueue = 'async'; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getCustomElementsBuildConditionals(config, cmps); - expect(bc.asyncQueue).toBe(false); - expect(bc.taskQueue).toBe(true); - expect(config.taskQueue).toBe('async'); - }); - - it('taskQueue immediate', () => { - userConfig.taskQueue = 'immediate'; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getCustomElementsBuildConditionals(config, cmps); - expect(bc.asyncQueue).toBe(false); - expect(bc.taskQueue).toBe(false); - expect(config.taskQueue).toBe('immediate'); - }); - - it('taskQueue congestionAsync', () => { - userConfig.taskQueue = 'congestionAsync'; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getCustomElementsBuildConditionals(config, cmps); - expect(bc.asyncQueue).toBe(true); - expect(bc.taskQueue).toBe(true); - expect(config.taskQueue).toBe('congestionAsync'); - }); - - it('taskQueue defaults', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getCustomElementsBuildConditionals(config, cmps); - expect(bc.asyncQueue).toBe(false); - expect(bc.taskQueue).toBe(true); - expect(config.taskQueue).toBe('async'); - }); - - it('hydrateClientSide true', () => { - const hydrateOutputTarget: d.OutputTargetHydrate = { - type: 'dist-hydrate-script', - }; - userConfig.outputTargets = [hydrateOutputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getCustomElementsBuildConditionals(config, cmps); - expect(bc.hydrateClientSide).toBe(true); - }); - - it('hydratedSelectorName', () => { - userConfig.hydratedFlag = { - name: 'boooop', - }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getCustomElementsBuildConditionals(config, cmps); - expect(bc.hydratedSelectorName).toBe('boooop'); - }); - }); - - describe('getLazyBuildConditionals', () => { - it('default', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc).toMatchObject({ - lazyLoad: true, - hydrateServerSide: false, - }); - }); - - it('taskQueue async', () => { - userConfig.taskQueue = 'async'; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc.asyncQueue).toBe(false); - expect(bc.taskQueue).toBe(true); - expect(config.taskQueue).toBe('async'); - }); - - it('taskQueue immediate', () => { - userConfig.taskQueue = 'immediate'; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc.asyncQueue).toBe(false); - expect(bc.taskQueue).toBe(false); - expect(config.taskQueue).toBe('immediate'); - }); - - it('taskQueue congestionAsync', () => { - userConfig.taskQueue = 'congestionAsync'; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc.asyncQueue).toBe(true); - expect(bc.taskQueue).toBe(true); - expect(config.taskQueue).toBe('congestionAsync'); - }); - - it('taskQueue defaults', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc.asyncQueue).toBe(false); - expect(bc.taskQueue).toBe(true); - expect(config.taskQueue).toBe('async'); - }); - - it('tagNameTransform default', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc.transformTagName).toBe(false); - }); - - it('tagNameTransform true', () => { - userConfig.extras = { tagNameTransform: true }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc.transformTagName).toBe(true); - }); - - it('hydrateClientSide default', () => { - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc.hydrateClientSide).toBe(false); - }); - - it('hydrateClientSide true', () => { - const hydrateOutputTarget: d.OutputTargetHydrate = { - type: 'dist-hydrate-script', - }; - userConfig.outputTargets = [hydrateOutputTarget]; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc.hydrateClientSide).toBe(true); - }); - - it('hydratedSelectorName', () => { - userConfig.hydratedFlag = { - name: 'boooop', - }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getLazyBuildConditionals(config, cmps); - expect(bc.hydratedSelectorName).toBe('boooop'); - }); - }); - - describe('getHydrateBuildConditionals', () => { - it('hydratedSelectorName', () => { - userConfig.hydratedFlag = { - name: 'boooop', - }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getHydrateBuildConditionals(config, cmps); - expect(bc.hydratedSelectorName).toBe('boooop'); - }); - - it('should allow setting to use a class for hydration', () => { - userConfig.hydratedFlag = { - selector: 'class', - }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getHydrateBuildConditionals(config, cmps); - expect(bc.hydratedClass).toBe(true); - expect(bc.hydratedAttribute).toBe(false); - }); - - it('should allow setting to use an attr for hydration', () => { - userConfig.hydratedFlag = { - selector: 'attribute', - }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - const bc = getHydrateBuildConditionals(config, cmps); - expect(bc.hydratedClass).toBe(false); - expect(bc.hydratedAttribute).toBe(true); - }); - }); -}); diff --git a/src/compiler/output-targets/test/custom-elements-types.spec.ts b/src/compiler/output-targets/test/custom-elements-types.spec.ts deleted file mode 100644 index 36c5cedc1a5..00000000000 --- a/src/compiler/output-targets/test/custom-elements-types.spec.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { - mockBuildCtx, - mockCompilerCtx, - mockCompilerSystem, - mockModule, - mockValidatedConfig, -} from '@stencil/core/testing'; -import { DIST_CUSTOM_ELEMENTS, normalizePath } from '@utils'; -import path from 'path'; - -import type * as d from '../../../declarations'; -import { stubComponentCompilerMeta } from '../../types/tests/ComponentCompilerMeta.stub'; -import * as outputCustomElementsMod from '../dist-custom-elements'; -import { generateCustomElementsTypes } from '../dist-custom-elements/custom-elements-types'; - -const setup = () => { - const sys = mockCompilerSystem(); - const config: d.ValidatedConfig = mockValidatedConfig({ - configPath: '/testing-path', - buildAppCore: true, - buildEs5: true, - namespace: 'TestApp', - outputTargets: [{ type: DIST_CUSTOM_ELEMENTS, dir: 'my-best-dir' }], - srcDir: '/src', - sys, - }); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - - const root = config.rootDir; - config.rootDir = normalizePath(path.join(root, 'User', 'testing', '/')); - config.globalScript = normalizePath(path.join(root, 'User', 'testing', 'src', 'global.ts')); - - const bundleCustomElementsSpy = jest.spyOn(outputCustomElementsMod, 'bundleCustomElements'); - - compilerCtx.moduleMap.set('test', mockModule()); - - return { config, compilerCtx, buildCtx, bundleCustomElementsSpy }; -}; - -describe('Custom Elements Typedef generation', () => { - describe('export behavior: single-export-module', () => { - let config: d.ValidatedConfig; - let compilerCtx: d.CompilerCtx; - let buildCtx: d.BuildCtx; - let writeFileSpy: jest.SpyInstance; - - beforeEach(() => { - // this component tests the 'happy path' of a component's filename coinciding with its - // tag name - const componentOne = stubComponentCompilerMeta({ - tagName: 'my-component', - sourceFilePath: '/src/components/my-component/my-component.tsx', - }); - // this component tests that we correctly resolve its path when the component tag does - // not match its filename - const componentTwo = stubComponentCompilerMeta({ - sourceFilePath: '/src/components/the-other-component/my-real-best-component.tsx', - componentClassName: 'MyBestComponent', - tagName: 'my-best-component', - }); - ({ config, compilerCtx, buildCtx } = setup()); - (config.outputTargets[0] as d.OutputTargetDistCustomElements).customElementsExportBehavior = - 'single-export-module'; - buildCtx.components = [componentOne, componentTwo]; - - writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile'); - }); - - afterEach(() => { - writeFileSpy.mockRestore(); - }); - - it('should generate an index.d.ts file corresponding to the index.js file when outputting to a sub-dir of dist', async () => { - await generateCustomElementsTypes(config, compilerCtx, buildCtx, 'types_dir'); - - const expectedTypedefOutput = [ - '/* TestApp custom elements */', - `export { StubCmp as MyComponent } from '../types_dir/components/my-component/my-component';`, - `export { defineCustomElement as defineCustomElementMyComponent } from './my-component';`, - `export { MyBestComponent as MyBestComponent } from '../types_dir/components/the-other-component/my-real-best-component';`, - `export { defineCustomElement as defineCustomElementMyBestComponent } from './my-best-component';`, - '', - `/**`, - ` * Get the base path to where the assets can be found. Use "setAssetPath(path)"`, - ` * if the path needs to be customized.`, - ` */`, - `export declare const getAssetPath: (path: string) => string;`, - '', - '/**', - ' * Used to manually set the base path where assets can be found.', - ' * If the script is used as "module", it\'s recommended to use "import.meta.url",', - ' * such as "setAssetPath(import.meta.url)". Other options include', - ' * "setAssetPath(document.currentScript.src)", or using a bundler\'s replace plugin to', - ' * dynamically set the path at build time, such as "setAssetPath(process.env.ASSET_PATH)".', - ' * But do note that this configuration depends on how your script is bundled, or lack of', - ' * bundling, and where your assets can be loaded from. Additionally custom bundling', - ' * will have to ensure the static assets are copied to its build directory.', - ' */', - 'export declare const setAssetPath: (path: string) => void;', - '', - '/**', - ` * Used to specify a nonce value that corresponds with an application's CSP.`, - ' * When set, the nonce will be added to all dynamically created script and style tags at runtime.', - ' * Alternatively, the nonce value can be set on a meta tag in the DOM head', - ' * () which', - ' * will result in the same behavior.', - ' */', - 'export declare const setNonce: (nonce: string) => void', - '', - 'export interface SetPlatformOptions {', - ' raf?: (c: FrameRequestCallback) => number;', - ' ael?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;', - ' rel?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;', - '}', - 'export declare const setPlatformOptions: (opts: SetPlatformOptions) => void;', - "export * from '../types_dir/components';", - '', - ].join('\n'); - - expect(compilerCtx.fs.writeFile).toHaveBeenCalledWith('my-best-dir/index.d.ts', expectedTypedefOutput, { - outputTargetType: DIST_CUSTOM_ELEMENTS, - }); - }); - - it('should generate an index.d.ts file corresponding to the index.js file when outputting to top-level of dist', async () => { - (config.outputTargets[0] as d.OutputTargetDistCustomElements).dir = 'dist'; - - await generateCustomElementsTypes(config, compilerCtx, buildCtx, 'dist/types_dir'); - - const expectedTypedefOutput = [ - '/* TestApp custom elements */', - `export { StubCmp as MyComponent } from './types_dir/components/my-component/my-component';`, - `export { defineCustomElement as defineCustomElementMyComponent } from './my-component';`, - `export { MyBestComponent as MyBestComponent } from './types_dir/components/the-other-component/my-real-best-component';`, - `export { defineCustomElement as defineCustomElementMyBestComponent } from './my-best-component';`, - '', - `/**`, - ` * Get the base path to where the assets can be found. Use "setAssetPath(path)"`, - ` * if the path needs to be customized.`, - ` */`, - `export declare const getAssetPath: (path: string) => string;`, - '', - '/**', - ' * Used to manually set the base path where assets can be found.', - ' * If the script is used as "module", it\'s recommended to use "import.meta.url",', - ' * such as "setAssetPath(import.meta.url)". Other options include', - ' * "setAssetPath(document.currentScript.src)", or using a bundler\'s replace plugin to', - ' * dynamically set the path at build time, such as "setAssetPath(process.env.ASSET_PATH)".', - ' * But do note that this configuration depends on how your script is bundled, or lack of', - ' * bundling, and where your assets can be loaded from. Additionally custom bundling', - ' * will have to ensure the static assets are copied to its build directory.', - ' */', - 'export declare const setAssetPath: (path: string) => void;', - '', - '/**', - ` * Used to specify a nonce value that corresponds with an application's CSP.`, - ' * When set, the nonce will be added to all dynamically created script and style tags at runtime.', - ' * Alternatively, the nonce value can be set on a meta tag in the DOM head', - ' * () which', - ' * will result in the same behavior.', - ' */', - 'export declare const setNonce: (nonce: string) => void', - '', - 'export interface SetPlatformOptions {', - ' raf?: (c: FrameRequestCallback) => number;', - ' ael?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;', - ' rel?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;', - '}', - 'export declare const setPlatformOptions: (opts: SetPlatformOptions) => void;', - "export * from './types_dir/components';", - '', - ].join('\n'); - - expect(compilerCtx.fs.writeFile).toHaveBeenCalledWith('dist/index.d.ts', expectedTypedefOutput, { - outputTargetType: DIST_CUSTOM_ELEMENTS, - }); - }); - }); - - it('should generate an index.d.ts file corresponding to the index.js file when barrel export behavior is disabled', async () => { - // this component tests the 'happy path' of a component's filename coinciding with its - // tag name - const componentOne = stubComponentCompilerMeta({ - tagName: 'my-component', - sourceFilePath: '/src/components/my-component/my-component.tsx', - }); - // this component tests that we correctly resolve its path when the component tag does - // not match its filename - const componentTwo = stubComponentCompilerMeta({ - sourceFilePath: '/src/components/the-other-component/my-real-best-component.tsx', - componentClassName: 'MyBestComponent', - tagName: 'my-best-component', - }); - const { config, compilerCtx, buildCtx } = setup(); - buildCtx.components = [componentOne, componentTwo]; - - const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile'); - - await generateCustomElementsTypes(config, compilerCtx, buildCtx, 'types_dir'); - - const expectedTypedefOutput = [ - `/**`, - ` * Get the base path to where the assets can be found. Use "setAssetPath(path)"`, - ` * if the path needs to be customized.`, - ` */`, - `export declare const getAssetPath: (path: string) => string;`, - '', - '/**', - ' * Used to manually set the base path where assets can be found.', - ' * If the script is used as "module", it\'s recommended to use "import.meta.url",', - ' * such as "setAssetPath(import.meta.url)". Other options include', - ' * "setAssetPath(document.currentScript.src)", or using a bundler\'s replace plugin to', - ' * dynamically set the path at build time, such as "setAssetPath(process.env.ASSET_PATH)".', - ' * But do note that this configuration depends on how your script is bundled, or lack of', - ' * bundling, and where your assets can be loaded from. Additionally custom bundling', - ' * will have to ensure the static assets are copied to its build directory.', - ' */', - 'export declare const setAssetPath: (path: string) => void;', - '', - '/**', - " * Used to specify a nonce value that corresponds with an application's CSP.", - ' * When set, the nonce will be added to all dynamically created script and style tags at runtime.', - ' * Alternatively, the nonce value can be set on a meta tag in the DOM head', - ' * () which', - ' * will result in the same behavior.', - ' */', - 'export declare const setNonce: (nonce: string) => void', - '', - 'export interface SetPlatformOptions {', - ' raf?: (c: FrameRequestCallback) => number;', - ' ael?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;', - ' rel?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;', - '}', - 'export declare const setPlatformOptions: (opts: SetPlatformOptions) => void;', - '', - ].join('\n'); - - expect(compilerCtx.fs.writeFile).toHaveBeenCalledWith('my-best-dir/index.d.ts', expectedTypedefOutput, { - outputTargetType: DIST_CUSTOM_ELEMENTS, - }); - - writeFileSpy.mockRestore(); - }); - - it('should generate a type signature for the `defineCustomElements` function when `bundle` export behavior is set', async () => { - const componentOne = stubComponentCompilerMeta({ - tagName: 'my-component', - sourceFilePath: '/src/components/my-component/my-component.tsx', - }); - const componentTwo = stubComponentCompilerMeta({ - sourceFilePath: '/src/components/the-other-component/my-real-best-component.tsx', - componentClassName: 'MyBestComponent', - tagName: 'my-best-component', - }); - const { config, compilerCtx, buildCtx } = setup(); - (config.outputTargets[0] as d.OutputTargetDistCustomElements).customElementsExportBehavior = 'bundle'; - buildCtx.components = [componentOne, componentTwo]; - - const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile'); - - await generateCustomElementsTypes(config, compilerCtx, buildCtx, 'types_dir'); - - const expectedTypedefOutput = [ - `/**`, - ` * Get the base path to where the assets can be found. Use "setAssetPath(path)"`, - ` * if the path needs to be customized.`, - ` */`, - `export declare const getAssetPath: (path: string) => string;`, - '', - '/**', - ' * Used to manually set the base path where assets can be found.', - ' * If the script is used as "module", it\'s recommended to use "import.meta.url",', - ' * such as "setAssetPath(import.meta.url)". Other options include', - ' * "setAssetPath(document.currentScript.src)", or using a bundler\'s replace plugin to', - ' * dynamically set the path at build time, such as "setAssetPath(process.env.ASSET_PATH)".', - ' * But do note that this configuration depends on how your script is bundled, or lack of', - ' * bundling, and where your assets can be loaded from. Additionally custom bundling', - ' * will have to ensure the static assets are copied to its build directory.', - ' */', - 'export declare const setAssetPath: (path: string) => void;', - '', - '/**', - " * Used to specify a nonce value that corresponds with an application's CSP.", - ' * When set, the nonce will be added to all dynamically created script and style tags at runtime.', - ' * Alternatively, the nonce value can be set on a meta tag in the DOM head', - ' * () which', - ' * will result in the same behavior.', - ' */', - 'export declare const setNonce: (nonce: string) => void', - '', - 'export interface SetPlatformOptions {', - ' raf?: (c: FrameRequestCallback) => number;', - ' ael?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;', - ' rel?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;', - '}', - 'export declare const setPlatformOptions: (opts: SetPlatformOptions) => void;', - '', - '/**', - ` * Utility to define all custom elements within this package using the tag name provided in the component's source.`, - ` * When defining each custom element, it will also check it's safe to define by:`, - ' *', - ' * 1. Ensuring the "customElements" registry is available in the global context (window).', - ' * 2. Ensuring that the component tag name is not already defined.', - ' *', - ' * Use the standard [customElements.define()](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define)', - ' * method instead to define custom elements individually, or to provide a different tag name.', - ' */', - 'export declare const defineCustomElements: (opts?: any) => void;', - '', - ].join('\n'); - - expect(compilerCtx.fs.writeFile).toHaveBeenCalledWith('my-best-dir/index.d.ts', expectedTypedefOutput, { - outputTargetType: DIST_CUSTOM_ELEMENTS, - }); - - writeFileSpy.mockRestore(); - }); -}); diff --git a/src/compiler/output-targets/test/output-lazy-loader.spec.ts b/src/compiler/output-targets/test/output-lazy-loader.spec.ts deleted file mode 100644 index abed2489055..00000000000 --- a/src/compiler/output-targets/test/output-lazy-loader.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockBuildCtx, mockCompilerCtx, mockCompilerSystem, mockValidatedConfig } from '@stencil/core/testing'; -import { DIST, resolve } from '@utils'; - -import { validateDist } from '../../config/outputs/validate-dist'; -import { outputLazyLoader } from '../output-lazy-loader'; - -function setup(configOverrides: Partial = {}) { - const sys = mockCompilerSystem(); - const config: d.ValidatedConfig = mockValidatedConfig({ - ...configOverrides, - configPath: '/testing-path', - buildAppCore: true, - namespace: 'TestApp', - outputTargets: [ - { - type: DIST, - dir: 'my-test-dir', - }, - ], - srcDir: '/src', - sys, - }); - - config.outputTargets = validateDist(config, config.outputTargets); - - const compilerCtx = mockCompilerCtx(config); - const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile'); - const buildCtx = mockBuildCtx(config, compilerCtx); - - return { config, compilerCtx, buildCtx, writeFileSpy }; -} - -describe('Lazy Loader Output Target', () => { - let config: d.ValidatedConfig; - let compilerCtx: d.CompilerCtx; - let writeFileSpy: jest.SpyInstance; - - afterEach(() => { - writeFileSpy.mockRestore(); - }); - - it('should write code for initializing polyfills when buildEs5=true', async () => { - ({ config, compilerCtx, writeFileSpy } = setup({ buildEs5: true })); - await outputLazyLoader(config, compilerCtx); - - const expectedIndexOutput = `export * from '../esm/polyfills/index.js'; -export * from '../esm-es5/loader.js';`; - expect(writeFileSpy).toHaveBeenCalledWith(resolve('/my-test-dir/loader/index.js'), expectedIndexOutput); - - const expectedCjsIndexOutput = `module.exports = require('../cjs/loader.cjs.js'); -module.exports.applyPolyfills = function() { return Promise.resolve() };`; - expect(writeFileSpy).toHaveBeenCalledWith(resolve('/my-test-dir/loader/index.cjs.js'), expectedCjsIndexOutput); - - const expectedES2017Output = `export * from '../esm/polyfills/index.js'; -export * from '../esm/loader.js';`; - expect(writeFileSpy).toHaveBeenCalledWith(resolve('/my-test-dir/loader/index.es2017.js'), expectedES2017Output); - }); - - it('should exclude polyfill code when buildEs5=false', async () => { - ({ config, compilerCtx, writeFileSpy } = setup({ buildEs5: false })); - await outputLazyLoader(config, compilerCtx); - - const expectedIndexOutput = `export * from '../esm/loader.js';`; - expect(writeFileSpy).toHaveBeenCalledWith(resolve('/my-test-dir/loader/index.js'), expectedIndexOutput); - - const expectedCjsIndexOutput = `module.exports = require('../cjs/loader.cjs.js');`; - expect(writeFileSpy).toHaveBeenCalledWith(resolve('/my-test-dir/loader/index.cjs.js'), expectedCjsIndexOutput); - - const expectedES2017Output = `export * from '../esm/loader.js';`; - expect(writeFileSpy).toHaveBeenCalledWith(resolve('/my-test-dir/loader/index.es2017.js'), expectedES2017Output); - }); -}); diff --git a/src/compiler/output-targets/test/output-targets-collection.spec.ts b/src/compiler/output-targets/test/output-targets-collection.spec.ts deleted file mode 100644 index 95d00e6bfce..00000000000 --- a/src/compiler/output-targets/test/output-targets-collection.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { mockBuildCtx, mockCompilerCtx, mockModule, mockValidatedConfig } from '@stencil/core/testing'; - -import type * as d from '../../../declarations'; -import * as test from '../../transformers/map-imports-to-path-aliases'; -import { outputCollection } from '../dist-collection'; - -describe('Dist Collection output target', () => { - let mockConfig: d.ValidatedConfig; - let mockedBuildCtx: d.BuildCtx; - let mockedCompilerCtx: d.CompilerCtx; - let changedModules: d.Module[]; - - let mapImportPathSpy: jest.SpyInstance; - - const mockTraverse = jest.fn().mockImplementation((source: any) => source); - const mockMap = jest.fn().mockImplementation(() => mockTraverse); - const target: d.OutputTargetDistCollection = { - type: 'dist-collection', - dir: '', - collectionDir: '/dist/collection', - }; - - beforeEach(() => { - mockConfig = mockValidatedConfig({ - srcDir: '/src', - }); - mockedBuildCtx = mockBuildCtx(); - mockedCompilerCtx = mockCompilerCtx(); - changedModules = [ - mockModule({ - staticSourceFileText: '', - jsFilePath: '/src/main.js', - sourceFilePath: '/src/main.ts', - }), - ]; - - jest.spyOn(mockedCompilerCtx.fs, 'writeFile'); - - mapImportPathSpy = jest.spyOn(test, 'mapImportsToPathAliases'); - mapImportPathSpy.mockReturnValue(mockMap); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('transform aliased import paths', () => { - // These tests ensure that the transformer for import paths is called regardless - // of the config value (the function will decide whether or not to actually do anything) to avoid - // a race condition with duplicate file writes - it.each([true, false])( - 'calls function to transform aliased import paths when the output target config flag is `%s`', - async (transformAliasedImportPaths: boolean) => { - mockConfig.outputTargets = [ - { - ...target, - transformAliasedImportPaths, - }, - ]; - - await outputCollection(mockConfig, mockedCompilerCtx, mockedBuildCtx, changedModules); - - expect(mapImportPathSpy).toHaveBeenCalledWith(mockConfig, '/dist/collection/main.js', { - collectionDir: '/dist/collection', - dir: '', - transformAliasedImportPaths, - type: 'dist-collection', - }); - expect(mapImportPathSpy).toHaveBeenCalledTimes(1); - }, - ); - }); -}); diff --git a/src/compiler/output-targets/test/output-targets-dist-custom-elements.spec.ts b/src/compiler/output-targets/test/output-targets-dist-custom-elements.spec.ts deleted file mode 100644 index 72334f4fdd9..00000000000 --- a/src/compiler/output-targets/test/output-targets-dist-custom-elements.spec.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { - mockBuildCtx, - mockCompilerCtx, - mockCompilerSystem, - mockModule, - mockValidatedConfig, -} from '@stencil/core/testing'; -import { DIST_CUSTOM_ELEMENTS } from '@utils'; -import path from 'path'; - -import type * as d from '../../../declarations'; -import { OutputTargetDistCustomElements } from '../../../declarations'; -import { STENCIL_APP_GLOBALS_ID, STENCIL_INTERNAL_CLIENT_ID, USER_INDEX_ENTRY_ID } from '../../bundle/entry-alias-ids'; -import { stubComponentCompilerMeta } from '../../types/tests/ComponentCompilerMeta.stub'; -import * as outputCustomElementsMod from '../dist-custom-elements'; -import { - addCustomElementInputs, - bundleCustomElements, - generateEntryPoint, - getBundleOptions, - outputCustomElements, -} from '../dist-custom-elements'; - -const setup = () => { - const sys = mockCompilerSystem(); - const config: d.ValidatedConfig = mockValidatedConfig({ - buildAppCore: true, - buildEs5: true, - configPath: '/testing-path', - namespace: 'TestApp', - outputTargets: [{ type: DIST_CUSTOM_ELEMENTS }], - srcDir: '/src', - sys, - }); - const compilerCtx = mockCompilerCtx(config); - const buildCtx = mockBuildCtx(config, compilerCtx); - - const root = config.rootDir; - config.rootDir = path.join(root, 'User', 'testing', '/'); - config.globalScript = path.join(root, 'User', 'testing', 'src', 'global.ts'); - - const bundleCustomElementsSpy = jest.spyOn(outputCustomElementsMod, 'bundleCustomElements'); - - compilerCtx.moduleMap.set('test', mockModule()); - - return { config, compilerCtx, buildCtx, bundleCustomElementsSpy }; -}; - -describe('Custom Elements output target', () => { - it('should return early if config.buildDist is false', async () => { - const { config, compilerCtx, buildCtx, bundleCustomElementsSpy } = setup(); - config.buildDist = false; - await outputCustomElements(config, compilerCtx, buildCtx); - expect(bundleCustomElementsSpy).not.toHaveBeenCalled(); - }); - - it.each([[[]], [[{ type: 'dist' }]]])( - 'should return early if no appropriate output target (%j)', - async (outputTargets) => { - const { config, compilerCtx, buildCtx, bundleCustomElementsSpy } = setup(); - config.outputTargets = outputTargets; - await outputCustomElements(config, compilerCtx, buildCtx); - expect(bundleCustomElementsSpy).not.toHaveBeenCalled(); - }, - ); - - describe('generateEntryPoint', () => { - it('should include global scripts when flag is `true`', () => { - const entryPoint = generateEntryPoint({ - type: DIST_CUSTOM_ELEMENTS, - includeGlobalScripts: true, - }); - - expect(entryPoint).toEqual(`import { globalScripts } from '${STENCIL_APP_GLOBALS_ID}'; -export { getAssetPath, setAssetPath, setNonce, setPlatformOptions, render } from '${STENCIL_INTERNAL_CLIENT_ID}'; -export * from '${USER_INDEX_ENTRY_ID}'; - -globalScripts(); -`); - }); - - it('should not include global scripts when flag is `false`', () => { - const entryPoint = generateEntryPoint({ - type: DIST_CUSTOM_ELEMENTS, - includeGlobalScripts: false, - }); - - expect(entryPoint) - .toEqual(`export { getAssetPath, setAssetPath, setNonce, setPlatformOptions, render } from '${STENCIL_INTERNAL_CLIENT_ID}'; -export * from '${USER_INDEX_ENTRY_ID}'; -`); - }); - }); - - describe('getBundleOptions', () => { - it('should set basic properties on BundleOptions', () => { - const { config, buildCtx, compilerCtx } = setup(); - const options = getBundleOptions(config, buildCtx, compilerCtx, { type: DIST_CUSTOM_ELEMENTS }); - expect(options.id).toBe('customElements'); - expect(options.platform).toBe('client'); - expect(options.inlineWorkers).toBe(true); - expect(options.inputs).toEqual({ - index: '\0core', - }); - expect(options.loader).toEqual({}); - expect(options.preserveEntrySignatures).toEqual('allow-extension'); - }); - - it.each([true, false, undefined])('should set externalRuntime correctly when %p', (externalRuntime) => { - const { config, buildCtx, compilerCtx } = setup(); - const options = getBundleOptions(config, buildCtx, compilerCtx, { - type: DIST_CUSTOM_ELEMENTS, - externalRuntime, - }); - if (externalRuntime) { - expect(options.externalRuntime).toBe(true); - } else { - expect(options.externalRuntime).toBe(false); - } - }); - }); - - describe('bundleCustomElements', () => { - it('should set a diagnostic if no `dir` prop on the output target', async () => { - const { config, compilerCtx, buildCtx } = setup(); - const outputTarget: OutputTargetDistCustomElements = { type: DIST_CUSTOM_ELEMENTS, externalRuntime: true }; - await bundleCustomElements(config, compilerCtx, buildCtx, outputTarget); - expect(buildCtx.diagnostics).toEqual([ - { - level: 'error', - lines: [], - type: 'build', - messageText: 'dist-custom-elements output target provided with no output target directory!', - }, - ]); - }); - }); - - describe('addCustomElementInputs', () => { - let config: d.ValidatedConfig; - let compilerCtx: d.CompilerCtx; - let buildCtx: d.BuildCtx; - - beforeEach(() => { - ({ config, compilerCtx, buildCtx } = setup()); - }); - - describe('no defined CustomElementsExportBehavior', () => { - it("doesn't re-export components from the index.js barrel file", () => { - const componentOne = stubComponentCompilerMeta(); - const componentTwo = stubComponentCompilerMeta({ - componentClassName: 'MyBestComponent', - tagName: 'my-best-component', - }); - - buildCtx.components = [componentOne, componentTwo]; - - const bundleOptions = getBundleOptions( - config, - buildCtx, - compilerCtx, - config.outputTargets[0] as OutputTargetDistCustomElements, - ); - addCustomElementInputs(buildCtx, bundleOptions, config.outputTargets[0] as OutputTargetDistCustomElements); - expect(bundleOptions.loader['\0core']).toEqual( - `import { globalScripts } from '${STENCIL_APP_GLOBALS_ID}'; -export { getAssetPath, setAssetPath, setNonce, setPlatformOptions, render } from '${STENCIL_INTERNAL_CLIENT_ID}'; -export * from '${USER_INDEX_ENTRY_ID}'; - -globalScripts(); -`, - ); - }); - }); - - describe('CustomElementsExportBehavior.SINGLE_EXPORT_MODULE', () => { - beforeEach(() => { - (config.outputTargets[0] as OutputTargetDistCustomElements).customElementsExportBehavior = - 'single-export-module'; - }); - - it('should add imports to index.js for all included components', () => { - const componentOne = stubComponentCompilerMeta(); - const componentTwo = stubComponentCompilerMeta({ - componentClassName: 'MyBestComponent', - tagName: 'my-best-component', - }); - - buildCtx.components = [componentOne, componentTwo]; - - const bundleOptions = getBundleOptions( - config, - buildCtx, - compilerCtx, - config.outputTargets[0] as OutputTargetDistCustomElements, - ); - addCustomElementInputs(buildCtx, bundleOptions, config.outputTargets[0] as OutputTargetDistCustomElements); - expect(bundleOptions.loader['\0core']).toEqual( - `import { globalScripts } from '${STENCIL_APP_GLOBALS_ID}'; -export { getAssetPath, setAssetPath, setNonce, setPlatformOptions, render } from '${STENCIL_INTERNAL_CLIENT_ID}'; -export * from '${USER_INDEX_ENTRY_ID}'; -export { StubCmp, defineCustomElement as defineCustomElementStubCmp } from '\0StubCmp'; -export { MyBestComponent, defineCustomElement as defineCustomElementMyBestComponent } from '\0MyBestComponent'; - -globalScripts(); -`, - ); - }); - - it('should correctly handle capitalization edge-cases', () => { - const component = stubComponentCompilerMeta({ - componentClassName: 'ComponentWithJSX', - tagName: 'component-with-jsx', - }); - - buildCtx.components = [component]; - - const bundleOptions = getBundleOptions( - config, - buildCtx, - compilerCtx, - config.outputTargets[0] as OutputTargetDistCustomElements, - ); - addCustomElementInputs(buildCtx, bundleOptions, config.outputTargets[0] as OutputTargetDistCustomElements); - expect(bundleOptions.loader['\0core']).toEqual( - `import { globalScripts } from '${STENCIL_APP_GLOBALS_ID}'; -export { getAssetPath, setAssetPath, setNonce, setPlatformOptions, render } from '${STENCIL_INTERNAL_CLIENT_ID}'; -export * from '${USER_INDEX_ENTRY_ID}'; -export { ComponentWithJsx, defineCustomElement as defineCustomElementComponentWithJsx } from '\0ComponentWithJsx'; - -globalScripts(); -`, - ); - }); - }); - - describe('CustomElementsExportBehavior.BUNDLE', () => { - beforeEach(() => { - (config.outputTargets[0] as OutputTargetDistCustomElements).customElementsExportBehavior = 'bundle'; - }); - - it('should add a `defineCustomElements` function to the index.js file', () => { - const componentOne = stubComponentCompilerMeta(); - const componentTwo = stubComponentCompilerMeta({ - componentClassName: 'MyBestComponent', - tagName: 'my-best-component', - }); - - buildCtx.components = [componentOne, componentTwo]; - - const bundleOptions = getBundleOptions( - config, - buildCtx, - compilerCtx, - config.outputTargets[0] as OutputTargetDistCustomElements, - ); - addCustomElementInputs(buildCtx, bundleOptions, config.outputTargets[0] as OutputTargetDistCustomElements); - expect(bundleOptions.loader['\0core']).toEqual( - `import { globalScripts } from '${STENCIL_APP_GLOBALS_ID}'; -import { transformTag } from '@stencil/core/internal/client'; -import { StubCmp } from '\0StubCmp'; -import { MyBestComponent } from '\0MyBestComponent'; -export { getAssetPath, setAssetPath, setNonce, setPlatformOptions, render } from '${STENCIL_INTERNAL_CLIENT_ID}'; -export * from '${USER_INDEX_ENTRY_ID}'; - -globalScripts(); -export const defineCustomElements = (opts) => { - if (typeof customElements !== 'undefined') { - [ - StubCmp, - MyBestComponent, - ].forEach(cmp => { - if (!customElements.get(transformTag(cmp.is))) { - customElements.define(transformTag(cmp.is), cmp, opts); - } - }); - } -}; -`, - ); - }); - }); - - describe('autoLoader', () => { - it('should add a loader virtual module when autoLoader is true', () => { - const componentOne = stubComponentCompilerMeta(); - const componentTwo = stubComponentCompilerMeta({ - componentClassName: 'MyBestComponent', - tagName: 'my-best-component', - }); - - buildCtx.components = [componentOne, componentTwo]; - - const outputTarget = config.outputTargets[0] as OutputTargetDistCustomElements; - outputTarget.autoLoader = { fileName: 'loader', autoStart: true }; - - const bundleOptions = getBundleOptions(config, buildCtx, compilerCtx, outputTarget); - addCustomElementInputs(buildCtx, bundleOptions, outputTarget); - - // Check loader input is added - expect(bundleOptions.inputs['loader']).toBe('\0loader'); - - // Check loader module content - const loaderContent = bundleOptions.loader['\0loader']; - expect(loaderContent).toContain(`import { transformTag } from '${STENCIL_INTERNAL_CLIENT_ID}'`); - expect(loaderContent).toContain("'stub-cmp': './stub-cmp.js'"); - expect(loaderContent).toContain("'my-best-component': './my-best-component.js'"); - expect(loaderContent).toContain('export function start('); - expect(loaderContent).toContain('export function stop('); - expect(loaderContent).toContain('start();'); // autoStart is true - }); - - it('should not auto-start when autoStart is false', () => { - const component = stubComponentCompilerMeta(); - buildCtx.components = [component]; - - const outputTarget = config.outputTargets[0] as OutputTargetDistCustomElements; - outputTarget.autoLoader = { fileName: 'loader', autoStart: false }; - - const bundleOptions = getBundleOptions(config, buildCtx, compilerCtx, outputTarget); - addCustomElementInputs(buildCtx, bundleOptions, outputTarget); - - const loaderContent = bundleOptions.loader['\0loader']; - // Should export start/stop but NOT auto-call start() - expect(loaderContent).toContain('export function start('); - expect(loaderContent).toContain('export function stop('); - expect(loaderContent).not.toMatch(/^start\(\);$/m); - }); - - it('should use custom fileName for loader', () => { - const component = stubComponentCompilerMeta(); - buildCtx.components = [component]; - - const outputTarget = config.outputTargets[0] as OutputTargetDistCustomElements; - outputTarget.autoLoader = { fileName: 'my-custom-loader', autoStart: true }; - - const bundleOptions = getBundleOptions(config, buildCtx, compilerCtx, outputTarget); - addCustomElementInputs(buildCtx, bundleOptions, outputTarget); - - expect(bundleOptions.inputs['my-custom-loader']).toBe('\0loader'); - }); - - it('should not add loader when autoLoader is not set', () => { - const component = stubComponentCompilerMeta(); - buildCtx.components = [component]; - - const outputTarget = config.outputTargets[0] as OutputTargetDistCustomElements; - // autoLoader is not set - - const bundleOptions = getBundleOptions(config, buildCtx, compilerCtx, outputTarget); - addCustomElementInputs(buildCtx, bundleOptions, outputTarget); - - expect(bundleOptions.inputs['loader']).toBeUndefined(); - expect(bundleOptions.loader['\0loader']).toBeUndefined(); - }); - }); - }); -}); diff --git a/src/compiler/output-targets/test/output-targets-dist.spec.ts b/src/compiler/output-targets/test/output-targets-dist.spec.ts deleted file mode 100644 index e4d5dd03f10..00000000000 --- a/src/compiler/output-targets/test/output-targets-dist.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -// @ts-nocheck -import { Compiler, Config } from '@stencil/core/compiler'; -import { mockConfig } from '@stencil/core/testing'; -import path from 'path'; - -import { expectFilesDoNotExist, expectFilesExist } from '../../../testing/testing-utils'; - -describe.skip('outputTarget, dist', () => { - jest.setTimeout(20000); - let compiler: Compiler; - let config: Config; - const root = path.resolve('/'); - - it('default dist files', async () => { - config = mockConfig({ - buildAppCore: true, - buildEs5: true, - globalScript: path.join(root, 'User', 'testing', 'src', 'global.ts'), - namespace: 'TestApp', - outputTargets: [{ type: 'dist' }], - rootDir: path.join(root, 'User', 'testing', '/'), - }); - - compiler = new Compiler(config); - - await compiler.fs.writeFiles({ - [path.join(config.sys.getClientPath('polyfills/index.js'))]: `/* polyfills */`, - [path.join(root, 'User', 'testing', 'package.json')]: `{ - "module": "dist/index.mjs", - "main": "dist/index.js", - "collection": "dist/collection/collection-manifest.json", - "types": "dist/types/components.d.ts" - }`, - [path.join(root, 'User', 'testing', 'src', 'index.html')]: ``, - [path.join(root, 'User', 'testing', 'src', 'components', 'cmp-a.tsx')]: ` - @Component({ - tag: 'cmp-a', - styleUrls: { - ios: 'cmp-a.ios.css', - md: 'cmp-a.md.css' - } - }) export class CmpA {}`, - [path.join(root, 'User', 'testing', 'src', 'components', 'cmp-a.ios.css')]: `cmp-a { color: blue; }`, - [path.join(root, 'User', 'testing', 'src', 'components', 'cmp-a.md.css')]: `cmp-a { color: green; }`, - [path.join(root, 'User', 'testing', 'src', 'global.ts')]: - `export default function() { console.log('my global'); }`, - }); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - - expectFilesExist(compiler.fs, [ - path.join(root, 'User', 'testing', 'dist', 'index.js'), - path.join(root, 'User', 'testing', 'dist', 'index.mjs'), - path.join(root, 'User', 'testing', 'dist', 'index.js.map'), - - path.join(root, 'User', 'testing', 'dist', 'collection', 'collection-manifest.json'), - path.join(root, 'User', 'testing', 'dist', 'collection', 'components', 'cmp-a.js'), - path.join(root, 'User', 'testing', 'dist', 'collection', 'components', 'cmp-a.js.map'), - path.join(root, 'User', 'testing', 'dist', 'collection', 'components', 'cmp-a.ios.css'), - path.join(root, 'User', 'testing', 'dist', 'collection', 'components', 'cmp-a.md.css'), - path.join(root, 'User', 'testing', 'dist', 'collection', 'global.js'), - path.join(root, 'User', 'testing', 'dist', 'collection', 'global.js.map'), - - path.join(root, 'User', 'testing', 'dist', 'esm', 'index.mjs'), - path.join(root, 'User', 'testing', 'dist', 'esm', 'index.js.map'), - path.join(root, 'User', 'testing', 'dist', 'esm', 'loader.mjs'), - path.join(root, 'User', 'testing', 'dist', 'esm-es5', 'index.mjs'), - path.join(root, 'User', 'testing', 'dist', 'esm-es5', 'index.js.map'), - path.join(root, 'User', 'testing', 'dist', 'esm-es5', 'loader.mjs'), - path.join(root, 'User', 'testing', 'dist', 'esm', 'polyfills', 'index.js'), - path.join(root, 'User', 'testing', 'dist', 'esm', 'polyfills', 'index.js.map'), - - path.join(root, 'User', 'testing', 'dist', 'loader'), - - path.join(root, 'User', 'testing', 'dist', 'types'), - - path.join(root, 'User', 'testing', 'src', 'components.d.ts'), - ]); - - expectFilesDoNotExist(compiler.fs, [ - path.join(root, 'User', 'testing', 'build'), - path.join(root, 'User', 'testing', 'esm'), - path.join(root, 'User', 'testing', 'es5'), - path.join(root, 'User', 'testing', 'www'), - path.join(root, 'User', 'testing', 'index.html'), - ]); - }); -}); diff --git a/src/compiler/output-targets/test/output-targets-www-dist.spec.ts b/src/compiler/output-targets/test/output-targets-www-dist.spec.ts deleted file mode 100644 index e4d93c933dd..00000000000 --- a/src/compiler/output-targets/test/output-targets-www-dist.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -// @ts-nocheck -import { Compiler, Config } from '@stencil/core/compiler'; -import type * as d from '@stencil/core/declarations'; -import { mockConfig } from '@stencil/core/testing'; -import path from 'path'; - -import { expectFilesDoNotExist, expectFilesExist } from '../../../testing/testing-utils'; - -describe.skip('outputTarget, www / dist / docs', () => { - jest.setTimeout(20000); - let compiler: Compiler; - let config: Config; - const root = path.resolve('/'); - - it('dist, www and readme files w/ custom paths', async () => { - config = mockConfig({ - buildAppCore: true, - flags: { docs: true }, - namespace: 'TestApp', - outputTargets: [ - { - type: 'www', - dir: 'custom-www', - buildDir: 'www-build', - indexHtml: 'custom-index.htm', - } as any as d.OutputTargetDist, - { - type: 'dist', - dir: 'custom-dist', - buildDir: 'dist-build', - collectionDir: 'dist-collection', - typesDir: 'custom-types', - }, - { - type: 'docs', - } as d.OutputTargetDocsReadme, - ], - rootDir: path.join(root, 'User', 'testing', '/'), - }); - - compiler = new Compiler(config); - - await compiler.fs.writeFiles({ - [path.join(root, 'User', 'testing', 'package.json')]: `{ - "module": "custom-dist/index.mjs", - "main": "custom-dist/index.js", - "collection": "custom-dist/dist-collection/collection-manifest.json", - "types": "custom-dist/custom-types/components.d.ts" - }`, - [path.join(root, 'User', 'testing', 'src', 'index.html')]: ``, - [path.join(config.sys.getClientPath('polyfills/index.js'))]: `/* polyfills */`, - [path.join(root, 'User', 'testing', 'src', 'components', 'cmp-a.tsx')]: - `@Component({ tag: 'cmp-a' }) export class CmpA {}`, - }); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - - expectFilesExist(compiler.fs, [ - path.join(root, 'User', 'testing', 'custom-dist', 'cjs'), - path.join(root, 'User', 'testing', 'custom-dist', 'esm', 'polyfills', 'index.js'), - path.join(root, 'User', 'testing', 'custom-dist', 'esm', 'polyfills', 'index.js.map'), - ]); - - expectFilesDoNotExist(compiler.fs, [ - path.join(root, 'User', 'testing', 'www', '/'), - path.join(root, 'User', 'testing', 'www', 'index.html'), - path.join(root, 'User', 'testing', 'www', 'custom-index.htm'), - path.join(root, 'User', 'testing', 'custom-www', 'index.html'), - ]); - }); -}); diff --git a/src/compiler/output-targets/test/output-targets-www.spec.ts b/src/compiler/output-targets/test/output-targets-www.spec.ts deleted file mode 100644 index 9f0c8c02cb8..00000000000 --- a/src/compiler/output-targets/test/output-targets-www.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -// @ts-nocheck -import { Compiler, Config } from '@stencil/core/compiler'; -import { mockConfig } from '@stencil/core/testing'; -import path from 'path'; - -import { expectFilesDoNotExist, expectFilesExist } from '../../../testing/testing-utils'; - -describe.skip('outputTarget, www', () => { - jest.setTimeout(20000); - let compiler: Compiler; - let config: Config; - const root = path.resolve('/'); - - it('default www files', async () => { - config = mockConfig({ - buildAppCore: true, - namespace: 'App', - rootDir: path.join(root, 'User', 'testing', '/'), - }); - - compiler = new Compiler(config); - - await compiler.fs.writeFiles({ - [path.join(root, 'User', 'testing', 'src', 'index.html')]: ``, - [path.join(root, 'User', 'testing', 'src', 'components', 'cmp-a.tsx')]: - `@Component({ tag: 'cmp-a' }) export class CmpA {}`, - }); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - - expectFilesExist(compiler.fs, [ - path.join(root, 'User', 'testing', 'www'), - path.join(root, 'User', 'testing', 'www', 'build'), - path.join(root, 'User', 'testing', 'www', 'build', 'app.js'), - path.join(root, 'User', 'testing', 'www', 'build', 'app.js.map'), - path.join(root, 'User', 'testing', 'www', 'build', 'app.esm.js'), - path.join(root, 'User', 'testing', 'www', 'build', 'cmp-a.entry.js'), - path.join(root, 'User', 'testing', 'www', 'build', 'cmp-a.entry.js.map'), - - path.join(root, 'User', 'testing', 'www', 'index.html'), - - path.join(root, 'User', 'testing', 'src', 'components.d.ts'), - ]); - - expectFilesDoNotExist(compiler.fs, [ - path.join(root, 'User', 'testing', 'src', 'components', 'cmp-a.js'), - - path.join(root, 'User', 'testing', 'dist', '/'), - path.join(root, 'User', 'testing', 'dist', 'collection'), - path.join(root, 'User', 'testing', 'dist', 'collection', 'collection-manifest.json'), - path.join(root, 'User', 'testing', 'dist', 'collection', 'components'), - path.join(root, 'User', 'testing', 'dist', 'collection', 'components', 'cmp-a.js'), - - path.join(root, 'User', 'testing', 'dist', 'testapp', '/'), - path.join(root, 'User', 'testing', 'dist', 'testapp.js'), - path.join(root, 'User', 'testing', 'dist', 'testapp', 'cmp-a.entry.js'), - path.join(root, 'User', 'testing', 'dist', 'testapp', 'es5-build-disabled.js'), - path.join(root, 'User', 'testing', 'dist', 'testapp', 'testapp.core.js'), - - path.join(root, 'User', 'testing', 'dist', 'types'), - path.join(root, 'User', 'testing', 'dist', 'types', 'components'), - path.join(root, 'User', 'testing', 'dist', 'types', 'components.d.ts'), - path.join(root, 'User', 'testing', 'dist', 'types', 'components', 'cmp-a.d.ts'), - path.join(root, 'User', 'testing', 'dist', 'types', 'stencil.core.d.ts'), - ]); - }); -}); diff --git a/src/compiler/output-targets/test/tsconfig.json b/src/compiler/output-targets/test/tsconfig.json deleted file mode 100644 index 0ff0be83173..00000000000 --- a/src/compiler/output-targets/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/plugin/test/plugin.spec.ts b/src/compiler/plugin/test/plugin.spec.ts deleted file mode 100644 index 9abf278b6cd..00000000000 --- a/src/compiler/plugin/test/plugin.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -// @ts-nocheck -import { createCompiler } from '@stencil/core/compiler'; -import { mockConfig } from '@stencil/core/testing'; -import { normalizePath } from '@utils'; -import path from 'path'; - -import type * as d from '../../../declarations'; - -describe.skip('plugin', () => { - jest.setTimeout(20000); - let compiler: d.Compiler; - let config: d.Config; - const root = path.resolve('/'); - - beforeEach(async () => { - config = mockConfig(); - compiler = await createCompiler(config); - console.log(compiler.sys); - await compiler.sys.writeFile(path.join(root, 'src', 'index.html'), ``); - }); - - afterEach(async () => { - await compiler.destroy(); - }); - - it('transform, async', async () => { - compiler.config.bundles = [{ components: ['cmp-a'] }]; - - await compiler.fs.writeFiles( - { - [path.join(root, 'stencil.config.js')]: ` - - exports.config = { - plugins: [myPlugin()] - }; - `, - [path.join(root, 'src', 'cmp-a.tsx')]: ` - @Component({ tag: 'cmp-a' }) export class CmpA { - constructor() { } - } - `, - }, - { clearFileCache: true }, - ); - await compiler.fs.commit(); - - function myPlugin() { - return { - transform: function (sourceText: string) { - return new Promise((resolve) => { - sourceText += `\nconsole.log('transformed!')`; - resolve(sourceText); - }); - }, - name: 'myPlugin', - }; - } - - config.rollupPlugins = [myPlugin()]; - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - - const cmpA = await compiler.fs.readFile(path.join(root, 'www', 'build', 'cmp-a.entry.js')); - expect(cmpA).toContain('transformed!'); - }); - - it('transform, sync', async () => { - await compiler.fs.writeFiles( - { - [path.join(root, 'src', 'cmp-a.tsx')]: ` - @Component({ tag: 'cmp-a' }) export class CmpA { - constructor() { } - } - `, - }, - { clearFileCache: true }, - ); - await compiler.fs.commit(); - - function myPlugin() { - return { - transform(sourceText: string) { - sourceText += `\nconsole.log('transformed!')`; - return sourceText; - }, - name: 'myPlugin', - }; - } - - config.rollupPlugins = [myPlugin()]; - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - - const cmpA = await compiler.fs.readFile(path.join(root, 'www', 'build', 'cmp-a.entry.js')); - expect(cmpA).toContain('transformed!'); - }); - - it('resolveId, async', async () => { - const filePath = normalizePath(path.join(root, 'dist', 'my-dep-fn.js')); - - await compiler.fs.writeFiles( - { - [path.join(root, 'src', 'cmp-a.tsx')]: ` - import { depFn } '#crazy-path!' - @Component({ tag: 'cmp-a' }) export class CmpA { - constructor() { - depFn(); - } - } - `, - [filePath]: ` - export function depFn(){ - console.log('imported depFun()'); - } - `, - }, - { clearFileCache: true }, - ); - await compiler.fs.commit(); - - function myPlugin() { - return { - resolveId(importee: string) { - if (importee === '#crazy-path!') { - return Promise.resolve(filePath); - } - return Promise.resolve(null); - }, - name: 'myPlugin', - }; - } - - config.rollupPlugins = [myPlugin()]; - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - - const cmpA = await compiler.fs.readFile(path.join(root, 'www', 'build', 'cmp-a.entry.js')); - expect(cmpA).toContain('imported depFun()'); - }); - - it('resolveId, sync', async () => { - const filePath = normalizePath(path.join(root, 'dist', 'my-dep-fn.js')); - - await compiler.fs.writeFiles( - { - [path.join(root, 'src', 'cmp-a.tsx')]: ` - import { depFn } '#crazy-path!' - @Component({ tag: 'cmp-a' }) export class CmpA { - constructor() { - depFn(); - } - } - `, - [filePath]: ` - export function depFn(){ - console.log('imported depFun()'); - } - `, - }, - { clearFileCache: true }, - ); - await compiler.fs.commit(); - - function myPlugin() { - return { - resolveId(importee: string) { - if (importee === '#crazy-path!') { - return filePath; - } - return null; - }, - name: 'myPlugin', - }; - } - config.rollupPlugins = [myPlugin()]; - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - - const cmpA = await compiler.fs.readFile(path.join(root, 'www', 'build', 'cmp-a.entry.js')); - expect(cmpA).toContain('imported depFun()'); - }); -}); diff --git a/src/compiler/plugin/test/tsconfig.json b/src/compiler/plugin/test/tsconfig.json deleted file mode 100644 index 0ff0be83173..00000000000 --- a/src/compiler/plugin/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/prerender/prerender-config.ts b/src/compiler/prerender/prerender-config.ts deleted file mode 100644 index f0ff1b37a6d..00000000000 --- a/src/compiler/prerender/prerender-config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { isString } from '@utils'; - -import type * as d from '../../declarations'; -import { nodeRequire } from '../sys/node-require'; - -export const getPrerenderConfig = (diagnostics: d.Diagnostic[], prerenderConfigPath: string) => { - const prerenderConfig: d.PrerenderConfig = {}; - - if (isString(prerenderConfigPath)) { - const results = nodeRequire(prerenderConfigPath); - diagnostics.push(...results.diagnostics); - - if (results.module != null && typeof results.module === 'object') { - if (results.module.config != null && typeof results.module.config === 'object') { - Object.assign(prerenderConfig, results.module.config); - } else { - Object.assign(prerenderConfig, results.module); - } - } - } - - return prerenderConfig; -}; diff --git a/src/compiler/prerender/test/crawl-urls.spec.ts b/src/compiler/prerender/test/crawl-urls.spec.ts deleted file mode 100644 index d1178ec8fca..00000000000 --- a/src/compiler/prerender/test/crawl-urls.spec.ts +++ /dev/null @@ -1,307 +0,0 @@ -import type * as d from '../../../declarations'; -import { crawlAnchorsForNextUrls } from '../crawl-urls'; - -describe('crawlAnchorsForNextUrls', () => { - let prerenderConfig: d.PrerenderConfig; - let diagnostics: d.Diagnostic[]; - let baseUrl: URL; - let currentUrl: URL; - let parsedAnchors: d.HydrateAnchorElement[]; - - beforeEach(() => { - prerenderConfig = {}; - diagnostics = []; - baseUrl = new URL('http://stenciljs.com/'); - currentUrl = new URL('http://stenciljs.com/docs'); - }); - - it('user filterUrl()', () => { - parsedAnchors = [{ href: '/docs' }, { href: '/docs/v3' }, { href: '/docs/v3/components' }]; - prerenderConfig.filterUrl = function (url) { - if (url.pathname.startsWith('/docs/v3')) { - return false; - } - return true; - }; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/docs'); - }); - - it('user normalizeUrl()', () => { - parsedAnchors = [{ href: '/doczz' }, { href: '/docs' }]; - prerenderConfig.normalizeUrl = function (href, base) { - const url = new URL(href, base); - - if (url.pathname === '/doczz') { - url.pathname = '/docs'; - } - - return url; - }; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/docs'); - }); - - it('user filterAnchor()', () => { - parsedAnchors = [ - { href: '/docs' }, - { href: '/docs/about-us', 'data-prerender': 'yes-plz' }, - { href: '/docs/app', 'data-prerender': 'no-prerender' }, - ]; - prerenderConfig.filterAnchor = function (anchor) { - if (anchor['data-prerender'] === 'no-prerender') { - return false; - } - return true; - }; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(2); - expect(hrefs[0]).toBe('http://stenciljs.com/docs'); - expect(hrefs[1]).toBe('http://stenciljs.com/docs/about-us'); - }); - - it('normalize with encoded characters', () => { - parsedAnchors = [{ href: '/about%20us' }, { href: '/about us' }]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/about%20us'); - }); - - it('normalize with trailing slash', () => { - prerenderConfig.trailingSlash = true; - parsedAnchors = [ - { href: '/' }, - { href: '/about-us' }, - { href: '/about-us/' }, - { href: '/docs' }, - { href: '/docs/' }, - { href: '/docs/index.html' }, - ]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(3); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - expect(hrefs[1]).toBe('http://stenciljs.com/about-us/'); - expect(hrefs[2]).toBe('http://stenciljs.com/docs/'); - }); - - it('normalize without trailing slash', () => { - parsedAnchors = [ - { href: '/' }, - { href: '/about-us' }, - { href: '/about-us/' }, - { href: '/docs' }, - { href: '/docs/' }, - { href: '/docs/index.html' }, - ]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(3); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - expect(hrefs[1]).toBe('http://stenciljs.com/about-us'); - expect(hrefs[2]).toBe('http://stenciljs.com/docs'); - }); - - it('skip directories below base path', () => { - baseUrl = new URL('http://stenciljs.com/docs'); - parsedAnchors = [ - { href: '/' }, - { href: '/about-us' }, - { href: '/contact-us' }, - { href: '/docs' }, - { href: '/docs/components' }, - ]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(2); - expect(hrefs[0]).toBe('http://stenciljs.com/docs'); - expect(hrefs[1]).toBe('http://stenciljs.com/docs/components'); - }); - - it('skip different domains', () => { - parsedAnchors = [ - { href: '/' }, - { href: '/docs' }, - { href: 'https://stenciljs.com/' }, - { href: 'https://ionicframework.com/' }, - { href: 'https://ionicframework.com/docs' }, - { href: 'https://ionicons.com/' }, - ]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(2); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - expect(hrefs[1]).toBe('http://stenciljs.com/docs'); - }); - - it('skip targets that arent _self', () => { - parsedAnchors = [ - { href: '/docs', target: '_self' }, - { href: '/whatever', target: '_blank' }, - { href: '/about-us', target: 'custom-target' }, - ]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/docs'); - }); - - it('trim up hrefs', () => { - parsedAnchors = [{ href: '/ ' }, { href: ' /' }, { href: ' / ' }]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - }); - - it('disregard querystring', () => { - parsedAnchors = [{ href: '/?' }, { href: '/?some=querystring' }, { href: '/?some=querystring2' }]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - }); - - it('disregard hash', () => { - parsedAnchors = [{ href: '/#' }, { href: '/#some-hash' }, { href: '/#some-hash2' }]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - }); - - it('normalize https protocol', () => { - currentUrl = new URL('https://stenciljs.com/docs'); - parsedAnchors = [{ href: 'http://stenciljs.com/' }, { href: 'https://stenciljs.com/' }]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('https://stenciljs.com/'); - }); - - it('normalize protocol', () => { - currentUrl = new URL('http://stenciljs.com/docs'); - parsedAnchors = [{ href: 'http://stenciljs.com/' }, { href: 'https://stenciljs.com/' }]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - }); - - it('normalize /docs/index.htm', () => { - parsedAnchors = [{ href: '/docs/index.htm' }, { href: './docs/index.htm' }]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/docs'); - }); - - it('normalize index.html', () => { - parsedAnchors = [{ href: '/index.html' }, { href: './index.html' }]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(1); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - }); - - it('parse absolute paths', () => { - parsedAnchors = [{ href: 'http://stenciljs.com/' }, { href: 'http://stenciljs.com/docs' }]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(2); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - expect(hrefs[1]).toBe('http://stenciljs.com/docs'); - }); - - it('parse relative paths', () => { - parsedAnchors = [ - { href: '/' }, - { href: './' }, - { href: './docs/../docs/../' }, - { href: '/docs' }, - { href: '/docs/../' }, - { href: '/docs/..' }, - ]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(2); - expect(hrefs[0]).toBe('http://stenciljs.com/'); - expect(hrefs[1]).toBe('http://stenciljs.com/docs'); - }); - - it('do nothing for invalid hrefs', () => { - parsedAnchors = [ - { href: '' }, - { href: ' ' }, - { href: '#' }, - { href: '#some-hash' }, - { href: '?' }, - { href: '?some=querystring' }, - ]; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(0); - }); - - it('do nothing for empty array', () => { - parsedAnchors = []; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(0); - }); - - it('do nothing for invalid parsedAnchors', () => { - parsedAnchors = null; - - const hrefs = crawlAnchorsForNextUrls(prerenderConfig, diagnostics, baseUrl, currentUrl, parsedAnchors); - expect(diagnostics).toHaveLength(0); - - expect(hrefs).toHaveLength(0); - }); -}); diff --git a/src/compiler/prerender/test/tsconfig.json b/src/compiler/prerender/test/tsconfig.json deleted file mode 100644 index 3593ee977ed..00000000000 --- a/src/compiler/prerender/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/public.ts b/src/compiler/public.ts deleted file mode 100644 index c0d10ca916a..00000000000 --- a/src/compiler/public.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - Compiler, - CompilerBuildResults, - CompilerSystem, - CompilerWatcher, - CompileScriptMinifyOptions, - Config, - Diagnostic, - LoadConfigInit, - LoadConfigResults, - OptimizeCssInput, - OptimizeCssOutput, - OptimizeJsInput, - OptimizeJsOutput, - PlatformPath, - PrerenderResults, - PrerenderStartOptions, - TranspileOptions, - TranspileResults, -} from '@stencil/core/internal'; - -export { transpile, transpileSync } from './transpile'; - -/** - * The compiler is the utility that brings together many tools to build optimized components, - * such as a transpiler, bundler, and minifier, along with many internal optimizations to - * create small efficient components. When using the CLI, the `stencil build` command uses - * the compiler for the various builds, such as a production build, or watch mode during - * development. If only one file should be transformed then the `transpile()` function - * should be used instead. - * - * Given a Stencil config, this method asynchronously returns a `Compiler` instance. The - * config provided should already be created using the `loadConfig({...})` method. - */ -export declare const createCompiler: (config: Config) => Promise; - -export declare const createPrerenderer: ( - config: Config, -) => Promise<{ start: (opts: PrerenderStartOptions) => Promise }>; - -/** - * The compiler uses a `CompilerSystem` instance to access any file system reads and writes. - * When used from the CLI, the CLI will provide its own system based on NodeJS. This method - * provide a compiler system is in-memory only and independent of any platform. - */ -export declare const createSystem: () => CompilerSystem; - -/** - * The `dependencies` array is only informational and provided to state which versions of - * dependencies the compiler was built and works with. For example, the version of TypeScript, - * Rollup and Terser used for this version of Stencil are listed here. - */ -export declare const dependencies: CompilerDependency[]; -export interface CompilerDependency { - name: string; - version: string; - main: string; - resources?: string[]; -} - -/** - * The `loadConfig(init)` method is used to take raw config information and transform it into a - * usable config object for the compiler and dev-server. The `init` argument should be given - * an already created system and logger which can also be used by the compiler. - */ -export declare const loadConfig: (init?: LoadConfigInit) => Promise; - -/** - * Utility function used by the compiler to optimize CSS. - */ -export declare const optimizeCss: (cssInput?: OptimizeCssInput) => Promise; - -/** - * Utility function used by the compiler to optimize JavaScript. Knowing the JavaScript target - * will further apply minification optimizations beyond usual minification. - */ -export declare const optimizeJs: (jsInput?: OptimizeJsInput) => Promise; - -/** - * Utility of the `path` API provided by NodeJS, but capable of running in any environment. - */ -export declare const path: PlatformPath; - -/** - * Current version of `@stencil/core`. - */ -export declare const version: string; - -export declare const versions: { - stencil: string; - typescript: string; - rollup: string; - terser: string; -}; - -/** - * Current version's emoji :) - */ -export declare const vermoji: string; - -/** - * Compiler's unique build ID. - */ -export declare const buildId: string; - -export { - Compiler, - CompilerBuildResults, - CompilerSystem, - CompilerWatcher, - CompileScriptMinifyOptions, - Config, - Diagnostic, - LoadConfigInit, - LoadConfigResults, - OptimizeCssInput, - OptimizeCssOutput, - OptimizeJsInput, - OptimizeJsOutput, - TranspileOptions, - TranspileResults, -}; diff --git a/src/compiler/service-worker/service-worker-util.ts b/src/compiler/service-worker/service-worker-util.ts deleted file mode 100644 index 5bfdfa1ff7d..00000000000 --- a/src/compiler/service-worker/service-worker-util.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { relative } from '@utils'; - -import type * as d from '../../declarations'; - -export const generateServiceWorkerUrl = (outputTarget: d.OutputTargetWww, serviceWorker: d.ServiceWorkerConfig) => { - let swUrl = relative(outputTarget.appDir, serviceWorker.swDest); - - if (swUrl.charAt(0) !== '/') { - swUrl = '/' + swUrl; - } - - const baseUrl = new URL(outputTarget.baseUrl, 'http://config.stenciljs.com'); - let basePath = baseUrl.pathname; - if (!basePath.endsWith('/')) { - basePath += '/'; - } - - swUrl = basePath + swUrl.substring(1); - - return swUrl; -}; diff --git a/src/compiler/service-worker/test/service-worker-util.spec.ts b/src/compiler/service-worker/test/service-worker-util.spec.ts deleted file mode 100644 index b4102bcea3c..00000000000 --- a/src/compiler/service-worker/test/service-worker-util.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; - -import { validateConfig } from '../../config/validate-config'; -import { generateServiceWorkerUrl } from '../service-worker-util'; - -describe('generateServiceWorkerUrl', () => { - let userConfig: d.Config; - let outputTarget: d.OutputTargetWww; - - it('sw url w/ baseUrl', () => { - userConfig = mockConfig({ - devMode: false, - outputTargets: [ - { - type: 'www', - baseUrl: '/docs', - } as d.OutputTargetWww, - ], - }); - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - outputTarget = config.outputTargets[0] as d.OutputTargetWww; - const swUrl = generateServiceWorkerUrl(outputTarget, outputTarget.serviceWorker as d.ServiceWorkerConfig); - expect(swUrl).toBe('/docs/sw.js'); - }); - - it('default sw url', () => { - userConfig = mockConfig({ devMode: false }); - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - outputTarget = config.outputTargets[0] as d.OutputTargetWww; - const swUrl = generateServiceWorkerUrl(outputTarget, outputTarget.serviceWorker as d.ServiceWorkerConfig); - expect(swUrl).toBe('/sw.js'); - }); -}); diff --git a/src/compiler/service-worker/test/service-worker.spec.ts b/src/compiler/service-worker/test/service-worker.spec.ts deleted file mode 100644 index 86c8d74e843..00000000000 --- a/src/compiler/service-worker/test/service-worker.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -// @ts-nocheck -// TODO(STENCIL-462): investigate getting this file to pass (remove ts-nocheck) -import { Compiler, Config } from '@stencil/core/compiler'; -import type * as d from '@stencil/core/declarations'; -import { mockConfig } from '@stencil/core/testing'; -import path from 'path'; - -// TODO(STENCIL-462): investigate getting this file to pass -describe.skip('service worker', () => { - jest.setTimeout(20000); - let compiler: Compiler; - let config: Config; - const root = path.resolve('/'); - - it('dev service worker', async () => { - config = mockConfig({ - devMode: true, - outputTargets: [ - { - type: 'www', - serviceWorker: { - swSrc: path.join('src', 'sw.js'), - globPatterns: ['**/*.{html,js,css,json,ico,png}'], - }, - } as d.OutputTargetWww, - ], - }); - - compiler = new Compiler(config); - await compiler.fs.writeFile(path.join(root, 'www', 'script.js'), `/**/`); - await compiler.fs.writeFile(path.join(root, 'src', 'index.html'), ``); - await compiler.fs.writeFile( - path.join(root, 'src', 'components', 'cmp-a', 'cmp-a.tsx'), - ` - @Component({ tag: 'cmp-a' }) export class CmpA { render() { return

    cmp-a

    ; } } - `, - ); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toEqual([]); - - const indexHtml = await compiler.fs.readFile(path.join(root, 'www', 'index.html')); - expect(indexHtml).toContain(`registration.unregister()`); - }); -}); diff --git a/src/compiler/service-worker/test/tsconfig.json b/src/compiler/service-worker/test/tsconfig.json deleted file mode 100644 index 0ff0be83173..00000000000 --- a/src/compiler/service-worker/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/style/css-parser/readme.md b/src/compiler/style/css-parser/readme.md deleted file mode 100644 index a0c7a965d70..00000000000 --- a/src/compiler/style/css-parser/readme.md +++ /dev/null @@ -1,13 +0,0 @@ -Forked from https://github.com/reworkcss/css -Ported to ESM and Typed -Modified so to remove selectors that are not used in HTML - -(The MIT License) - -Copyright (c) 2012 TJ Holowaychuk - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/compiler/style/global-styles.ts b/src/compiler/style/global-styles.ts deleted file mode 100644 index d77b8946344..00000000000 --- a/src/compiler/style/global-styles.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { catchError, isOutputTargetDistGlobalStyles, normalizePath } from '@utils'; - -import type * as d from '../../declarations'; -import { runPluginTransforms } from '../plugin/plugin'; -import { getCssImports } from './css-imports'; -import { optimizeCss } from './optimize-css'; - -export const generateGlobalStyles = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -) => { - const outputTargets = config.outputTargets.filter(isOutputTargetDistGlobalStyles); - if (outputTargets.length === 0) { - return ''; - } - - const globalStyles = await buildGlobalStyles(config, compilerCtx, buildCtx); - if (globalStyles) { - await Promise.all(outputTargets.map((o) => compilerCtx.fs.writeFile(o.file, globalStyles))); - } - - return globalStyles; -}; - -const buildGlobalStyles = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - let globalStylePath = config.globalStyle; - if (!globalStylePath) { - return null; - } - - const canSkip = await canSkipGlobalStyles(config, compilerCtx, buildCtx); - if (canSkip) { - return compilerCtx.cachedGlobalStyle; - } - - try { - globalStylePath = normalizePath(globalStylePath); - compilerCtx.addWatchFile(globalStylePath); - - const transformResults = await runPluginTransforms(config, compilerCtx, buildCtx, globalStylePath); - - if (transformResults) { - let cssCode: string; - let dependencies: string[] | undefined; - - if (typeof transformResults === 'string') { - // Handle case where transformResults is a string (the CSS code directly) - cssCode = transformResults; - dependencies = undefined; - } else if (typeof transformResults === 'object' && transformResults.code) { - // Handle case where transformResults is a PluginTransformationDescriptor object - cssCode = transformResults.code; - dependencies = transformResults.dependencies; - } else { - // Invalid transformResults - compilerCtx.cachedGlobalStyle = null; - return null; - } - - const optimizedCss = await optimizeCss(config, compilerCtx, buildCtx.diagnostics, cssCode, globalStylePath); - compilerCtx.cachedGlobalStyle = optimizedCss; - - if (Array.isArray(dependencies)) { - const cssModuleImports = compilerCtx.cssModuleImports.get(globalStylePath) || []; - dependencies.forEach((dep: string) => { - compilerCtx.addWatchFile(dep); - if (!cssModuleImports.includes(dep)) { - cssModuleImports.push(dep); - } - }); - compilerCtx.cssModuleImports.set(globalStylePath, cssModuleImports); - } - - // Track global style changes for HMR - if (buildCtx.isRebuild && config.devServer?.reloadStrategy === 'hmr') { - buildCtx.stylesUpdated.push({ - styleTag: 'global', - styleMode: undefined, - styleText: optimizedCss, - }); - } - - return optimizedCss; - } - } catch (e: any) { - const d = catchError(buildCtx.diagnostics, e); - d.absFilePath = globalStylePath; - } - - compilerCtx.cachedGlobalStyle = null; - return null; -}; - -const canSkipGlobalStyles = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - if (!compilerCtx.cachedGlobalStyle) { - return false; - } - - if (buildCtx.requiresFullBuild) { - return false; - } - - if (buildCtx.isRebuild && !buildCtx.hasStyleChanges) { - return true; - } - - if (buildCtx.filesChanged.includes(config.globalStyle)) { - // changed file IS the global entry style - return false; - } - - const cssModuleImports = compilerCtx.cssModuleImports.get(config.globalStyle); - if (cssModuleImports && buildCtx.filesChanged.some((f) => cssModuleImports.includes(f))) { - return false; - } - - const hasChangedImports = await hasChangedImportFile( - config, - compilerCtx, - buildCtx, - config.globalStyle, - compilerCtx.cachedGlobalStyle, - [], - ); - if (hasChangedImports) { - return false; - } - - return true; -}; - -const hasChangedImportFile = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - filePath: string, - content: string, - noLoop: string[], -): Promise => { - if (noLoop.includes(filePath)) { - return false; - } - noLoop.push(filePath); - - return hasChangedImportContent(config, compilerCtx, buildCtx, filePath, content, noLoop); -}; - -const hasChangedImportContent = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - filePath: string, - content: string, - checkedFiles: string[], -) => { - const cssImports = await getCssImports(config, compilerCtx, buildCtx, filePath, content); - if (cssImports.length === 0) { - // don't bother - return false; - } - - const isChangedImport = buildCtx.filesChanged.some((changedFilePath) => { - return cssImports.some((c) => c.filePath === changedFilePath); - }); - - if (isChangedImport) { - // one of the changed files is an import of this file - return true; - } - - // keep digging - const promises = cssImports.map(async (cssImportData) => { - try { - const content = await compilerCtx.fs.readFile(cssImportData.filePath); - return hasChangedImportFile(config, compilerCtx, buildCtx, cssImportData.filePath, content, checkedFiles); - } catch (e) { - return false; - } - }); - - const results = await Promise.all(promises); - - return results.includes(true); -}; diff --git a/src/compiler/style/normalize-styles.ts b/src/compiler/style/normalize-styles.ts deleted file mode 100644 index b43af888c73..00000000000 --- a/src/compiler/style/normalize-styles.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { DEFAULT_STYLE_MODE, join, normalizePath, relative } from '@utils'; -import { dirname, isAbsolute } from 'path'; - -import type * as d from '../../declarations'; - -export const normalizeStyles = (tagName: string, componentFilePath: string, styles: d.StyleCompiler[]) => { - styles.forEach((style) => { - if (style.modeName === DEFAULT_STYLE_MODE) { - style.styleId = tagName.toUpperCase(); - } else { - style.styleId = `${tagName.toUpperCase()}#${style.modeName}`; - } - - if (Array.isArray(style.externalStyles)) { - style.externalStyles.forEach((externalStyle) => { - normalizeExternalStyle(componentFilePath, externalStyle); - }); - } - }); -}; - -const normalizeExternalStyle = (componentFilePath: string, externalStyle: d.ExternalStyleCompiler) => { - if ( - typeof externalStyle.originalComponentPath !== 'string' || - externalStyle.originalComponentPath.trim().length === 0 - ) { - return; - } - - // get the absolute path of the directory which the component is sitting in - const componentDir = dirname(componentFilePath); - - if (isAbsolute(externalStyle.originalComponentPath)) { - // this path is absolute already! - // add to our list of style absolute paths - externalStyle.absolutePath = normalizePath(externalStyle.originalComponentPath); - - // if this is an absolute path already, let's convert it to be relative - externalStyle.relativePath = normalizePath(relative(componentDir, externalStyle.originalComponentPath)); - } else { - // this path is relative to the component - // add to our list of style relative paths - externalStyle.relativePath = normalizePath(externalStyle.originalComponentPath); - - // create the absolute path to the style file - externalStyle.absolutePath = normalizePath(join(componentDir, externalStyle.originalComponentPath)); - } -}; diff --git a/src/compiler/style/optimize-css.ts b/src/compiler/style/optimize-css.ts deleted file mode 100644 index c8145a6bea5..00000000000 --- a/src/compiler/style/optimize-css.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { hasError, normalizePath } from '@utils'; - -import type * as d from '../../declarations'; -import { optimizeCssId } from '../../version'; - -export const optimizeCss = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - diagnostics: d.Diagnostic[], - styleText: string, - // TODO(STENCIL-1076): Investigate removing this parameter, which appears to be unused. This function is exported by - // the compiler, making this a breaking change should we remove it. - filePath: string, -) => { - if (typeof styleText !== 'string' || !styleText.length) { - // don't bother with invalid data - return styleText; - } - - if ((config.autoprefixCss === false || config.autoprefixCss === null) && !config.minifyCss) { - // don't wanna autoprefix or minify, so just skip this - return styleText; - } - - if (typeof filePath === 'string') { - filePath = normalizePath(filePath); - } - - const opts: d.OptimizeCssInput = { - input: styleText, - filePath: filePath, - autoprefixer: config.autoprefixCss, - minify: config.minifyCss, - }; - - const cacheKey = await compilerCtx.cache.createKey('optimizeCss', optimizeCssId, opts); - const cachedContent = await compilerCtx.cache.get(cacheKey); - if (cachedContent != null) { - // let's use the cached data we already figured out - return cachedContent; - } - - const minifyResults = await compilerCtx.worker!.optimizeCss(opts); - minifyResults.diagnostics.forEach((d) => { - // collect up any diagnostics from minifying - diagnostics.push(d); - }); - - if (typeof minifyResults.output === 'string' && !hasError(diagnostics)) { - // cool, we got valid minified output - - // only cache if we got a cache key, if not it probably has an @import - await compilerCtx.cache.put(cacheKey, minifyResults.output); - - return minifyResults.output; - } - - return styleText; -}; diff --git a/src/compiler/style/test/build-conditionals.spec.ts b/src/compiler/style/test/build-conditionals.spec.ts deleted file mode 100644 index d14a60460b3..00000000000 --- a/src/compiler/style/test/build-conditionals.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -// @ts-nocheck -// TODO(STENCIL-463): as part of getting these tests to pass, remove // @ts-nocheck -import { Compiler, Config } from '@stencil/core/compiler'; -import { mockConfig } from '@stencil/core/testing'; -import path from 'path'; - -// TODO(STENCIL-463): investigate getting these tests to pass again -describe.skip('build-conditionals', () => { - jest.setTimeout(20000); - let compiler: Compiler; - let config: Config; - const root = path.resolve('/'); - - beforeEach(async () => { - config = mockConfig(); - compiler = new Compiler(config); - await compiler.fs.writeFile(path.join(root, 'src', 'index.html'), ``); - await compiler.fs.commit(); - }); - - it('should import function svg/slot build conditionals, remove on rebuild, and add back on rebuild', async () => { - compiler.config.watch = true; - await compiler.fs.writeFiles({ - [path.join(root, 'src', 'cmp-a.tsx')]: ` - import {icon, slot} from './icon'; - @Component({ tag: 'cmp-a', shadow: true }) export class CmpA { - render() { - return
    {icon()}{slot()}
    - } - }`, - [path.join(root, 'src', 'slot.tsx')]: ` - export default () => ; - `, - [path.join(root, 'src', 'icon.tsx')]: ` - import slot from './slot'; - export const icon = () => ; - export { slot }; - `, - }); - await compiler.fs.commit(); - - let r = await compiler.build(); - let rebuildListener = compiler.once('buildFinish'); - - expect(r.diagnostics).toHaveLength(0); - expect(r.buildConditionals).toEqual({ - shadow: true, - slot: true, - svg: true, - vdom: true, - }); - - await compiler.fs.writeFiles( - { - [path.join(root, 'src', 'cmp-a.tsx')]: `@Component({ tag: 'cmp-a' }) export class CmpA {}`, - }, - { clearFileCache: true }, - ); - await compiler.fs.commit(); - - compiler.trigger('fileUpdate', path.join(root, 'src', 'cmp-a.tsx')); - - r = await rebuildListener; - - expect(r.diagnostics).toHaveLength(0); - expect(r.buildConditionals).toEqual({ - shadow: false, - slot: false, - svg: false, - vdom: false, - }); - - await compiler.fs.writeFiles( - { - [path.join(root, 'src', 'cmp-a.tsx')]: ` - import {icon, slot} from './icon'; - @Component({ tag: 'cmp-a', shadow: true }) export class CmpA { - render() { - return
    {icon()}{slot()}
    - } - }`, - }, - { clearFileCache: true }, - ); - await compiler.fs.commit(); - - rebuildListener = compiler.once('buildFinish'); - - compiler.trigger('fileUpdate', path.join(root, 'src', 'cmp-a.tsx')); - - r = await rebuildListener; - - expect(r.diagnostics).toHaveLength(0); - expect(r.buildConditionals).toEqual({ - shadow: true, - slot: true, - svg: true, - vdom: true, - }); - }); - - it('should set slot build conditionals, not import unused svg import', async () => { - await compiler.fs.writeFiles({ - [path.join(root, 'src', 'cmp-a.tsx')]: ` - import icon from './icon'; - @Component({ tag: 'cmp-a', shadow: true }) export class CmpA { - render() { - return
    - } - }`, - [path.join(root, 'src', 'icon.tsx')]: ` - export default () => ; - `, - }); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - expect(r.buildConditionals).toEqual({ - shadow: true, - slot: true, - svg: false, - vdom: true, - }); - }); - - it('should set slot build conditionals', async () => { - await compiler.fs.writeFiles({ - [path.join(root, 'src', 'cmp-a.tsx')]: `@Component({ tag: 'cmp-a' }) export class CmpA { - render() { - return
    - } - }`, - }); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - expect(r.buildConditionals).toEqual({ - shadow: false, - slot: true, - svg: false, - vdom: true, - }); - }); - - it('should set vdom build conditionals', async () => { - await compiler.fs.writeFiles({ - [path.join(root, 'src', 'cmp-a.tsx')]: `@Component({ tag: 'cmp-a' }) export class CmpA { - render() { - return
    Hello World
    - } - }`, - }); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - expect(r.buildConditionals).toEqual({ - shadow: false, - slot: false, - svg: false, - vdom: true, - }); - }); - - it('should not set vdom build conditionals', async () => { - await compiler.fs.writeFiles({ - [path.join(root, 'src', 'cmp-a.tsx')]: `@Component({ tag: 'cmp-a' }) export class CmpA { - render() { - return 'Hello World'; - } - }`, - }); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - expect(r.buildConditionals).toEqual({ - shadow: false, - slot: false, - svg: false, - vdom: false, - }); - }); -}); diff --git a/src/compiler/style/test/style.spec.ts b/src/compiler/style/test/style.spec.ts deleted file mode 100644 index 55dc340ddbd..00000000000 --- a/src/compiler/style/test/style.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -// @ts-nocheck -/* eslint-disable jest/no-test-prefixes, jest/no-commented-out-tests -- this file needs to be brought up to date at some point */ -// TODO(STENCIL-464): remove // @ts-nocheck as part of getting these tests to pass -import { Compiler, Config } from '@stencil/core/compiler'; -import { mockConfig } from '@stencil/core/testing'; -import path from 'path'; - -// TODO(STENCIL-464): investigate getting these tests to run again -xdescribe('component-styles', () => { - jest.setTimeout(20000); - let compiler: Compiler; - let config: Config; - const root = path.resolve('/'); - - beforeEach(async () => { - config = mockConfig({ - minifyCss: true, - minifyJs: true, - hashFileNames: true, - }); - compiler = new Compiler(config); - await compiler.fs.writeFile(path.join(root, 'src', 'index.html'), ``); - await compiler.fs.commit(); - }); - - it('should add mode styles to hashed filename/minified builds', async () => { - compiler.config.hashedFileNameLength = 2; - await compiler.fs.writeFiles({ - [path.join(root, 'src', 'cmp-a.tsx')]: `@Component({ - tag: 'cmp-a', - styleUrls: { - ios: 'cmp-a.ios.css', - md: 'cmp-a.md.css' - } - }) - export class CmpA {}`, - - [path.join(root, 'src', 'cmp-a.ios.css')]: `body{font-family:Helvetica}`, - [path.join(root, 'src', 'cmp-a.md.css')]: `body{font-family:Roboto}`, - }); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - - let hasIos = false; - let hasMd = false; - - r.filesWritten.forEach((f) => { - const content = compiler.fs.readFileSync(f); - if (content.includes(`body{font-family:Helvetica}`)) { - hasIos = true; - } else if (content.includes(`body{font-family:Roboto}`)) { - hasMd = true; - } - }); - - expect(hasIos).toBe(true); - expect(hasMd).toBe(true); - }); - - it('should add default styles to hashed filename/minified builds', async () => { - compiler.config.sys.generateContentHash = function () { - return 'hashed'; - }; - - await compiler.fs.writeFiles({ - [path.join(root, 'src', 'cmp-a.tsx')]: `@Component({ tag: 'cmp-a', styleUrl: 'cmp-a.css' }) export class CmpA {}`, - [path.join(root, 'src', 'cmp-a.css')]: `body{color:red}`, - }); - await compiler.fs.commit(); - - const r = await compiler.build(); - expect(r.diagnostics).toHaveLength(0); - - const content = await compiler.fs.readFile(path.join(root, 'www', 'build', 'p-hashed.entry.js')); - expect(content).toContain(`body{color:red}`); - }); -}); diff --git a/src/compiler/style/test/tsconfig.json b/src/compiler/style/test/tsconfig.json deleted file mode 100644 index 0ff0be83173..00000000000 --- a/src/compiler/style/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/sys/config.ts b/src/compiler/sys/config.ts deleted file mode 100644 index 55f5d6c7224..00000000000 --- a/src/compiler/sys/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createNodeLogger } from '@sys-api-node'; - -import { createConfigFlags } from '../../cli/config-flags'; -import type * as d from '../../declarations'; -import { validateConfig } from '../config/validate-config'; - -/** - * Given a user-supplied config, get a validated config which can be used to - * start building a Stencil project. - * - * @param userConfig a configuration object - * @returns a validated config object with stricter typing - */ -export const getConfig = (userConfig: d.Config): d.ValidatedConfig => { - userConfig.logger = userConfig.logger ?? createNodeLogger(); - const flags = createConfigFlags(userConfig.flags ?? {}); - userConfig.flags = flags; - const config: d.ValidatedConfig = validateConfig(userConfig, {}).config; - - return config; -}; diff --git a/src/compiler/sys/environment.ts b/src/compiler/sys/environment.ts deleted file mode 100644 index 2c995bab72c..00000000000 --- a/src/compiler/sys/environment.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const IS_WINDOWS_ENV = process.platform === 'win32'; - -export const IS_CASE_SENSITIVE_FILE_NAMES = !IS_WINDOWS_ENV; diff --git a/src/compiler/sys/fetch/fetch-module-sync.ts b/src/compiler/sys/fetch/fetch-module-sync.ts deleted file mode 100644 index ae0060be357..00000000000 --- a/src/compiler/sys/fetch/fetch-module-sync.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { isString } from '@utils'; - -import type * as d from '../../../declarations'; -import { InMemoryFileSystem } from '../in-memory-fs'; -import { known404Urls } from './fetch-utils'; -import { skipFilePathFetch, skipUrlFetch } from './fetch-utils'; -import { writeFetchSuccessSync } from './write-fetch-success'; - -export const fetchModuleSync = ( - sys: d.CompilerSystem, - inMemoryFs: InMemoryFileSystem, - pkgVersions: Map, - url: string, - filePath: string, -) => { - if (skipFilePathFetch(filePath)) { - return undefined; - } - - const content = fetchUrlSync(url); - if (isString(content)) { - writeFetchSuccessSync(sys, inMemoryFs, url, filePath, content, pkgVersions); - } - - return content; -}; - -export const fetchUrlSync = (url: string) => { - if (known404Urls.has(url) || skipUrlFetch(url)) { - return undefined; - } - - try { - const xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.send(null); - - if (xhr.status >= 200 && xhr.status <= 299) { - return xhr.responseText; - } - } catch (e) {} - - known404Urls.add(url); - - return undefined; -}; diff --git a/src/compiler/sys/fetch/write-fetch-success.ts b/src/compiler/sys/fetch/write-fetch-success.ts deleted file mode 100644 index 0330cd4f233..00000000000 --- a/src/compiler/sys/fetch/write-fetch-success.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { dirname } from 'path'; - -import type * as d from '../../../declarations'; -import { InMemoryFileSystem } from '../in-memory-fs'; -import { setPackageVersionByContent } from '../resolve/resolve-utils'; - -export const writeFetchSuccessSync = ( - sys: d.CompilerSystem, - inMemoryFs: InMemoryFileSystem, - url: string, - filePath: string, - content: string, - pkgVersions: Map, -) => { - if (url.endsWith('package.json')) { - setPackageVersionByContent(pkgVersions, content); - } - - let dir = dirname(filePath); - while (dir !== '/' && dir !== '') { - if (inMemoryFs) { - inMemoryFs.clearFileCache(dir); - inMemoryFs.sys.createDirSync(dir); - } else { - sys.createDirSync(dir); - } - - dir = dirname(dir); - } - - if (inMemoryFs) { - inMemoryFs.clearFileCache(filePath); - inMemoryFs.sys.writeFileSync(filePath, content); - } else { - sys.writeFileSync(filePath, content); - } -}; - -export const writeFetchSuccessAsync = async ( - sys: d.CompilerSystem, - inMemoryFs: InMemoryFileSystem, - url: string, - filePath: string, - content: string, - pkgVersions: Map, -) => { - if (url.endsWith('package.json')) { - setPackageVersionByContent(pkgVersions, content); - } - - let dir = dirname(filePath); - while (dir !== '/' && dir !== '') { - if (inMemoryFs) { - inMemoryFs.clearFileCache(dir); - await inMemoryFs.sys.createDir(dir); - } else { - await sys.createDir(dir); - } - - dir = dirname(dir); - } - - if (inMemoryFs) { - inMemoryFs.clearFileCache(filePath); - await inMemoryFs.sys.writeFile(filePath, content); - } else { - await sys.writeFile(filePath, content); - } -}; diff --git a/src/compiler/sys/node-require.ts b/src/compiler/sys/node-require.ts deleted file mode 100644 index 5282a5e024c..00000000000 --- a/src/compiler/sys/node-require.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { catchError, loadTypeScriptDiagnostic } from '@utils'; -import ts from 'typescript'; - -import type { Diagnostic } from '../../declarations'; - -export const nodeRequire = (id: string) => { - const results = { - module: undefined as any, - id, - diagnostics: [] as Diagnostic[], - }; - - try { - const fs: typeof import('fs') = require('fs'); - const path: typeof import('path') = require('path'); - - results.id = path.resolve(id); - - // ensure we cleared out node's internal require() cache for this file - delete require.cache[results.id]; - - // let's override node's require for a second - // don't worry, we'll revert this when we're done - require.extensions['.ts'] = (module: NodeJS.Module, fileName: string) => { - let sourceText = fs.readFileSync(fileName, 'utf8'); - - if (fileName.endsWith('.ts')) { - // looks like we've got a typed config file - // let's transpile it to .js quick - const tsResults = ts.transpileModule(sourceText, { - fileName, - compilerOptions: { - module: ts.ModuleKind.CommonJS, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - esModuleInterop: true, - target: ts.ScriptTarget.ES2017, - allowJs: true, - }, - }); - sourceText = tsResults.outputText; - - results.diagnostics.push(...tsResults.diagnostics.map(loadTypeScriptDiagnostic)); - } else { - // quick hack to turn a modern es module - // into and old school commonjs module - sourceText = sourceText.replace(/export\s+\w+\s+(\w+)/gm, 'exports.$1'); - } - - try { - // we need to coerce because of the requirements for the arguments to - // this function. It's safe enough since it's already wrapped in a - // `try { } catch`. - (module as NodeModuleWithCompile)._compile(sourceText, fileName); - } catch (e: any) { - catchError(results.diagnostics, e); - } - }; - - // let's do this! - results.module = require(results.id); - - // all set, let's go ahead and reset the require back to the default - require.extensions['.ts'] = undefined; - } catch (e: any) { - catchError(results.diagnostics, e); - } - - return results; -}; - -interface NodeModuleWithCompile extends NodeModule { - _compile(code: string, filename: string): any; -} diff --git a/src/compiler/sys/resolve/resolve-module-sync.ts b/src/compiler/sys/resolve/resolve-module-sync.ts deleted file mode 100644 index 0b484aa497d..00000000000 --- a/src/compiler/sys/resolve/resolve-module-sync.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { isString, normalizeFsPath, normalizePath } from '@utils'; -import { dirname } from 'path'; -import resolve, { SyncOpts } from 'resolve'; - -import type * as d from '../../../declarations'; -import { InMemoryFileSystem } from '../in-memory-fs'; - -export const resolveRemoteModuleIdSync = ( - config: d.Config, - inMemoryFs: InMemoryFileSystem, - opts: d.ResolveModuleIdOptions, -) => { - const packageJson = resolveRemotePackageJsonSync(config, inMemoryFs, opts.moduleId); - if (packageJson) { - const resolveModuleSyncOpts: d.ResolveModuleIdOptions = { - ...opts, - exts: ['.js', '.mjs'], - }; - const resolvedUrl = resolveModuleIdSync(config.sys, inMemoryFs, resolveModuleSyncOpts); - if (typeof resolvedUrl === 'string') { - return { - resolvedUrl, - packageJson, - }; - } - } - return null; -}; - -const resolveRemotePackageJsonSync = (config: d.Config, inMemoryFs: InMemoryFileSystem, moduleId: string) => { - if (inMemoryFs) { - const filePath = normalizePath( - config.sys.getLocalModulePath({ rootDir: config.rootDir, moduleId, path: 'package.json' }), - ); - const pkgJson = inMemoryFs.readFileSync(filePath); - if (typeof pkgJson === 'string') { - try { - return JSON.parse(pkgJson) as d.PackageJsonData; - } catch (e) {} - } - } - return null; -}; - -export const resolveModuleIdSync = ( - sys: d.CompilerSystem, - inMemoryFs: InMemoryFileSystem, - opts: d.ResolveModuleIdOptions, -) => { - if (inMemoryFs) { - const resolverOpts = createCustomResolverSync(sys, inMemoryFs, opts.exts); - resolverOpts.basedir = dirname(opts.containingFile); - resolverOpts.packageFilter = opts.packageFilter; - - const resolvedModule = resolve.sync(opts.moduleId, resolverOpts); - return resolvedModule; - } - return null; -}; - -export const createCustomResolverSync = ( - sys: d.CompilerSystem, - inMemoryFs: InMemoryFileSystem, - exts: string[], -): SyncOpts => { - return { - isFile(filePath: string) { - const fsFilePath = normalizeFsPath(filePath); - - const stat = inMemoryFs.statSync(fsFilePath); - return stat.isFile; - }, - - isDirectory(dirPath: string) { - const fsDirPath = normalizeFsPath(dirPath); - - const stat = inMemoryFs.statSync(fsDirPath); - return stat.isDirectory; - }, - - readFileSync(p: string) { - const data = inMemoryFs.readFileSync(p); - if (isString(data)) { - return data; - } - - throw new Error(`file not found: ${p}`); - }, - - realpathSync(p: string) { - const fsFilePath = normalizeFsPath(p); - try { - return sys.realpathSync(fsFilePath); - } catch (realpathErr: unknown) { - if (isErrnoException(realpathErr)) { - if (realpathErr.code !== 'ENOENT') { - throw realpathErr; - } - } - } - return fsFilePath; - }, - - extensions: exts, - } as any; -}; - -/** - * Type guard to determine if an Error is an instance of `ErrnoException`. For the purposes of this type guard, we - * must ensure that the `code` field is present. This type guard was written with the `ErrnoException` definition from - * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/d121716ed123957f6a86f8985eb013fcaddab345/types/node/globals.d.ts#L183-L188 - * in mind. - * @param err the entity to check the type of - * @returns true if the provided value is an instance of `ErrnoException`, `false` otherwise - */ -function isErrnoException(err: unknown): err is NodeJS.ErrnoException { - return err instanceof Error && err.hasOwnProperty('code'); -} diff --git a/src/compiler/sys/resolve/resolve-utils.ts b/src/compiler/sys/resolve/resolve-utils.ts deleted file mode 100644 index 5921fc1571b..00000000000 --- a/src/compiler/sys/resolve/resolve-utils.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { normalizePath } from '@utils'; - -import type * as d from '../../../declarations'; - -const COMMON_DIR_MODULE_EXTS = ['.tsx', '.ts', '.mts', '.cts', '.mjs', '.js', '.cjs', '.jsx', '.json', '.md']; - -export const isCommonDirModuleFile = (p: string) => COMMON_DIR_MODULE_EXTS.some((ext) => p.endsWith(ext)); - -export const setPackageVersion = (pkgVersions: Map, pkgName: string, pkgVersion: string) => { - pkgVersions.set(pkgName, pkgVersion); -}; - -export const setPackageVersionByContent = (pkgVersions: Map, pkgContent: string) => { - try { - const pkg = JSON.parse(pkgContent) as d.PackageJsonData; - if (pkg.name && pkg.version) { - setPackageVersion(pkgVersions, pkg.name, pkg.version); - } - } catch (e) {} -}; - -export const isLocalModule = (p: string) => p.startsWith('.') || p.startsWith('/'); - -export const isStencilCoreImport = (p: string) => p.startsWith('@stencil/core'); - -export const isNodeModulePath = (p: string) => normalizePath(p).split('/').includes('node_modules'); - -export const getModuleId = (orgImport: string) => { - if (orgImport.startsWith('~')) { - orgImport = orgImport.substring(1); - } - const splt = orgImport.split('/'); - const m = { - moduleId: null as string, - filePath: null as string, - scope: null as string, - scopeSubModuleId: null as string, - }; - - if (orgImport.startsWith('@') && splt.length > 1) { - m.moduleId = splt.slice(0, 2).join('/'); - m.filePath = splt.slice(2).join('/'); - m.scope = splt[0]; - m.scopeSubModuleId = splt[1]; - } else { - m.moduleId = splt[0]; - m.filePath = splt.slice(1).join('/'); - } - - return m; -}; - -export const getPackageDirPath = (p: string, moduleId: string) => { - const parts = normalizePath(p).split('/'); - const m = getModuleId(moduleId); - for (let i = parts.length - 1; i >= 1; i--) { - if (parts[i - 1] === 'node_modules') { - if (m.scope) { - if (parts[i] === m.scope && parts[i + 1] === m.scopeSubModuleId) { - return parts.slice(0, i + 2).join('/'); - } - } else if (parts[i] === m.moduleId) { - return parts.slice(0, i + 1).join('/'); - } - } - } - return null; -}; diff --git a/src/compiler/sys/typescript/tests/typescript-config.spec.ts b/src/compiler/sys/typescript/tests/typescript-config.spec.ts deleted file mode 100644 index 75ef85f7e53..00000000000 --- a/src/compiler/sys/typescript/tests/typescript-config.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import ts from 'typescript'; - -import { ValidatedConfig } from '../../../../declarations'; -import { mockValidatedConfig } from '../../../../testing/mocks'; -import { createTestingSystem, TestingSystem } from '../../../../testing/testing-sys'; -import * as tsConfig from '../typescript-config'; - -describe('typescript-config', () => { - describe('hasSrcDirectoryInclude', () => { - it('returns `false` for a non-array argument', () => { - // the intent of this test is to evaluate when a user doesn't provide an array, hence the type assertion - expect(tsConfig.hasSrcDirectoryInclude('src' as unknown as string[], 'src')).toBe(false); - }); - - it('returns `false` for an empty array', () => { - expect(tsConfig.hasSrcDirectoryInclude([], 'src/')).toBe(false); - }); - - it('returns `false` when an entry does not exist in the array', () => { - expect(tsConfig.hasSrcDirectoryInclude(['src'], 'source')).toBe(false); - }); - - it('returns `true` when an entry does exist in the array', () => { - expect(tsConfig.hasSrcDirectoryInclude(['src', 'foo'], 'src')).toBe(true); - }); - - it('returns `true` for globs', () => { - expect(tsConfig.hasSrcDirectoryInclude(['src/**/*.ts', 'foo/'], 'src/**/*.ts')).toBe(true); - }); - - it.each([ - [['src'], './src'], - [['./src'], 'src'], - [['../src'], '../src'], - [['*'], './*'], - ])('returns `true` for relative paths', (includedPaths, srcDir) => { - expect(tsConfig.hasSrcDirectoryInclude(includedPaths, srcDir)).toBe(true); - }); - }); - - describe('validateTsConfig', () => { - let mockSys: TestingSystem; - let config: ValidatedConfig; - let tsSpy: jest.SpyInstance; - - beforeEach(() => { - mockSys = createTestingSystem(); - config = mockValidatedConfig(); - - jest.spyOn(tsConfig, 'getTsConfigPath').mockResolvedValue({ - path: 'tsconfig.json', - content: '', - }); - tsSpy = jest.spyOn(ts, 'getParsedCommandLineOfConfigFile'); - }); - - it('includes watchOptions when provided', async () => { - tsSpy.mockReturnValueOnce({ - watchOptions: { - excludeFiles: ['exclude.ts'], - excludeDirectories: ['exclude-dir'], - }, - options: null, - fileNames: [], - errors: [], - }); - - const result = await tsConfig.validateTsConfig(config, mockSys, {}); - expect(result.watchOptions).toEqual({ - excludeFiles: ['exclude.ts'], - excludeDirectories: ['exclude-dir'], - }); - }); - - it('does not include watchOptions when not provided', async () => { - tsSpy.mockReturnValueOnce({ - options: null, - fileNames: [], - errors: [], - }); - - const result = await tsConfig.validateTsConfig(config, mockSys, {}); - expect(result.watchOptions).toEqual({}); - }); - }); -}); diff --git a/src/compiler/sys/typescript/typescript-resolve-module.ts b/src/compiler/sys/typescript/typescript-resolve-module.ts deleted file mode 100644 index 473716425a3..00000000000 --- a/src/compiler/sys/typescript/typescript-resolve-module.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { isDtsFile, isJsFile, isJsxFile, isString, isTsFile, isTsxFile, join, normalizePath, resolve } from '@utils'; -import { basename, dirname } from 'path'; -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { patchTsSystemFileSystem } from './typescript-sys'; - -export const tsResolveModuleName = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - moduleName: string, - containingFile: string, -) => { - const resolveModuleName: typeof ts.resolveModuleName = (ts as any).__resolveModuleName || ts.resolveModuleName; - - if (moduleName && resolveModuleName && config.tsCompilerOptions) { - const host: ts.ModuleResolutionHost = patchTsSystemFileSystem(config, config.sys, compilerCtx.fs, ts.sys); - - const compilerOptions: ts.CompilerOptions = { ...config.tsCompilerOptions }; - compilerOptions.resolveJsonModule = true; - return resolveModuleName(moduleName, containingFile, compilerOptions, host); - } - - return null; -}; - -export const tsGetSourceFile = (config: d.ValidatedConfig, module: ts.ResolvedModuleWithFailedLookupLocations) => { - if (!module || !module.resolvedModule) { - return null; - } - const compilerOptions: ts.CompilerOptions = { ...config.tsCompilerOptions }; - const host = ts.createCompilerHost(compilerOptions); - const program = ts.createProgram([module.resolvedModule.resolvedFileName], compilerOptions, host); - return program.getSourceFile(module.resolvedModule.resolvedFileName); -}; - -export const tsResolveModuleNamePackageJsonPath = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - moduleName: string, - containingFile: string, -) => { - try { - const resolvedModule = tsResolveModuleName(config, compilerCtx, moduleName, containingFile); - if (resolvedModule && resolvedModule.resolvedModule && resolvedModule.resolvedModule.resolvedFileName) { - const rootDir = resolve('/'); - let resolvedFileName = resolvedModule.resolvedModule.resolvedFileName; - - for (let i = 0; i < 30; i++) { - if (rootDir === resolvedFileName) { - return null; - } - resolvedFileName = dirname(resolvedFileName); - const pkgJsonPath = join(resolvedFileName, 'package.json'); - const exists = config.sys.accessSync(pkgJsonPath); - if (exists) { - return normalizePath(pkgJsonPath); - } - } - } - } catch (e) { - config.logger.error(e); - } - return null; -}; - -export const ensureExtension = (fileName: string, containingFile: string) => { - if (!basename(fileName).includes('.') && isString(containingFile)) { - containingFile = containingFile.toLowerCase(); - if (isJsFile(containingFile)) { - fileName += '.js'; - } else if (isDtsFile(containingFile)) { - fileName += '.d.ts'; - } else if (isTsxFile(containingFile)) { - fileName += '.tsx'; - } else if (isTsFile(containingFile)) { - fileName += '.ts'; - } else if (isJsxFile(containingFile)) { - fileName += '.jsx'; - } - } - - return fileName; -}; diff --git a/src/compiler/sys/typescript/typescript-sys.ts b/src/compiler/sys/typescript/typescript-sys.ts deleted file mode 100644 index db5d4eba9b0..00000000000 --- a/src/compiler/sys/typescript/typescript-sys.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { isRemoteUrl, isString, noop, normalizePath, resolve } from '@utils'; -import { basename } from 'path'; -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { IS_CASE_SENSITIVE_FILE_NAMES } from '../environment'; -import { InMemoryFileSystem } from '../in-memory-fs'; - -// TODO(STENCIL-728): fix typing of `inMemoryFs` parameter in `patchTypescript`, related functions -export const patchTsSystemFileSystem = ( - config: d.ValidatedConfig, - compilerSys: d.CompilerSystem, - inMemoryFs: InMemoryFileSystem, - tsSys: ts.System, -): ts.System => { - const realpath = (path: string) => { - const rp = compilerSys.realpathSync(path); - if (isString(rp)) { - return rp; - } - return path; - }; - - const getAccessibleFileSystemEntries = (path: string) => { - try { - const entries = compilerSys.readDirSync(path || '.').sort(); - const files: string[] = []; - const directories: string[] = []; - - for (const absPath of entries) { - // This is necessary because on some file system node fails to exclude - // "." and "..". See https://github.com/nodejs/node/issues/4002 - const stat = inMemoryFs.statSync(absPath); - if (!stat) { - continue; - } - - const entry = basename(absPath); - if (stat.isFile) { - files.push(entry); - } else if (stat.isDirectory) { - directories.push(entry); - } - } - return { files, directories }; - } catch (e) { - return { files: [], directories: [] }; - } - }; - - tsSys.createDirectory = (p) => { - compilerSys.createDirSync(p, { recursive: true }); - }; - - tsSys.directoryExists = (p) => { - // At present the typing for `inMemoryFs` in this function is not accurate - // TODO(STENCIL-728): fix typing of `inMemoryFs` parameter in `patchTypescript`, related functions - if (inMemoryFs) { - const s = inMemoryFs.statSync(p); - return s.isDirectory; - } else { - const s = compilerSys.statSync(p); - return s.isDirectory; - } - }; - - tsSys.exit = compilerSys.exit; - - tsSys.fileExists = (p) => { - let filePath = p; - - if (isRemoteUrl(p)) { - filePath = getTypescriptPathFromUrl(config, tsSys.getExecutingFilePath(), p); - } - - // At present the typing for `inMemoryFs` in this function is not accurate - // TODO(STENCIL-728): fix typing of `inMemoryFs` parameter in `patchTypescript`, related functions - if (inMemoryFs) { - const s = inMemoryFs.statSync(filePath); - return !!(s && s.isFile); - } else { - const s = compilerSys.statSync(filePath); - return !!(s && s.isFile); - } - }; - - tsSys.getCurrentDirectory = compilerSys.getCurrentDirectory; - - tsSys.getExecutingFilePath = compilerSys.getCompilerExecutingPath; - - tsSys.getDirectories = (p) => { - const items = compilerSys.readDirSync(p); - return items.filter((itemPath) => { - // At present the typing for `inMemoryFs` in this function is not accurate - // TODO(STENCIL-728): fix typing of `inMemoryFs` parameter in `patchTypescript`, related functions - if (inMemoryFs) { - const s = inMemoryFs.statSync(itemPath); - return !!(s && s.exists && s.isDirectory); - } else { - const s = compilerSys.statSync(itemPath); - return !!(s && s.isDirectory); - } - }); - }; - - tsSys.readDirectory = (path, extensions, exclude, include, depth) => { - const cwd = compilerSys.getCurrentDirectory(); - // TODO(STENCIL-344): Replace `matchFiles` with a function that is publicly exposed - return (ts as any).matchFiles( - path, - extensions, - exclude, - include, - IS_CASE_SENSITIVE_FILE_NAMES, - cwd, - depth, - getAccessibleFileSystemEntries, - realpath, - ); - }; - - tsSys.readFile = (filePath) => { - return inMemoryFs ? inMemoryFs.readFileSync(filePath, { useCache: false }) : compilerSys.readFileSync(filePath); - }; - - // At present the typing for `inMemoryFs` in this function is not accurate - // TODO(STENCIL-728): fix typing of `inMemoryFs` parameter in `patchTypescript`, related functions - tsSys.writeFile = (p, data) => (inMemoryFs ? inMemoryFs.writeFile(p, data) : compilerSys.writeFile(p, data)); - - return tsSys; -}; - -const patchTsSystemWatch = (compilerSystem: d.CompilerSystem, tsSys: ts.System) => { - tsSys.watchDirectory = (p, cb, recursive) => { - const watcher = compilerSystem.watchDirectory( - p, - (filePath) => { - cb(filePath); - }, - recursive, - ); - return { - close() { - watcher.close(); - }, - }; - }; - - tsSys.watchFile = (p, cb) => { - const watcher = compilerSystem.watchFile(p, (filePath, eventKind) => { - if (eventKind === 'fileAdd') { - cb(filePath, ts.FileWatcherEventKind.Created); - } else if (eventKind === 'fileUpdate') { - cb(filePath, ts.FileWatcherEventKind.Changed); - } else if (eventKind === 'fileDelete') { - cb(filePath, ts.FileWatcherEventKind.Deleted); - } - }); - return { - close() { - watcher.close(); - }, - }; - }; -}; - -// TODO(STENCIL-728): fix typing of `inMemoryFs` parameter in `patchTypescript`, related functions -export const patchTypescript = (config: d.ValidatedConfig, inMemoryFs: InMemoryFileSystem) => { - if (!(ts as any).__patched) { - patchTsSystemFileSystem(config, config.sys, inMemoryFs, ts.sys); - patchTsSystemWatch(config.sys, ts.sys); - (ts as any).__patched = true; - } -}; - -const patchTypeScriptSysMinimum = () => { - if (!ts.sys) { - // patches just the bare minimum - // if ts.sys already exists then it must be node ts.sys - // otherwise we're browser - // will be updated later on with the stencil sys - ts.sys = { - args: [], - createDirectory: noop, - directoryExists: () => false, - exit: noop, - fileExists: () => false, - getCurrentDirectory: process.cwd, - getDirectories: () => [], - getExecutingFilePath: () => './', - readDirectory: () => [], - readFile: noop, - newLine: '\n', - resolvePath: resolve, - useCaseSensitiveFileNames: false, - write: noop, - writeFile: noop, - }; - } -}; -patchTypeScriptSysMinimum(); - -export const getTypescriptPathFromUrl = (config: d.ValidatedConfig, tsExecutingUrl: string, url: string) => { - const tsBaseUrl = new URL('..', tsExecutingUrl).href; - if (url.startsWith(tsBaseUrl)) { - const tsFilePath = url.replace(tsBaseUrl, '/'); - const tsNodePath = config.sys.getLocalModulePath({ - rootDir: config.rootDir, - moduleId: '@stencil/core', - path: tsFilePath, - }); - return normalizePath(tsNodePath); - } - return url; -}; diff --git a/src/compiler/transformers/add-tag-transform.ts b/src/compiler/transformers/add-tag-transform.ts deleted file mode 100644 index 0a9af4f2a25..00000000000 --- a/src/compiler/transformers/add-tag-transform.ts +++ /dev/null @@ -1,176 +0,0 @@ -import ts from 'typescript'; -import type * as d from '../../declarations'; -import { getModuleFromSourceFile } from './transform-utils'; -import { addCoreRuntimeApi, RUNTIME_APIS, TRANSFORM_TAG } from './core-runtime-apis'; -import { parse, SelectorType, stringify } from 'css-what'; - -export const addTagTransform = ( - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -): ts.TransformerFactory => { - return (transformCtx) => { - return (tsSourceFile) => { - const moduleFile = getModuleFromSourceFile(compilerCtx, tsSourceFile); - const tagNames = buildCtx.components.map((cmp) => cmp.tagName); - - addCoreRuntimeApi(moduleFile, RUNTIME_APIS.transformTag); - - const visitNode = (node: ts.Node): any => { - let newNode: ts.Node = node; - - // turns `element.querySelector("my-tag")` into `element.querySelector(`${transformTag("my-tag")}`)` - if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { - const methodName = node.expression.name.text; - - if ( - (methodName === 'querySelector' || methodName === 'querySelectorAll' || methodName === 'closest') && - node.arguments.length > 0 - ) { - const selectorArgument = node.arguments[0]; - - if (ts.isStringLiteral(selectorArgument)) { - const selectorText = selectorArgument.text; - const parsed = parse(selectorText); // from css-what - - const placeholders: string[] = []; - let modified = false; - - // Replace tag tokens with placeholder tokens and record tag names - const transformed = parsed.map((subSelector) => - subSelector.map((token) => { - if (token.type === SelectorType.Tag && tagNames.includes(token.name)) { - const idx = placeholders.length; - placeholders.push(token.name); - modified = true; - return { ...token, name: `___EXPR_${idx}___` }; // safe placeholder - } - return token; - }), - ); - - if (modified) { - // stringify will produce a selector like "div > ___EXPR_0___ + ___EXPR_1___[attr]" - const selectorWithPlaceholders = stringify(transformed); - - // Split into [literal, idx, literal, idx, literal, ...] - const splitParts = selectorWithPlaceholders.split(/___EXPR_(\d+)___/); - - // If no placeholders for whatever reason, fallback to original literal - if (!splitParts || splitParts.length === 0) { - // fallback — keep original string literal - newNode = ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [ - ts.factory.createStringLiteral(selectorText), - ...node.arguments.slice(1), - ]); - } else { - // Build TemplateExpression: head + spans - const firstLiteral = splitParts[0] ?? ''; - const head = ts.factory.createTemplateHead(firstLiteral); - - const spans: ts.TemplateSpan[] = []; - for (let i = 1; i < splitParts.length; i += 2) { - const idxStr = splitParts[i]; - const literalAfter = splitParts[i + 1] ?? ''; - const exprIndex = Number(idxStr); - const tagName = placeholders[exprIndex]; - - // transformTag("tagName") - const callExpr = ts.factory.createCallExpression( - ts.factory.createIdentifier(TRANSFORM_TAG), - undefined, - [ts.factory.createStringLiteral(tagName)], - ); - - const isLast = i + 1 >= splitParts.length - 1; - const literalNode = isLast - ? ts.factory.createTemplateTail(literalAfter) - : ts.factory.createTemplateMiddle(literalAfter); - - spans.push(ts.factory.createTemplateSpan(callExpr, literalNode)); - } - - const templateExpr = ts.factory.createTemplateExpression(head, spans); - - // Replace the original selector arg with the template expression - newNode = ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [ - templateExpr, - ...node.arguments.slice(1), - ]); - } - } - } - } - } - - // turns `customElements.get("my-tag")` into `customElements.get(transformTag("my-tag"))` - - if (ts.isCallExpression(node)) { - const expression = node.expression; - if ( - ts.isPropertyAccessExpression(expression) && // customElements.get / define / whenDefined - (((expression.name.text === 'get' || - expression.name.text === 'define' || - expression.name.text === 'whenDefined') && - ts.isIdentifier(expression.expression) && - expression.expression.text === 'customElements') || - // document.createElement - (expression.name.text === 'createElement' && - ts.isIdentifier(expression.expression) && - expression.expression.text === 'document')) - ) { - const [firstArg, ...restArgs] = node.arguments; - if (firstArg) { - // Wrap the argument in transformTag(...) - const newFirstArg = ts.factory.createCallExpression( - ts.factory.createIdentifier(TRANSFORM_TAG), - undefined, - [firstArg], - ); - - newNode = ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [ - newFirstArg, - ...restArgs, - ]); - } - } else { - node.expression; - } - } - - // turns el.tagName === 'my-tag' into el.tagName === transformTag('my-tag') - // or 'my-tag' == elTag into transformTag('my-tag') == elTag - // ... this feels like a bit much? - - // if (ts.isBinaryExpression(node)) { - // const { left, right, operatorToken } = node; - // const stringLiteral = ts.isStringLiteral(left) ? left : ts.isStringLiteral(right) ? right : null; - - // if (stringLiteral && tagNames.includes(stringLiteral.text)) { - // const transformedLiteral = ts.factory.createCallExpression( - // ts.factory.createIdentifier(TRANSFORM_TAG), - // undefined, - // [ts.factory.createStringLiteral(stringLiteral.text)], - // ); - - // let newLeft = left; - // let newRight = right; - - // if (stringLiteral === left) { - // newLeft = transformedLiteral; - // } else { - // newRight = transformedLiteral; - // } - - // newNode = ts.factory.updateBinaryExpression(node, newLeft, operatorToken, newRight); - // } - // } - - return ts.visitEachChild(newNode, visitNode, transformCtx); - }; - - tsSourceFile = ts.visitEachChild(tsSourceFile, visitNode, transformCtx); - - return tsSourceFile; - }; - }; -}; diff --git a/src/compiler/transformers/automatic-key-insertion/index.ts b/src/compiler/transformers/automatic-key-insertion/index.ts deleted file mode 100644 index 974bda42394..00000000000 --- a/src/compiler/transformers/automatic-key-insertion/index.ts +++ /dev/null @@ -1,253 +0,0 @@ -import ts from 'typescript'; - -import { getComponentTagName, isStaticGetter } from '../transform-utils'; -import { deriveJSXKey } from './utils'; - -/** - * A transformer factory to create a transformer which will add `key` - * properties to all of the JSX nodes contained inside of a Stencil component's - * `render` function. - * - * This can be thought of as transforming the following: - * - * ```tsx - * class MyComponent { - * render() { - *
    hey!
    - * } - * } - * ``` - * - * to this: - * - * ```tsx - * class MyComponent { - * render() { - *
    hey!
    - * } - * } - * ``` - * - * The inserted keys are generated by {@link deriveJSXKey}. - * - * **Note**: this transformer must be run _after_ the - * `convertDecoratorsToStatic` transformer, since it depends on static getters - * created by that transformer to determine when to transform a class node. - * - * @param transformCtx a transformation context - * @returns a typescript transformer for inserting keys into JSX nodes - */ -export const performAutomaticKeyInsertion = (transformCtx: ts.TransformationContext): ts.Transformer => { - /** - * This is our outer-most visitor function which serves to locate a class - * declaration which is also a Stencil component, at which point it hands - * things over to the next visitor function ({@link findRenderMethodVisitor}) - * which locates the `render` method. - * - * @param node a typescript syntax tree node - * @returns the result of handling the node - */ - function findClassDeclVisitor(node: ts.Node): ts.VisitResult { - if (ts.isClassDeclaration(node)) { - const tagName = getComponentTagName(node.members.filter(isStaticGetter)); - if (tagName != null) { - // we've got a class node with an `is` property, which tells us that - // the class we're dealing with is a Stencil component which has - // already been through the `convertDecoratorsToStatic` transformer. - return ts.visitEachChild(node, findRenderMethodVisitor, transformCtx); - } - } - // we either didn't find a class node, or we found a class node without a - // component tag name, so this is not a stencil component! - return ts.visitEachChild(node, findClassDeclVisitor, transformCtx); - } - - /** - * This middle visitor function is responsible for finding the render method - * on a Stencil class and then passing off responsibility to the inner-most - * visitor, which deals with syntax nodes inside the method. - * - * @param node a typescript syntax tree node - * @returns the result of handling the node - */ - function findRenderMethodVisitor(node: ts.Node): ts.VisitResult { - // we want to keep going (to drill down into JSX nodes and transform them) - // only in particular circumstances: - // - // 1. the syntax tree node is a method declaration - // 2. this method's name is 'render' - // 3. the method only has a single return statement - // - // We want to only keep going if there's a single return statement because - // if there are multiple return statements inserting keys could cause - // needless re-renders. If a `render` method looked like this, for - // instance: - // - // ```tsx - // render() { - // if (foo) { - // return
    hey!
    ; - // } else { - // return
    hay!
    ; - // } - // } - // ``` - // - // Since the `
    ` tags don't have `key` attributes the Stencil vdom will - // re-use the same div element between re-renders, and will just swap out - // the children (the text nodes in this case). If our key insertion - // transformer put unique keys onto each tag then this wouldn't happen any - // longer. - if (ts.isMethodDeclaration(node) && node.name.getText() === 'render' && numReturnStatements(node) === 1) { - return ts.visitEachChild(node, jsxElementVisitor, transformCtx); - } else { - return ts.visitEachChild(node, findRenderMethodVisitor, transformCtx); - } - } - - /** - * Our inner-most visitor function. This will edit any JSX nodes that it - * finds, adding a `key` attribute to them via {@link addKeyAttr}. - * - * @param node a typescript syntax tree node - * @returns the result of handling the node - */ - function jsxElementVisitor(node: ts.Node): ts.VisitResult { - if (ts.isCallExpression(node)) { - // if there are any JSX nodes which are children of the call expression - // (i.e. arguments) we don't want to transform them since we can't know - // _a priori_ what could be done with them at runtime - return node; - } else if (ts.isConditionalExpression(node)) { - // we're going to encounter the same problem here that we encounter with - // multiple return statements, so we just return the node and don't recur into - // its children - return node; - } else if (isJSXElWithAttrs(node)) { - return addKeyAttr(node); - } else { - return ts.visitEachChild(node, jsxElementVisitor, transformCtx); - } - } - - return (tsSourceFile) => { - return ts.visitEachChild(tsSourceFile, findClassDeclVisitor, transformCtx); - }; -}; - -/** - * Count the number of return statements in a {@link ts.MethodDeclaration} - * - * @param method the node within which we're going to count `return` statements - * @returns the number of return statements found - */ -function numReturnStatements(method: ts.MethodDeclaration): number { - let count = 0; - - function walker(node: ts.Node) { - for (const child of node.getChildren()) { - if (ts.isReturnStatement(child)) { - count++; - } else { - walker(child); - } - } - } - - walker(method); - - return count; -} - -/** - * Type guard to see if a TypeScript syntax node is one of the node types which - * corresponds to a JSX element that can have attributes on it. This is either - * an opening node, like `
    `, or a 'self-closing' node like - * ``. - * - * @param node a typescript syntax tree node - * @returns whether or not the node is JSX node which could have attributes - */ -function isJSXElWithAttrs(node: ts.Node): node is ts.JsxOpeningElement | ts.JsxSelfClosingElement { - return ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node); -} - -/** - * Given a JSX syntax tree node update it to include a unique key attribute. - * This will respect any attributes already set on the node, including a - * pre-existing, user-defined `key` attribute. - * - * @param jsxElement a typescript JSX syntax tree node - * @returns an updated JSX element, with a key added. - */ -function addKeyAttr( - jsxElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement, -): ts.JsxOpeningElement | ts.JsxSelfClosingElement { - if (jsxElement.attributes.properties.some(isKeyAttr)) { - // this node already has a key! let's get out of here - return jsxElement; - } - - const updatedAttributes = ts.factory.createJsxAttributes([ - ts.factory.createJsxAttribute( - ts.factory.createIdentifier('key'), - ts.factory.createStringLiteral(deriveJSXKey(jsxElement)), - ), - ...jsxElement.attributes.properties, - ]); - - if (ts.isJsxOpeningElement(jsxElement)) { - return ts.factory.updateJsxOpeningElement( - jsxElement, - jsxElement.tagName, - jsxElement.typeArguments, - updatedAttributes, - ); - } else { - return ts.factory.updateJsxSelfClosingElement( - jsxElement, - jsxElement.tagName, - jsxElement.typeArguments, - updatedAttributes, - ); - } -} - -/** - * Check whether or not a JSX attribute node (well, technically a - * {@link ts.JsxAttributeLike} node) has the name `"key"` or not - * - * @param attr the JSX attribute node to check - * @returns whether or not this node has the name 'key' - */ -function isKeyAttr(attr: ts.JsxAttributeLike): boolean { - return !!attr.name && attrNameToString(attr) === 'key'; -} - -/** - * Given a JSX attribute get its name as a string - * - * @param attr the attribute of interest - * @returns the attribute's name, formatted as a string - */ -function attrNameToString(attr: ts.JsxAttributeLike): string { - switch (attr.name?.kind) { - case ts.SyntaxKind.Identifier: - case ts.SyntaxKind.PrivateIdentifier: - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NumericLiteral: - return attr.name.text; - case ts.SyntaxKind.JsxNamespacedName: - // this is a JSX attribute name like `foo:bar` - // see https://facebook.github.io/jsx/#prod-JSXNamespacedName - return attr.name.getText(); - case ts.SyntaxKind.ComputedPropertyName: - const expression = attr.name.expression; - if (ts.isStringLiteral(expression) || ts.isNumericLiteral(expression)) { - return expression.text; - } - return ''; - default: - return ''; - } -} diff --git a/src/compiler/transformers/automatic-key-insertion/utils.ts b/src/compiler/transformers/automatic-key-insertion/utils.ts deleted file mode 100644 index 645915aee29..00000000000 --- a/src/compiler/transformers/automatic-key-insertion/utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createHash } from 'crypto'; -import ts from 'typescript'; - -/** - * An incrementing-number generator, just as a little extra 'uniqueness' - * insurance for {@link deriveJSXKey} - */ -const incrementer = (function* () { - let val = 0; - while (true) { - yield val++; - } -})(); - -/** - * Generate a unique key for a given JSX element. The key is creating by - * concatenating and then hashing (w/ SHA1) the following: - * - * - an incrementing value - * - the element's tag name - * - the start position for the element's token in the original source file - * - the end position for the element's token in the original source file - * - * It is hoped this provides enough uniqueness that a collision won't occur. - * - * @param jsxElement a typescript JSX syntax tree node which needs a key - * @returns a computed unique key for that element - */ -export function deriveJSXKey(jsxElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement): string { - const hash = createHash('sha1') - .update(`${incrementer.next().value}__${jsxElement.tagName}__${jsxElement.pos}_${jsxElement.end}`) - .digest('hex') - .toLowerCase(); - return hash; -} diff --git a/src/compiler/transformers/collections/add-external-import.ts b/src/compiler/transformers/collections/add-external-import.ts deleted file mode 100644 index c441f82792a..00000000000 --- a/src/compiler/transformers/collections/add-external-import.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { isString, normalizePath, parsePackageJson } from '@utils'; -import { dirname } from 'path'; - -import type * as d from '../../../declarations'; -import { tsResolveModuleNamePackageJsonPath } from '../../sys/typescript/typescript-resolve-module'; -import { parseCollection } from './parse-collection-module'; - -export const addExternalImport = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - moduleFile: d.Module, - containingFile: string, - moduleId: string, - resolveCollections: boolean, -) => { - if (!moduleFile.externalImports.includes(moduleId)) { - moduleFile.externalImports.push(moduleId); - moduleFile.externalImports.sort(); - } - - if (!resolveCollections || compilerCtx.resolvedCollections.has(moduleId)) { - // we've already handled this collection moduleId before - return; - } - - let pkgJsonFilePath = tsResolveModuleNamePackageJsonPath(config, compilerCtx, moduleId, containingFile); - - // cache that we've already parsed this - compilerCtx.resolvedCollections.add(moduleId); - - if (pkgJsonFilePath == null) { - return; - } - - const realPkgJsonFilePath = config.sys.realpathSync(pkgJsonFilePath); - if (realPkgJsonFilePath.path) { - pkgJsonFilePath = realPkgJsonFilePath.path; - } - - // realpathSync may return a path that uses Windows path separators ('\'). - // normalize it for the purposes of this comparison - if (normalizePath(pkgJsonFilePath) === config.packageJsonFilePath) { - // same package silly! - return; - } - - // open up and parse the package.json - // sync on purpose :( - const pkgJsonStr = compilerCtx.fs.readFileSync(pkgJsonFilePath); - if (pkgJsonStr == null) { - return; - } - const parsedPkgJson = parsePackageJson(pkgJsonStr, pkgJsonFilePath); - if (parsedPkgJson.diagnostic) { - buildCtx.diagnostics.push(parsedPkgJson.diagnostic); - return; - } - - if (!isString(parsedPkgJson.data.collection) || !parsedPkgJson.data.collection.endsWith('.json')) { - // this import is not a stencil collection - return; - } - - if (!isString(parsedPkgJson.data.types) || !parsedPkgJson.data.types.endsWith('.d.ts')) { - // this import should have types - return; - } - - // this import is a stencil collection - // let's parse it and gather all the module data about it - // internally it'll cached collection data if we've already done this - const collection = parseCollection( - config, - compilerCtx, - buildCtx, - moduleId, - parsedPkgJson.filePath, - parsedPkgJson.data, - ); - if (!collection) { - return; - } - - // check if we already added this collection to the build context - const alreadyHasCollection = buildCtx.collections.some((c) => { - return c.collectionName === collection.collectionName; - }); - - if (alreadyHasCollection) { - // we already have this collection in our build context - return; - } - - // let's add the collection to the build context - buildCtx.collections.push(collection); - - if (Array.isArray(collection.dependencies)) { - // this collection has more collections - // let's keep digging down and discover all of them - collection.dependencies.forEach((dependencyModuleId) => { - const resolveFromDir = dirname(pkgJsonFilePath); - addExternalImport( - config, - compilerCtx, - buildCtx, - moduleFile, - resolveFromDir, - dependencyModuleId, - resolveCollections, - ); - }); - } -}; diff --git a/src/compiler/transformers/collections/parse-collection-manifest.ts b/src/compiler/transformers/collections/parse-collection-manifest.ts deleted file mode 100644 index 0c23d5fc0af..00000000000 --- a/src/compiler/transformers/collections/parse-collection-manifest.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { join, normalizePath } from '@utils'; - -import type * as d from '../../../declarations'; -import { parseCollectionComponents, transpileCollectionModule } from './parse-collection-components'; - -export const parseCollectionManifest = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - collectionName: string, - collectionDir: string, - collectionJsonStr: string, -) => { - const collectionManifest: d.CollectionManifest = JSON.parse(collectionJsonStr); - - const compilerVersion: d.CollectionCompilerVersion = collectionManifest.compiler || ({} as any); - - const collection: d.CollectionCompilerMeta = { - collectionName: collectionName, - moduleId: collectionName, - moduleDir: collectionDir, - moduleFiles: [], - dependencies: parseCollectionDependencies(collectionManifest), - compiler: { - name: compilerVersion.name || '', - version: compilerVersion.version || '', - typescriptVersion: compilerVersion.typescriptVersion || '', - }, - bundles: parseBundles(collectionManifest), - }; - - parseGlobal(config, compilerCtx, buildCtx, collectionDir, collectionManifest, collection); - parseCollectionComponents(config, compilerCtx, buildCtx, collectionDir, collectionManifest, collection); - - return collection; -}; - -export const parseCollectionDependencies = (collectionManifest: d.CollectionManifest) => { - return (collectionManifest.collections || []).map((c) => c.name); -}; - -export const parseGlobal = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - collectionDir: string, - collectionManifest: d.CollectionManifest, - collection: d.CollectionCompilerMeta, -) => { - if (typeof collectionManifest.global !== 'string') { - return; - } - - const sourceFilePath = normalizePath(join(collectionDir, collectionManifest.global)); - const globalModule = transpileCollectionModule(config, compilerCtx, buildCtx, collection, sourceFilePath); - collection.global = globalModule; -}; - -export const parseBundles = (collectionManifest: d.CollectionManifest) => { - if (invalidArrayData(collectionManifest.bundles)) { - return []; - } - - return collectionManifest.bundles.map((b) => { - return { - components: b.components.slice().sort(), - }; - }); -}; - -const invalidArrayData = (arr: any[]) => { - return !arr || !Array.isArray(arr) || arr.length === 0; -}; diff --git a/src/compiler/transformers/collections/parse-collection-module.ts b/src/compiler/transformers/collections/parse-collection-module.ts deleted file mode 100644 index 1a4492bfb7d..00000000000 --- a/src/compiler/transformers/collections/parse-collection-module.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { join, normalizePath, relative } from '@utils'; -import { dirname } from 'path'; - -import type * as d from '../../../declarations'; -import { parseCollectionManifest } from './parse-collection-manifest'; - -export const parseCollection = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - moduleId: string, - pkgJsonFilePath: string, - pkgData: d.PackageJsonData, -) => { - // note this MUST be synchronous because this is used during transpile - const collectionName = pkgData.name; - - let collection: d.CollectionCompilerMeta = compilerCtx.collections.find((c) => c.collectionName === collectionName); - if (collection != null) { - // we've already cached the collection, no need for another resolve/readFile/parse - // thought being that /node_modules/ isn't changing between watch builds - return collection; - } - - // get the root directory of the dependency - const collectionPackageRootDir = dirname(pkgJsonFilePath); - - // figure out the full path to the collection collection file - const collectionFilePath = join(collectionPackageRootDir, pkgData.collection); - - const relPath = relative(config.rootDir, collectionFilePath); - config.logger.debug(`load collection: ${collectionName}, ${relPath}`); - - // we haven't cached the collection yet, let's read this file - // sync on purpose :( - const collectionJsonStr = compilerCtx.fs.readFileSync(collectionFilePath); - if (!collectionJsonStr) { - return null; - } - - // get the directory where the collection collection file is sitting - const collectionDir = normalizePath(dirname(collectionFilePath)); - - // parse the json string into our collection data - collection = parseCollectionManifest(config, compilerCtx, buildCtx, collectionName, collectionDir, collectionJsonStr); - - collection.moduleId = moduleId; - - if (pkgData.module && pkgData.module !== pkgData.main) { - collection.hasExports = true; - } - - // remember the source of this collection node_module - collection.moduleDir = collectionPackageRootDir; - - // cache it for later yo - compilerCtx.collections.push(collection); - - return collection; -}; diff --git a/src/compiler/transformers/component-hydrate/hydrate-component.ts b/src/compiler/transformers/component-hydrate/hydrate-component.ts deleted file mode 100644 index 63036b31cd8..00000000000 --- a/src/compiler/transformers/component-hydrate/hydrate-component.ts +++ /dev/null @@ -1,45 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { updateLazyComponentConstructor } from '../component-lazy/lazy-constructor'; -import { addLazyElementGetter } from '../component-lazy/lazy-element-getter'; -import { transformHostData } from '../host-data-transform'; -import { removeStaticMetaProperties } from '../remove-static-meta-properties'; -import { retrieveModifierLike } from '../transform-utils'; -import { addReactivePropHandlers } from '../reactive-handler-meta-transform'; -import { addHydrateRuntimeCmpMeta } from './hydrate-runtime-cmp-meta'; - -export const updateHydrateComponentClass = ( - classNode: ts.ClassDeclaration, - moduleFile: d.Module, - cmp: d.ComponentCompilerMeta, - buildCtx: d.BuildCtx, -) => { - return ts.factory.updateClassDeclaration( - classNode, - retrieveModifierLike(classNode), - classNode.name, - classNode.typeParameters, - classNode.heritageClauses, - updateHydrateHostComponentMembers(classNode, moduleFile, cmp, buildCtx), - ); -}; - -const updateHydrateHostComponentMembers = ( - classNode: ts.ClassDeclaration, - moduleFile: d.Module, - cmp: d.ComponentCompilerMeta, - buildCtx: d.BuildCtx, -) => { - const classMembers = removeStaticMetaProperties(classNode); - - updateLazyComponentConstructor(classMembers, classNode, moduleFile, cmp); - addLazyElementGetter(classMembers, moduleFile, cmp); - addReactivePropHandlers(classMembers, cmp, 'watchers'); - addReactivePropHandlers(classMembers, cmp, 'serializers'); - addReactivePropHandlers(classMembers, cmp, 'deserializers'); - addHydrateRuntimeCmpMeta(classMembers, cmp, buildCtx); - transformHostData(classMembers, moduleFile); - - return classMembers; -}; diff --git a/src/compiler/transformers/component-hydrate/hydrate-runtime-cmp-meta.ts b/src/compiler/transformers/component-hydrate/hydrate-runtime-cmp-meta.ts deleted file mode 100644 index f142ef11610..00000000000 --- a/src/compiler/transformers/component-hydrate/hydrate-runtime-cmp-meta.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CMP_FLAGS, formatComponentRuntimeMeta } from '@utils'; -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { addStaticStyleGetterWithinClass } from '../add-static-style'; -import { convertValueToLiteral, createStaticGetter } from '../transform-utils'; - -export const addHydrateRuntimeCmpMeta = ( - classMembers: ts.ClassElement[], - cmp: d.ComponentCompilerMeta, - buildCtx: d.BuildCtx, -) => { - const compactMeta: d.ComponentRuntimeMetaCompact = formatComponentRuntimeMeta(cmp, true); - const cmpMeta: d.ComponentRuntimeMeta = { - $flags$: compactMeta[0], - $tagName$: compactMeta[1], - $members$: compactMeta[2], - $listeners$: compactMeta[3], - $lazyBundleId$: fakeBundleIds(cmp), - $attrsToReflect$: getHydrateAttrsToReflect(cmp), - }; - // We always need shadow-dom shim in hydrate runtime - if (cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) { - // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field - cmpMeta.$flags$ |= CMP_FLAGS.needsShadowDomShim; - } - const staticMember = createStaticGetter('cmpMeta', convertValueToLiteral(cmpMeta)); - addStaticStyleGetterWithinClass(classMembers, cmp, buildCtx); - - classMembers.push(staticMember); -}; - -const fakeBundleIds = (_cmp: d.ComponentCompilerMeta) => { - return '-'; -}; - -const getHydrateAttrsToReflect = (cmp: d.ComponentCompilerMeta): d.ComponentRuntimeReflectingAttr[] => { - return cmp.properties.reduce((attrs: d.ComponentRuntimeReflectingAttr[], prop: d.ComponentCompilerProperty) => { - if (prop.reflect) { - attrs.push([prop.name, prop.attribute]); - } - return attrs; - }, []); -}; diff --git a/src/compiler/transformers/component-hydrate/tranform-to-hydrate-component.ts b/src/compiler/transformers/component-hydrate/tranform-to-hydrate-component.ts deleted file mode 100644 index afb2b778a4a..00000000000 --- a/src/compiler/transformers/component-hydrate/tranform-to-hydrate-component.ts +++ /dev/null @@ -1,45 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { addImports } from '../add-imports'; -import { addLegacyApis } from '../core-runtime-apis'; -import { updateStyleImports } from '../style-imports'; -import { getComponentMeta, getModuleFromSourceFile, updateMixin } from '../transform-utils'; -import { updateHydrateComponentClass } from './hydrate-component'; - -export const hydrateComponentTransform = ( - compilerCtx: d.CompilerCtx, - transformOpts: d.TransformOptions, - buildCtx: d.BuildCtx, -): ts.TransformerFactory => { - return (transformCtx) => { - return (tsSourceFile) => { - const moduleFile = getModuleFromSourceFile(compilerCtx, tsSourceFile); - - const visitNode = (node: ts.Node): any => { - if (ts.isClassDeclaration(node)) { - const cmp = getComponentMeta(compilerCtx, tsSourceFile, node); - if (cmp != null) { - return updateHydrateComponentClass(node, moduleFile, cmp, buildCtx); - } else if (compilerCtx.moduleMap.get(tsSourceFile.fileName)?.isMixin) { - return updateMixin(node, moduleFile, cmp, transformOpts); - } - } - - return ts.visitEachChild(node, visitNode, transformCtx); - }; - - tsSourceFile = ts.visitEachChild(tsSourceFile, visitNode, transformCtx); - - if (moduleFile.cmps.length > 0) { - tsSourceFile = updateStyleImports(transformOpts, tsSourceFile, moduleFile); - } - if (moduleFile.isLegacy) { - addLegacyApis(moduleFile); - } - tsSourceFile = addImports(transformOpts, tsSourceFile, moduleFile.coreRuntimeApis, transformOpts.coreImportPath); - - return tsSourceFile; - }; - }; -}; diff --git a/src/compiler/transformers/component-lazy/attach-internals.ts b/src/compiler/transformers/component-lazy/attach-internals.ts deleted file mode 100644 index 19f2c1a2284..00000000000 --- a/src/compiler/transformers/component-lazy/attach-internals.ts +++ /dev/null @@ -1,194 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import ts from 'typescript'; - -import { HOST_REF_ARG } from './constants'; - -/** - * Create a binding for an `ElementInternals` object compatible with a - * lazy-load ready Stencil component. - * - * In order to create a lazy-loaded form-associated component we need to access - * the underlying host element (via the "$hostElement$" prop on {@link d.HostRef}) - * to make the `attachInternals` call on the right element. This means that the - * code generated by this function depends on there being a variable in scope - * called {@link HOST_REF_ARG} with type {@link HTMLElement}. - * - * If an `@AttachInternals` decorator is present on a component like this: - * - * ```ts - * @AttachInternals({ states: { open: true, active: false } }) - * internals: ElementInternals; - * ``` - * - * then this transformer will create syntax nodes which represent the - * following TypeScript source: - * - * ```ts - * if (hostRef.$hostElement$["s-ei"]) { - * this.internals = hostRef.$hostElement$["s-ei"]; - * } else { - * this.internals = hostRef.$hostElement$.attachInternals(); - * hostRef.$hostElement$["s-ei"] = this.internals; - * } - * this.internals.states.add('open'); - * // 'active' is false, so no call needed (not in set by default) - * ``` - * - * The `"s-ei"` prop on a {@link d.HostElement} may hold a reference to the - * `ElementInternals` instance for that host. We store a reference to it - * there in order to support HMR because `.attachInternals` may only be - * called on an `HTMLElement` one time, so we need to store a reference to - * the returned value across HMR updates. - * - * @param cmp metadata about the component of interest, gathered during compilation - * @returns a list of expression statements - */ -export function createLazyAttachInternalsBinding(cmp: d.ComponentCompilerMeta): ts.Statement[] { - if (!cmp?.attachInternalsMemberName) { - return []; - } - - const statements: ts.Statement[] = [ - ts.factory.createIfStatement( - // the condition for the `if` statement here is just whether the - // following is defined: - // - // ```ts - // hostRef.$hostElement$["s-ei"] - // ``` - hostRefElementInternalsPropAccess(), - ts.factory.createBlock( - [ - // this `ts.factory` call creates the following statement: - // - // ```ts - // this.${ cmp.formInternalsMemberName } = hostRef.$hostElement$['s-ei']; - // ``` - ts.factory.createExpressionStatement( - ts.factory.createBinaryExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createThis(), - // use the name set on the {@link d.ComponentCompilerMeta} - ts.factory.createIdentifier(cmp.attachInternalsMemberName), - ), - ts.factory.createToken(ts.SyntaxKind.EqualsToken), - hostRefElementInternalsPropAccess(), - ), - ), - ], - true, - ), - ts.factory.createBlock( - [ - // this `ts.factory` call creates the following statement: - // - // ```ts - // this.${ cmp.attachInternalsMemberName } = hostRef.$hostElement$.attachInternals(); - // ``` - ts.factory.createExpressionStatement( - ts.factory.createBinaryExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createThis(), - // use the name set on the {@link d.ComponentCompilerMeta} - ts.factory.createIdentifier(cmp.attachInternalsMemberName), - ), - ts.factory.createToken(ts.SyntaxKind.EqualsToken), - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(HOST_REF_ARG), - ts.factory.createIdentifier('$hostElement$'), - ), - ts.factory.createIdentifier('attachInternals'), - ), - undefined, - [], - ), - ), - ), - // this `ts.factory` call produces the following: - // - // ```ts - // hostRef.$hostElement$['s-ei'] = this.${ cmp.attachInternalsMemberName }; - // ``` - ts.factory.createExpressionStatement( - ts.factory.createBinaryExpression( - hostRefElementInternalsPropAccess(), - ts.factory.createToken(ts.SyntaxKind.EqualsToken), - ts.factory.createPropertyAccessExpression( - ts.factory.createThis(), - // use the name set on the {@link d.ComponentCompilerMeta} - ts.factory.createIdentifier(cmp.attachInternalsMemberName), - ), - ), - ), - ], - true, - ), - ), - ]; - - // Add custom states initialization for states with initialValue: true - // CustomStateSet only has add/delete/has methods (extends Set), so we only - // need to call add() for true values - false values are the default (not in set) - if (cmp.attachInternalsCustomStates?.length > 0) { - for (const customState of cmp.attachInternalsCustomStates) { - if (customState.initialValue) { - statements.push(createStatesAddCall(cmp.attachInternalsMemberName, customState.name)); - } - } - } - - return statements; -} - -/** - * Create a `states.add()` call for initializing a custom state. - * - * Generates code like: - * ```ts - * this.internals.states.add('stateName'); - * ``` - * - * @param memberName the name of the ElementInternals property - * @param stateName the name of the custom state to add - * @returns an expression statement for the add call - */ -function createStatesAddCall(memberName: string, stateName: string): ts.ExpressionStatement { - return ts.factory.createExpressionStatement( - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createIdentifier(memberName)), - ts.factory.createIdentifier('states'), - ), - ts.factory.createIdentifier('add'), - ), - undefined, - [ts.factory.createStringLiteral(stateName)], - ), - ); -} - -/** - * Create TS syntax nodes which represent accessing the `"s-ei"` (stencil - * element internals) property on `$hostElement$` (a {@link d.HostElement}) on a - * {@link d.HostRef} element which is called {@link HOST_REF_ARG}. - * - * The corresponding TypeScript source will look like: - * - * ```ts - * hostRef.$hostElement$["s-ei"] - * ``` - * - * @returns TS syntax nodes - */ -function hostRefElementInternalsPropAccess(): ts.ElementAccessExpression { - return ts.factory.createElementAccessExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(HOST_REF_ARG), - ts.factory.createIdentifier('$hostElement$'), - ), - ts.factory.createStringLiteral('s-ei'), - ); -} diff --git a/src/compiler/transformers/component-lazy/transform-lazy-component.ts b/src/compiler/transformers/component-lazy/transform-lazy-component.ts deleted file mode 100644 index 3f0eebfe8ab..00000000000 --- a/src/compiler/transformers/component-lazy/transform-lazy-component.ts +++ /dev/null @@ -1,71 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { addImports } from '../add-imports'; -import { addLegacyApis } from '../core-runtime-apis'; -import { updateStyleImports } from '../style-imports'; -import { getComponentMeta, getModuleFromSourceFile, updateConstructor, updateMixin } from '../transform-utils'; -import { updateLazyComponentClass } from './lazy-component'; - -/** - * Return a transformer factory which transforms a Stencil component to make it - * suitable for 'taking over' a bootstrapped component in the lazy build. - * - * Note that this is an 'output target' level transformer, i.e. it is - * designed to be run on a Stencil component which has already undergone - * initial transformation (which handles things like converting decorators to - * static and so on). - * - * @param compilerCtx a Stencil compiler context object - * @param transformOpts transform options - * @returns a {@link ts.TransformerFactory} for carrying out necessary transformations - */ -export const lazyComponentTransform = ( - compilerCtx: d.CompilerCtx, - transformOpts: d.TransformOptions, - buildCtx: d.BuildCtx, -): ts.TransformerFactory => { - return (transformCtx) => { - return (tsSourceFile) => { - const styleStatements: ts.Statement[] = []; - const moduleFile = getModuleFromSourceFile(compilerCtx, tsSourceFile); - - const visitNode = (node: ts.Node): any => { - if (ts.isClassDeclaration(node)) { - const cmp = getComponentMeta(compilerCtx, tsSourceFile, node); - const module = compilerCtx.moduleMap.get(tsSourceFile.fileName); - - if (cmp != null) { - return updateLazyComponentClass(transformOpts, styleStatements, node, moduleFile, cmp, buildCtx); - } else if (module?.isMixin) { - return updateMixin(node, moduleFile, cmp, transformOpts); - } else if (buildCtx.config._isTesting && buildCtx.config.flags.spec && !buildCtx.config.flags.e2e) { - // because (during spec tests) *only* the component class is added as a module - // let's tidy up all class nodes in testing mode, but only when running spec tests alone - // (not when running both spec and e2e, as e2e builds will handle transformation differently) - return updateConstructor(node, Array.from(node.members), [], []); - } - } - return ts.visitEachChild(node, visitNode, transformCtx); - }; - - tsSourceFile = ts.visitEachChild(tsSourceFile, visitNode, transformCtx); - - if (moduleFile.cmps.length > 0) { - tsSourceFile = updateStyleImports(transformOpts, tsSourceFile, moduleFile); - } - - if (moduleFile.isLegacy) { - addLegacyApis(moduleFile); - } - - tsSourceFile = addImports(transformOpts, tsSourceFile, moduleFile.coreRuntimeApis, transformOpts.coreImportPath); - - if (styleStatements.length > 0) { - tsSourceFile = ts.factory.updateSourceFile(tsSourceFile, [...tsSourceFile.statements, ...styleStatements]); - } - - return tsSourceFile; - }; - }; -}; diff --git a/src/compiler/transformers/component-native/attach-internals.ts b/src/compiler/transformers/component-native/attach-internals.ts deleted file mode 100644 index d7e94ddd3da..00000000000 --- a/src/compiler/transformers/component-native/attach-internals.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import ts from 'typescript'; - -/** - * Create a binding for an `ElementInternals` object compatible with a 'native' - * component (i.e. one which extends `HTMLElement` and is distributed as a - * standalone custom element). - * - * Since a 'native' custom element will extend `HTMLElement` we can call - * `this.attachInternals` directly, binding it to the name annotated by the - * developer with the `@AttachInternals` decorator. - * - * Thus if an `@AttachInternals` decorator is present on a component like - * this: - * - * ```ts - * @AttachInternals({ states: { open: true, active: false } }) - * internals: ElementInternals; - * ``` - * - * then this transformer will emit TS syntax nodes representing the - * following TypeScript source code: - * - * ```ts - * this.internals = this.attachInternals(); - * this.internals.states.add('open'); - * // 'active' is false, so no call needed (not in set by default) - * ``` - * - * @param cmp metadata about the component of interest, gathered during - * compilation - * @returns an expression statement syntax tree node - */ -export function createNativeAttachInternalsBinding(cmp: d.ComponentCompilerMeta): ts.ExpressionStatement[] { - if (!cmp.attachInternalsMemberName) { - return []; - } - - const statements: ts.ExpressionStatement[] = [ - ts.factory.createExpressionStatement( - ts.factory.createBinaryExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createThis(), - // use the name set on the {@link d.ComponentCompilerMeta} - ts.factory.createIdentifier(cmp.attachInternalsMemberName), - ), - ts.factory.createToken(ts.SyntaxKind.EqualsToken), - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createThis(), - ts.factory.createIdentifier('attachInternals'), - ), - undefined, - [], - ), - ), - ), - ]; - - // Add custom states initialization for states with initialValue: true - // CustomStateSet only has add/delete/has methods (extends Set), so we only - // need to call add() for true values - false values are the default (not in set) - if (cmp.attachInternalsCustomStates?.length > 0) { - for (const customState of cmp.attachInternalsCustomStates) { - if (customState.initialValue) { - statements.push(createStatesAddCall(cmp.attachInternalsMemberName, customState.name)); - } - } - } - - return statements; -} - -/** - * Create a `states.add()` call for initializing a custom state. - * - * Generates code like: - * ```ts - * this.internals.states.add('stateName'); - * ``` - * - * @param memberName the name of the ElementInternals property - * @param stateName the name of the custom state to add - * @returns an expression statement for the add call - */ -function createStatesAddCall(memberName: string, stateName: string): ts.ExpressionStatement { - return ts.factory.createExpressionStatement( - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createIdentifier(memberName)), - ts.factory.createIdentifier('states'), - ), - ts.factory.createIdentifier('add'), - ), - undefined, - [ts.factory.createStringLiteral(stateName)], - ), - ); -} diff --git a/src/compiler/transformers/component-native/native-meta.ts b/src/compiler/transformers/component-native/native-meta.ts deleted file mode 100644 index aebf6f43664..00000000000 --- a/src/compiler/transformers/component-native/native-meta.ts +++ /dev/null @@ -1,8 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { convertValueToLiteral, createStaticGetter } from '../transform-utils'; - -export const addNativeComponentMeta = (classMembers: ts.ClassElement[], cmp: d.ComponentCompilerMeta) => { - classMembers.push(createStaticGetter('is', convertValueToLiteral(cmp.tagName))); -}; diff --git a/src/compiler/transformers/decorators-to-static/attach-internals.ts b/src/compiler/transformers/decorators-to-static/attach-internals.ts deleted file mode 100644 index 5386918680f..00000000000 --- a/src/compiler/transformers/decorators-to-static/attach-internals.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { buildError } from '@utils'; -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { convertValueToLiteral, createStaticGetter, retrieveTsDecorators, tsPropDeclName } from '../transform-utils'; -import { isDecoratorNamed } from './decorator-utils'; - -/** - * Convert the attach internals decorator to static, saving the name of the - * decorated property so an `ElementInternals` object can be bound to it later - * on. - * - * The `@AttachInternals` decorator is used to indicate a field on a class - * where the return value of the `HTMLElement.attachInternals` method should be - * bound. This then allows component authors to use that interface to make their - * Stencil components rich participants in whatever `HTMLFormElement` instances - * they find themselves inside of in the future. - * - * The decorator also accepts an optional `states` option to define initial - * custom states that will be set on the `ElementInternals.states` CustomStateSet. - * Each state property can have a JSDoc comment that will be extracted for documentation. - * - * **Note**: this function will mutate the `newMembers` parameter in order to - * add new members to the class. - * - * @param diagnostics for reporting errors and warnings - * @param decoratedMembers the decorated members found on the class - * @param newMembers an out param for new class members - * @param typeChecker a TypeScript typechecker, needed for resolving the prop - * declaration name - * @param decoratorName the name of the decorator to look for - */ -export const attachInternalsDecoratorsToStatic = ( - diagnostics: d.Diagnostic[], - decoratedMembers: ts.ClassElement[], - newMembers: ts.ClassElement[], - typeChecker: ts.TypeChecker, - decoratorName: string, -) => { - const attachInternalsMembers = decoratedMembers.filter(ts.isPropertyDeclaration).filter((prop) => { - return !!retrieveTsDecorators(prop)?.find(isDecoratorNamed(decoratorName)); - }); - - // no decorated fields, return! - if (attachInternalsMembers.length === 0) { - return; - } - - // found too many! - if (attachInternalsMembers.length > 1) { - const error = buildError(diagnostics); - error.messageText = `Stencil does not support adding more than one AttachInternals() decorator to a component`; - return; - } - - const [decoratedProp] = attachInternalsMembers; - - const { staticName: name } = tsPropDeclName(decoratedProp, typeChecker); - - // Parse decorator options for custom states, extracting JSDoc comments from AST - const decorator = retrieveTsDecorators(decoratedProp)?.find(isDecoratorNamed(decoratorName)); - const customStates = parseCustomStatesFromDecorator(decorator, typeChecker); - - newMembers.push(createStaticGetter('attachInternalsMemberName', convertValueToLiteral(name))); - - // Only add custom states static getter if there are states defined - if (customStates.length > 0) { - newMembers.push(createStaticGetter('attachInternalsCustomStates', convertValueToLiteral(customStates))); - } -}; - -/** - * Parse custom states from the decorator AST, including JSDoc comments. - * - * Supports JSDoc comments on state properties: - * ```ts - * @AttachInternals({ - * states: { - * hovered: false, - * /** Whether is currently active */ - * active: true - * } - * }) - * ``` - * - * @param decorator the decorator node to parse - * @returns array of custom state metadata with docs - */ -function parseCustomStatesFromDecorator( - decorator: ts.Decorator | undefined, - typeChecker: ts.TypeChecker, -): d.ComponentCompilerCustomState[] { - if (!decorator || !ts.isCallExpression(decorator.expression)) { - return []; - } - - const [firstArg] = decorator.expression.arguments; - if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) { - return []; - } - - // Find the 'states' property in the options object - const statesProp = firstArg.properties.find( - (prop): prop is ts.PropertyAssignment => - ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'states', - ); - - if (!statesProp || !ts.isObjectLiteralExpression(statesProp.initializer)) { - return []; - } - - const customStates: d.ComponentCompilerCustomState[] = []; - - // Iterate through each property in the states object - for (const prop of statesProp.initializer.properties) { - if (!ts.isPropertyAssignment(prop)) { - continue; - } - - const stateName = ts.isIdentifier(prop.name) - ? prop.name.text - : ts.isStringLiteral(prop.name) - ? prop.name.text - : null; - - if (!stateName) { - continue; - } - - // Get the boolean value - let initialValue = false; - if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) { - initialValue = true; - } else if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) { - initialValue = false; - } - - // Extract JSDoc comment using TypeChecker (consistent with rest of codebase) - const symbol = typeChecker.getSymbolAtLocation(prop.name); - const docs = symbol ? ts.displayPartsToString(symbol.getDocumentationComment(typeChecker)) : ''; - - customStates.push({ - name: stateName, - initialValue, - docs, - }); - } - - return customStates; -} diff --git a/src/compiler/transformers/decorators-to-static/component-decorator.ts b/src/compiler/transformers/decorators-to-static/component-decorator.ts deleted file mode 100644 index f6e04f7ded7..00000000000 --- a/src/compiler/transformers/decorators-to-static/component-decorator.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { augmentDiagnosticWithNode, buildError, validateComponentTag } from '@utils'; -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { convertValueToLiteral, createStaticGetter, retrieveTsDecorators } from '../transform-utils'; -import { getDecoratorParameters } from './decorator-utils'; -import { styleToStatic } from './style-to-static'; - -/** - * Perform code generation to create new class members for a Stencil component - * which will drive the runtime functionality specified by various options - * passed to the `@Component` decorator. - * - * Inputs are validated (@see {@link validateComponent}) before code generation - * is performed. - * - * **Note**: in this function and in functions that it calls the `newMembers` - * parameter is treated as an out parameter and mutated, with new class members - * added to it. - * - * @param config a user-supplied config - * @param typeChecker a TypeScript type checker instance - * @param diagnostics an array of diagnostics for surfacing errors and warnings - * @param cmpNode a TypeScript class declaration node corresponding to a - * Stencil component - * @param newMembers an out param to hold newly generated class members - * @param componentDecorator the TypeScript decorator node for the `@Component` - * decorator - */ -export const componentDecoratorToStatic = ( - config: d.ValidatedConfig, - typeChecker: ts.TypeChecker, - diagnostics: d.Diagnostic[], - cmpNode: ts.ClassDeclaration, - newMembers: ts.ClassElement[], - componentDecorator: ts.Decorator, -) => { - const [componentOptions] = getDecoratorParameters(componentDecorator, typeChecker, diagnostics); - if (!componentOptions) { - return; - } - - if (!validateComponent(config, diagnostics, typeChecker, componentOptions, cmpNode, componentDecorator)) { - return; - } - - newMembers.push(createStaticGetter('is', convertValueToLiteral(componentOptions.tag.trim()))); - - if (componentOptions.shadow) { - newMembers.push(createStaticGetter('encapsulation', convertValueToLiteral('shadow'))); - - if (typeof componentOptions.shadow !== 'boolean') { - if (componentOptions.shadow.delegatesFocus === true) { - newMembers.push(createStaticGetter('delegatesFocus', convertValueToLiteral(true))); - } - if (componentOptions.shadow.slotAssignment === 'manual') { - newMembers.push(createStaticGetter('slotAssignment', convertValueToLiteral('manual'))); - } - } - } else if (componentOptions.scoped) { - newMembers.push(createStaticGetter('encapsulation', convertValueToLiteral('scoped'))); - } - - if (componentOptions.formAssociated === true) { - newMembers.push(createStaticGetter('formAssociated', convertValueToLiteral(true))); - } - - styleToStatic(newMembers, componentOptions); - - const assetsDirs = componentOptions.assetsDirs || []; - - if (assetsDirs.length > 0) { - newMembers.push(createStaticGetter('assetsDirs', convertValueToLiteral(assetsDirs))); - } -}; - -/** - * Perform validation on a Stencil component in preparation for some - * component-level code generation, checking that the class declaration node - * itself doesn't have any problems and that the options passed to the - * `@Component` decorator are valid. - * - * @param config a user-supplied config - * @param diagnostics an array of diagnostics for surfacing errors and warnings - * @param typeChecker a TypeScript type checker instance - * @param componentOptions the options passed to the `@Component` director - * @param cmpNode a TypeScript class declaration node corresponding to a - * Stencil component - * @param componentDecorator the TypeScript decorator node for the `@Component` - * decorator - * @returns whether or not the component is valid - */ -const validateComponent = ( - config: d.ValidatedConfig, - diagnostics: d.Diagnostic[], - typeChecker: ts.TypeChecker, - componentOptions: d.ComponentOptions, - cmpNode: ts.ClassDeclaration, - componentDecorator: ts.Decorator, -) => { - if (componentOptions.shadow && componentOptions.scoped) { - const err = buildError(diagnostics); - err.messageText = `Components cannot be "scoped" and "shadow" at the same time, they are mutually exclusive configurations.`; - augmentDiagnosticWithNode(err, findTagNode('scoped', componentDecorator)); - return false; - } - - // Validate slotAssignment is only used with shadow: true - if (typeof componentOptions.shadow === 'object' && componentOptions.shadow.slotAssignment) { - if (componentOptions.shadow.slotAssignment !== 'manual' && componentOptions.shadow.slotAssignment !== 'named') { - const err = buildError(diagnostics); - err.messageText = `The "slotAssignment" option must be either "manual" or "named".`; - augmentDiagnosticWithNode(err, findTagNode('slotAssignment', componentDecorator)); - return false; - } - } - - const constructor = cmpNode.members.find(ts.isConstructorDeclaration); - if (constructor && constructor.parameters.length > 0) { - const err = buildError(diagnostics); - err.messageText = `Classes decorated with @Component can not have a "constructor" that takes arguments. - All data required by a component must be passed by using class properties decorated with @Prop()`; - augmentDiagnosticWithNode(err, constructor.parameters[0]); - return false; - } - - // check if class has more than one decorator - const otherDecorator = retrieveTsDecorators(cmpNode)?.find((d) => d !== componentDecorator); - if (otherDecorator) { - const err = buildError(diagnostics); - err.messageText = `Classes decorated with @Component can not be decorated with more decorators. - Stencil performs extensive static analysis on top of your components in order to generate the necessary metadata, runtime decorators at the components level make this task very hard.`; - augmentDiagnosticWithNode(err, otherDecorator); - return false; - } - - const tag = componentOptions.tag; - if (typeof tag !== 'string' || tag.trim().length === 0) { - const err = buildError(diagnostics); - err.messageText = `tag missing in component decorator`; - augmentDiagnosticWithNode(err, componentDecorator); - return false; - } - - const tagError = validateComponentTag(tag); - if (tagError) { - const err = buildError(diagnostics); - err.messageText = `${tagError}. Please refer to https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name for more info.`; - augmentDiagnosticWithNode(err, findTagNode('tag', componentDecorator)); - return false; - } - - if (!config._isTesting) { - const nonTypeExports = typeChecker - .getExportsOfModule(typeChecker.getSymbolAtLocation(cmpNode.getSourceFile())) - .filter( - (symbol) => (symbol.flags & (ts.SymbolFlags.Interface | ts.SymbolFlags.TypeAlias | ts.SymbolFlags.Enum)) === 0, - ) - .filter((symbol) => symbol.name !== cmpNode.name.text); - - nonTypeExports.forEach((symbol) => { - const err = buildError(diagnostics); - err.messageText = `To allow efficient bundling, modules using @Component() can only have a single export which is the component class itself. - Any other exports should be moved to a separate file. - For further information check out: https://stenciljs.com/docs/module-bundling`; - const errorNode = symbol.valueDeclaration ? symbol.valueDeclaration : symbol.declarations[0]; - - augmentDiagnosticWithNode(err, errorNode); - }); - if (nonTypeExports.length > 0) { - return false; - } - } - return true; -}; - -/** - * Given a TypeScript Decorator node, try to find a property with a given name - * on an object possibly passed to it as an argument. If found, return the node - * to initialize the value, and if no such property is found return the - * decorator instead. - * - * @param propName the name of the argument to search for - * @param node the decorator node to search within - * @returns the initializer for the property (if found) or the decorator - */ -const findTagNode = (propName: string, node: ts.Decorator): ts.Decorator | ts.Expression => { - let out: ts.Decorator | ts.Expression = node; - - if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) { - const arg = node.expression.arguments[0]; - if (ts.isObjectLiteralExpression(arg)) { - arg.properties.forEach((prop) => { - if (ts.isPropertyAssignment(prop) && prop.name.getText() === propName) { - out = prop.initializer; - } - }); - } - } - - return out; -}; diff --git a/src/compiler/transformers/decorators-to-static/element-decorator.ts b/src/compiler/transformers/decorators-to-static/element-decorator.ts deleted file mode 100644 index cbae8b911d9..00000000000 --- a/src/compiler/transformers/decorators-to-static/element-decorator.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { buildError } from '@utils'; -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { createStaticGetter, retrieveTsDecorators } from '../transform-utils'; -import { isDecoratorNamed } from './decorator-utils'; - -export const elementDecoratorsToStatic = ( - diagnostics: d.Diagnostic[], - decoratedMembers: ts.ClassElement[], - newMembers: ts.ClassElement[], - decoratorName: string, -) => { - const elementRefs = decoratedMembers - .filter(ts.isPropertyDeclaration) - .map((prop) => parseElementDecorator(prop, decoratorName)) - .filter((element): element is string => !!element); - - if (elementRefs.length > 0) { - newMembers.push(createStaticGetter('elementRef', ts.factory.createStringLiteral(elementRefs[0]))); - if (elementRefs.length > 1) { - const error = buildError(diagnostics); - error.messageText = `It's not valid to add more than one Element() decorator`; - } - } -}; - -const parseElementDecorator = (prop: ts.PropertyDeclaration, decoratorName: string): string | null => { - const elementDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed(decoratorName)); - - if (elementDecorator == null) { - return null; - } - return prop.name.getText(); -}; diff --git a/src/compiler/transformers/static-to-meta/attach-internals.ts b/src/compiler/transformers/static-to-meta/attach-internals.ts deleted file mode 100644 index e9575a19d9e..00000000000 --- a/src/compiler/transformers/static-to-meta/attach-internals.ts +++ /dev/null @@ -1,40 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { getStaticValue } from '../transform-utils'; - -/** - * Parse the name of the form internals prop from a transformed Stencil - * component if present - * - * @param staticMembers class members for the Stencil component of interest - * @returns the parsed value, if present, else null - */ -export const parseAttachInternals = (staticMembers: ts.ClassElement[]): string | null => { - const parsedAttachInternalsMemberName = getStaticValue(staticMembers, 'attachInternalsMemberName'); - if (parsedAttachInternalsMemberName && typeof parsedAttachInternalsMemberName === 'string') { - return parsedAttachInternalsMemberName; - } else { - return null; - } -}; - -/** - * Parse custom states configuration from a transformed Stencil component - * - * @param staticMembers class members for the Stencil component of interest - * @returns array of custom state metadata, or empty array if none defined - */ -export const parseAttachInternalsCustomStates = ( - staticMembers: ts.ClassElement[], -): d.ComponentCompilerCustomState[] => { - const parsedCustomStates = getStaticValue(staticMembers, 'attachInternalsCustomStates'); - if (Array.isArray(parsedCustomStates)) { - return parsedCustomStates.map((state: { name: string; initialValue: boolean; docs?: string }) => ({ - name: String(state.name), - initialValue: Boolean(state.initialValue), - docs: state.docs ?? '', - })); - } - return []; -}; diff --git a/src/compiler/transformers/static-to-meta/class-extension.ts b/src/compiler/transformers/static-to-meta/class-extension.ts deleted file mode 100644 index c608f9ba3ad..00000000000 --- a/src/compiler/transformers/static-to-meta/class-extension.ts +++ /dev/null @@ -1,526 +0,0 @@ -import ts from 'typescript'; -import { augmentDiagnosticWithNode, buildWarn, normalizePath } from '@utils'; -import { tsResolveModuleName, tsGetSourceFile } from '../../sys/typescript/typescript-resolve-module'; -import { isStaticGetter } from '../transform-utils'; -import { parseStaticEvents } from './events'; -import { parseStaticListeners } from './listeners'; -import { parseStaticMethods } from './methods'; -import { parseStaticProps } from './props'; -import { parseStaticStates } from './states'; -import { parseStaticWatchers } from './watchers'; -import { parseStaticSerializers } from './serializers'; - -import type * as d from '../../../declarations'; -import { detectModernPropDeclarations } from '../detect-modern-prop-decls'; - -type DeDupeMember = - | d.ComponentCompilerProperty - | d.ComponentCompilerState - | d.ComponentCompilerMethod - | d.ComponentCompilerListener - | d.ComponentCompilerEvent - | d.ComponentCompilerChangeHandler; - -type DependentClass = { - classNode: ts.ClassDeclaration; - sourceFile: ts.SourceFile; - fileName: string; -}; - -/** - * Given two arrays of static members, return a new array containing only the - * members from the first array that are not present in the second array. - * This is used to de-dupe static members that are inherited from a parent class. - * - * @param dedupeMembers the array of static members to de-dupe - * @param staticMembers the array of static members to compare against - * @returns an array of static members that are not present in the second array - */ -const deDupeMembers = (dedupeMembers: T[], staticMembers: T[]) => { - return dedupeMembers.filter( - (s) => - !staticMembers.some((d) => { - if ((d as d.ComponentCompilerChangeHandler).methodName) { - return (d as any).methodName === (s as any).methodName; - } - return (d as any).name === (s as any).name; - }), - ); -}; - -/** - * Helper function to resolve and process an extended class from a module. - * This handles: - * 1. Resolving the module path - * 2. Getting the source file - * 3. Finding the class declaration - * 4. Adding to dependent classes tree - * - * @param compilerCtx - * @param buildCtx - * @param classDeclaration the current class being analyzed - * @param currentSource the source file of the current class - * @param moduleSpecifier the module path to resolve - * @param className the name of the class to find in the resolved module - * @param dependentClasses the array to add found classes to - * @param keepLooking whether to continue recursively looking for more extended classes - * @param typeChecker - * @param ogModule - * @returns the found class declaration or undefined - */ -function resolveAndProcessExtendedClass( - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - classDeclaration: ts.ClassDeclaration, - currentSource: ts.SourceFile, - moduleSpecifier: string, - className: string, - dependentClasses: DependentClass[], - keepLooking: boolean, - typeChecker: ts.TypeChecker, - ogModule: d.Module, -): ts.ClassDeclaration | undefined { - const foundFile = tsResolveModuleName(buildCtx.config, compilerCtx, moduleSpecifier, currentSource.fileName); - - if (!foundFile?.resolvedModule || !className) { - return undefined; - } - - // 1) resolve the module name to a file - let foundSource: ts.SourceFile = compilerCtx.moduleMap.get( - foundFile.resolvedModule.resolvedFileName, - )?.staticSourceFile; - - if (!foundSource) { - // Stencil only loads full-fledged component modules from node_modules collections, - // so if we didn't find the source file in the module map, - // let's create a temporary program and get the source file from there - foundSource = tsGetSourceFile(buildCtx.config, foundFile); - - if (!foundSource) { - // ts could not resolve the module. Likely because `allowJs` is not set to `true` - const err = buildWarn(buildCtx.diagnostics); - err.messageText = `Unable to resolve import "${moduleSpecifier}" from "${currentSource.fileName}". - This can happen when trying to resolve .js files and "allowJs" is not set to "true" in your tsconfig.json.`; - if (!buildCtx.config._isTesting) augmentDiagnosticWithNode(err, classDeclaration); - return undefined; - } - } - - // 2) get the exported declaration from the module - const matchedStatement = foundSource.statements.find(matchesNamedDeclaration(className)); - if (!matchedStatement) { - // we couldn't find the imported declaration as an exported statement in the module - const err = buildWarn(buildCtx.diagnostics); - err.messageText = `Unable to find "${className}" in the imported module "${moduleSpecifier}". - Please import class / mixin-factory declarations directly and not via barrel files.`; - if (!buildCtx.config._isTesting) augmentDiagnosticWithNode(err, classDeclaration); - return undefined; - } - - let foundClassDeclaration = matchedStatement - ? ts.isClassDeclaration(matchedStatement) - ? matchedStatement - : undefined - : undefined; - - if (!foundClassDeclaration && matchedStatement) { - // the found `extends` type does not resolve to a class declaration; - // if it's wrapped in a function - let's try and find it inside - foundClassDeclaration = findClassWalk(matchedStatement); - keepLooking = false; - } - - if (foundClassDeclaration && !dependentClasses.some((dc) => dc.classNode === foundClassDeclaration)) { - // 3) if we found the class declaration, push it and check if it itself extends from another class - dependentClasses.push({ - classNode: foundClassDeclaration, - sourceFile: foundSource, - fileName: foundFile.resolvedModule.resolvedFileName, - }); - - if (keepLooking) { - buildExtendsTree(compilerCtx, foundClassDeclaration, dependentClasses, typeChecker, buildCtx, ogModule); - } - } - - return foundClassDeclaration; -} - -/** - * A recursive function that walks the AST to find a class declaration. - * @param node the current AST node - * @param depth the current depth in the AST - * @param name optional name of the class to find - * @returns the found class declaration or undefined - */ -function findClassWalk(node?: ts.Node, name?: string): ts.ClassDeclaration | undefined { - if (!node) return undefined; - if (node && ts.isClassDeclaration(node) && (!name || node.name?.text === name)) { - return node; - } else if ( - node && - ts.isVariableDeclaration(node) && - // @ts-ignore - (!name || name === (node.name?.text || node.name?.escapedText)) && - node.initializer && - ts.isArrowFunction(node.initializer) - ) { - // handle case where class is wrapped in a mixin factory function - let found: ts.ClassDeclaration | undefined; - ts.forEachChild(node.initializer.body, (child) => { - if (found) return; - if (ts.isClassDeclaration(child)) found = child; - }); - return found; - } - let found: ts.ClassDeclaration | undefined; - - ts.forEachChild(node, (child) => { - if (found) return; - const result = findClassWalk(child, name); - if (result) found = result; - }); - - return found; -} - -/** - * A function that checks if a statement matches a named declaration. - * @param name the name to match - * @returns a function that checks if a statement is a named declaration - */ -function matchesNamedDeclaration(name: string) { - return function (stmt: ts.Statement): stmt is ts.ClassDeclaration | ts.FunctionDeclaration | ts.VariableStatement { - // ClassDeclaration: class Foo {} - if (ts.isClassDeclaration(stmt) && stmt.name?.text === name) { - return true; - } - - // FunctionDeclaration: function Foo() {} - if (ts.isFunctionDeclaration(stmt) && stmt.name?.text === name) { - return true; - } - - // VariableStatement: const Foo = ... - if (ts.isVariableStatement(stmt)) { - for (const decl of stmt.declarationList.declarations) { - if (ts.isIdentifier(decl.name) && decl.name.text === name) { - return true; - } - } - } - - return false; - }; -} - -/** - * Helper function to convert a .d.ts declaration file path to its corresponding - * .js source file path and get the source file from the compiler context. - * This is needed because in external projects the extended class may only be found as a .d.ts declaration. - * * - * @param declarationSourceFile the path to the .d.ts declaration file - * @param compilerCtx the current compiler context - * @returns the corresponding .js source file - */ -function convertDtsToJs(declarationSourceFile: string, compilerCtx: d.CompilerCtx): ts.SourceFile { - const jsPath = normalizePath(declarationSourceFile.replace(/\.d\.ts$/, '.js').replace('/types/', '/collection/')); - const jsModule = compilerCtx.moduleMap.get(jsPath); - return jsModule?.staticSourceFile as ts.SourceFile; -} - -/** - * A recursive function that builds a tree of classes that extend from each other. - * - * @param compilerCtx the current compiler context - * @param classDeclaration a class declaration to analyze - * @param dependentClasses a flat array tree of classes that extend from each other - * @param typeChecker the TypeScript type checker - * @param buildCtx the current build context - * @param ogModule the original module file of the class declaration - * @returns a flat array of classes that extend from each other, including the current class - */ -function buildExtendsTree( - compilerCtx: d.CompilerCtx, - classDeclaration: ts.ClassDeclaration, - dependentClasses: DependentClass[], - typeChecker: ts.TypeChecker, - buildCtx: d.BuildCtx, - ogModule: d.Module, -) { - const hasHeritageClauses = classDeclaration.heritageClauses; - if (!hasHeritageClauses?.length) return dependentClasses; - - const extendsClause = hasHeritageClauses.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword); - if (!extendsClause) return dependentClasses; - - let classIdentifiers: ts.Identifier[] = []; - let foundClassDeclaration: ts.ClassDeclaration | undefined; - // used when the class we found is wrapped in a mixin factory function - - // the extender ctor will be from a dynamic function argument - so we stop recursing - let keepLooking = true; - - extendsClause.types.forEach((type) => { - if ( - ts.isExpressionWithTypeArguments(type) && - ts.isCallExpression(type.expression) && - type.expression.expression.getText() === 'Mixin' - ) { - // handle mixin case: extends Mixin(SomeClassFactoryFunction1, SomeClassFactoryFunction2) - classIdentifiers = type.expression.arguments.filter(ts.isIdentifier); - } else if (ts.isIdentifier(type.expression)) { - // handle simple case: extends SomeClass - classIdentifiers = [type.expression]; - } - }); - - classIdentifiers.forEach((extendee) => { - try { - // happy path (normally 1 file level removed): the extends type resolves to a class declaration in another file - - const symbol = typeChecker?.getSymbolAtLocation(extendee); - const aliasedSymbol = symbol ? typeChecker.getAliasedSymbol(symbol) : undefined; - - let source = aliasedSymbol?.declarations?.[0].getSourceFile(); - let declarations: ts.Declaration[] | ts.Statement[] = aliasedSymbol?.declarations; - - if (source.fileName.endsWith('.d.ts')) { - source = convertDtsToJs(source.fileName, compilerCtx); - declarations = [...source.statements]; - } - - foundClassDeclaration = declarations?.find(ts.isClassDeclaration); - - if (!foundClassDeclaration) { - // the found `extends` type does not resolve to a class declaration; - // if it's wrapped in a function - let's try and find it inside - const node = declarations?.[0]; - foundClassDeclaration = findClassWalk(node); - if (!node) { - throw 'revert to sad path'; - } - keepLooking = false; - } - - if (foundClassDeclaration && !dependentClasses.some((dc) => dc.classNode === foundClassDeclaration)) { - const foundModule = compilerCtx.moduleMap.get(foundClassDeclaration.getSourceFile().fileName); - - if (foundModule) { - const source = foundModule.staticSourceFile as ts.SourceFile; - const sourceClass = findClassWalk(source, foundClassDeclaration.name?.getText()); - - if (sourceClass) { - dependentClasses.push({ classNode: sourceClass, sourceFile: source, fileName: source.fileName }); - if (keepLooking) { - buildExtendsTree(compilerCtx, foundClassDeclaration, dependentClasses, typeChecker, buildCtx, ogModule); - } - } - } - } - } catch (_e) { - // sad path (>1 levels removed or node_modules): the extends type does not resolve so let's find it manually: - - let currentSource: ts.SourceFile = classDeclaration.getSourceFile(); - let matchedStatement: ts.ClassDeclaration | ts.FunctionDeclaration | ts.VariableStatement; - - if (!currentSource) { - // fallback for jest tests where getSourceFile() is undefined - use the original classNode's source file - currentSource = ogModule?.staticSourceFile; - matchedStatement = findClassWalk(currentSource, extendee.getText()); - } else { - matchedStatement = currentSource.statements.find(matchesNamedDeclaration(extendee.getText())); - } - - if (!currentSource) { - // no source file :( - const err = buildWarn(buildCtx.diagnostics); - err.messageText = `Unable to find source file for class "${classDeclaration.name?.getText()}"`; - if (!buildCtx.config._isTesting) augmentDiagnosticWithNode(err, classDeclaration); - return; - } - - // try to see if we can find the class in the current source file first - if (matchedStatement && ts.isClassDeclaration(matchedStatement)) { - foundClassDeclaration = matchedStatement; - } else if (matchedStatement) { - // the found `extends` type does not resolve to a class declaration; - // if it's wrapped in a function - let's try and find it inside - foundClassDeclaration = findClassWalk(matchedStatement); - keepLooking = false; - } - - if (foundClassDeclaration && !dependentClasses.some((dc) => dc.classNode === foundClassDeclaration)) { - // we found the class declaration in the current module - dependentClasses.push({ - classNode: foundClassDeclaration, - sourceFile: currentSource, - fileName: currentSource.fileName, - }); - if (keepLooking) { - buildExtendsTree(compilerCtx, foundClassDeclaration, dependentClasses, typeChecker, buildCtx, ogModule); - } - return; - } - - // if not found, let's check the import statements - const importStatements = currentSource.statements.filter(ts.isImportDeclaration); - importStatements.forEach((statement) => { - // 1) loop through import declarations in the current source file - if (statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause?.namedBindings)) { - statement.importClause?.namedBindings.elements.forEach((element) => { - // 2) loop through the named bindings of the import declaration - - if (element.name.getText() === extendee.getText()) { - // 3) check the name matches the `extends` type expression - const className = element.propertyName?.getText() || element.name.getText(); - const moduleSpecifier = statement.moduleSpecifier.getText().replaceAll(/['"]/g, ''); - - resolveAndProcessExtendedClass( - compilerCtx, - buildCtx, - classDeclaration, - currentSource, - moduleSpecifier, - className, - dependentClasses, - keepLooking, - typeChecker, - ogModule, - ); - } - }); - } - }); - - if (!importStatements.length) { - // we're in a cjs module (probably in a Jest test) - loop through require modules statements - const requireStatements = currentSource.statements.filter(ts.isVariableStatement); - requireStatements.forEach((statement) => { - statement.declarationList.declarations.forEach((declaration) => { - if ( - declaration.initializer && - ts.isCallExpression(declaration.initializer) && - ts.isIdentifier(declaration.initializer.expression) && - declaration.initializer.expression.escapedText === 'require' && - declaration.initializer.arguments.length === 1 && - ts.isStringLiteral(declaration.initializer.arguments[0]) - ) { - const moduleSpecifier = declaration.initializer.arguments[0].text.replaceAll(/['"]/g, ''); - const className = extendee.getText(); - - resolveAndProcessExtendedClass( - compilerCtx, - buildCtx, - classDeclaration, - currentSource, - moduleSpecifier, - className, - dependentClasses, - keepLooking, - typeChecker, - ogModule, - ); - } - }); - }); - } - } - }); - - return dependentClasses; -} - -/** - * Given a class declaration, this function will analyze its heritage clauses - * to find any extended classes, and then parse the static members of those - * extended classes to merge them into the current class's metadata. - * - * @param compilerCtx - * @param typeChecker - * @param buildCtx - * @param cmpNode the extending class declaration - * @param staticMembers the static members of the extending class to merge with the extended class members - * @param moduleFile the module file of the extending class - * @returns an object containing merged metadata from extended classes - */ -export function mergeExtendedClassMeta( - compilerCtx: d.CompilerCtx, - typeChecker: ts.TypeChecker, - buildCtx: d.BuildCtx, - cmpNode: ts.ClassDeclaration, - staticMembers: ts.ClassElement[], - moduleFile: d.Module, -) { - const tree = buildExtendsTree(compilerCtx, cmpNode, [], typeChecker, buildCtx, moduleFile); - let hasMixin = false; - let doesExtend = false; - let properties = parseStaticProps(staticMembers); - let states = parseStaticStates(staticMembers); - let methods = parseStaticMethods(staticMembers); - let listeners = parseStaticListeners(staticMembers); - let events = parseStaticEvents(staticMembers); - let watchers = parseStaticWatchers(staticMembers); - let classMethods = cmpNode.members.filter(ts.isMethodDeclaration); - let serializers = parseStaticSerializers(staticMembers, 'serializers'); - let deserializers = parseStaticSerializers(staticMembers, 'deserializers'); - - tree.forEach((extendedClass) => { - const extendedStaticMembers = extendedClass.classNode.members.filter(isStaticGetter); - const mixinProps = parseStaticProps(extendedStaticMembers) ?? []; - const mixinStates = parseStaticStates(extendedStaticMembers) ?? []; - const mixinMethods = parseStaticMethods(extendedStaticMembers) ?? []; - const mixinEvents = parseStaticEvents(extendedStaticMembers) ?? []; - const isMixin = - mixinProps.length > 0 || mixinStates.length > 0 || mixinMethods.length > 0 || mixinEvents.length > 0; - const module = compilerCtx.moduleMap.get(extendedClass.fileName); - if (!module) return; - - module.isMixin = isMixin; - module.isExtended = true; - doesExtend = true; - - if ( - (mixinProps.length > 0 || mixinStates.length > 0) && - !detectModernPropDeclarations(extendedClass.classNode, extendedClass.sourceFile) - ) { - const err = buildWarn(buildCtx.diagnostics); - const target = buildCtx.config.tsCompilerOptions?.target; - err.messageText = `Component classes can only extend from other Stencil decorated base classes when targetting more modern JavaScript (ES2022 and above). - ${target ? `Your current TypeScript configuration is set to target \`${ts.ScriptTarget[target]}\`.` : ''} Please amend your tsconfig.json.`; - if (!buildCtx.config._isTesting) augmentDiagnosticWithNode(err, extendedClass.classNode); - } - - properties = [...deDupeMembers(mixinProps, properties), ...properties]; - states = [...deDupeMembers(mixinStates, states), ...states]; - methods = [...deDupeMembers(mixinMethods, methods), ...methods]; - events = [...deDupeMembers(mixinEvents, events), ...events]; - listeners = [...deDupeMembers(parseStaticListeners(extendedStaticMembers) ?? [], listeners), ...listeners]; - watchers = [...deDupeMembers(parseStaticWatchers(extendedStaticMembers) ?? [], watchers), ...watchers]; - serializers = [ - ...deDupeMembers(parseStaticSerializers(extendedStaticMembers, 'serializers') ?? [], serializers), - ...serializers, - ]; - deserializers = [ - ...deDupeMembers(parseStaticSerializers(extendedStaticMembers, 'deserializers') ?? [], deserializers), - ...deserializers, - ]; - classMethods = [...classMethods, ...(extendedClass.classNode.members.filter(ts.isMethodDeclaration) ?? [])]; - - if (isMixin) hasMixin = true; - }); - - return { - hasMixin, - doesExtend, - properties, - states, - methods, - listeners, - events, - watchers, - classMethods, - serializers, - deserializers, - }; -} diff --git a/src/compiler/transformers/static-to-meta/component.ts b/src/compiler/transformers/static-to-meta/component.ts deleted file mode 100644 index 794abf989ee..00000000000 --- a/src/compiler/transformers/static-to-meta/component.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { augmentDiagnosticWithNode, buildWarn, join, normalizePath, relative, unique } from '@utils'; -import { dirname, isAbsolute } from 'path'; -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { addComponentMetaStatic } from '../add-component-meta-static'; -import { setComponentBuildConditionals } from '../component-build-conditionals'; -import { detectModernPropDeclarations } from '../detect-modern-prop-decls'; -import { getComponentTagName, getStaticValue, isInternal, isStaticGetter, serializeSymbol } from '../transform-utils'; -import { parseAttachInternals, parseAttachInternalsCustomStates } from './attach-internals'; -import { parseCallExpression } from './call-expression'; -import { parseClassMethods } from './class-methods'; -import { parseStaticElementRef } from './element-ref'; -import { parseStaticEncapsulation, parseStaticShadowDelegatesFocus, parseStaticSlotAssignment } from './encapsulation'; -import { parseFormAssociated } from './form-associated'; -import { parseStringLiteral } from './string-literal'; -import { parseStaticStyles } from './styles'; -import { mergeExtendedClassMeta } from './class-extension'; - -const BLACKLISTED_COMPONENT_METHODS = [ - /** - * If someone would define a getter called "shadowRoot" on a component - * this would cause issues when Stencil tries to hydrate the component. - */ - 'shadowRoot', -]; - -/** - * Given a {@see ts.ClassDeclaration} which represents a Stencil component - * class declaration, parse and format various pieces of data about static class - * members which we use in the compilation process. - * - * This performs some checks that this class is indeed a Stencil component - * and, if it is, will perform a side-effect, adding an object containing - * metadata about the component to the module map and the node map. - * - * Additionally, it will optionally transform the supplied class declaration - * node to add a static getter for the component metadata if the transformation - * options specify to do so. - * - * @param compilerCtx the current compiler context - * @param typeChecker a TypeScript type checker instance - * @param cmpNode the TypeScript class declaration for the component - * @param moduleFile Stencil's IR for a module, used here as an out param - * @param buildCtx the current build context, used to surface diagnostics - * @param transformOpts options which control various aspects of the - * transformation - * @returns the TypeScript class declaration IR instance with which the - * function was called - */ -export const parseStaticComponentMeta = ( - compilerCtx: d.CompilerCtx, - typeChecker: ts.TypeChecker, - cmpNode: ts.ClassDeclaration, - moduleFile: d.Module, - buildCtx: d.BuildCtx, - transformOpts?: d.TransformOptions, -): ts.ClassDeclaration => { - if (cmpNode.members == null) { - return cmpNode; - } - const staticMembers = cmpNode.members.filter(isStaticGetter); - const tagName = getComponentTagName(staticMembers); - if (tagName == null) { - return cmpNode; - } - - const { - doesExtend, - properties, - states, - methods, - listeners, - events, - watchers, - classMethods, - serializers, - deserializers, - } = mergeExtendedClassMeta(compilerCtx, typeChecker, buildCtx, cmpNode, staticMembers, moduleFile); - const symbol = typeChecker ? typeChecker.getSymbolAtLocation(cmpNode.name) : undefined; - const docs = serializeSymbol(typeChecker, symbol); - const isCollectionDependency = moduleFile.isCollectionDependency; - const encapsulation = parseStaticEncapsulation(staticMembers); - const cmp: d.ComponentCompilerMeta = { - attachInternalsMemberName: parseAttachInternals(staticMembers), - attachInternalsCustomStates: parseAttachInternalsCustomStates(staticMembers), - formAssociated: parseFormAssociated(staticMembers), - tagName: tagName, - excludeFromCollection: moduleFile.excludeFromCollection, - isCollectionDependency, - componentClassName: cmpNode.name ? cmpNode.name.text : '', - elementRef: parseStaticElementRef(staticMembers), - encapsulation, - shadowDelegatesFocus: !!parseStaticShadowDelegatesFocus(encapsulation, staticMembers), - slotAssignment: parseStaticSlotAssignment(encapsulation, staticMembers), - properties, - virtualProperties: parseVirtualProps(docs), - states, - methods, - listeners, - events, - watchers, - doesExtend, - styles: parseStaticStyles(compilerCtx, tagName, moduleFile.sourceFilePath, isCollectionDependency, staticMembers), - internal: isInternal(docs), - assetsDirs: parseAssetsDirs(staticMembers, moduleFile.jsFilePath), - styleDocs: [], - docs, - jsFilePath: moduleFile.jsFilePath, - sourceFilePath: moduleFile.sourceFilePath, - sourceMapPath: moduleFile.sourceMapPath, - serializers, - deserializers, - - hasAttributeChangedCallbackFn: false, - hasComponentWillLoadFn: false, - hasComponentDidLoadFn: false, - hasComponentShouldUpdateFn: false, - hasComponentWillUpdateFn: false, - hasComponentDidUpdateFn: false, - hasComponentWillRenderFn: false, - hasComponentDidRenderFn: false, - hasConnectedCallbackFn: false, - hasDeserializer: false, - hasDisconnectedCallbackFn: false, - hasElement: false, - hasEvent: false, - hasLifecycle: false, - hasListener: false, - hasListenerTarget: false, - hasListenerTargetWindow: false, - hasListenerTargetDocument: false, - hasListenerTargetBody: false, - hasListenerTargetParent: false, - hasMember: false, - hasMethod: false, - hasMode: false, - hasModernPropertyDecls: false, - hasAttribute: false, - hasProp: false, - hasPropNumber: false, - hasPropBoolean: false, - hasPropString: false, - hasPropMutable: false, - hasReflect: false, - hasRenderFn: false, - hasSerializer: false, - hasSlot: false, - hasState: false, - hasStyle: false, - hasVdomAttribute: false, - hasVdomXlink: false, - hasVdomClass: false, - hasVdomFunctional: false, - hasVdomKey: false, - hasVdomListener: false, - hasVdomPropOrAttr: false, - hasVdomRef: false, - hasVdomRender: false, - hasVdomStyle: false, - hasVdomText: false, - hasWatchCallback: false, - isPlain: false, - htmlAttrNames: [], - htmlTagNames: [], - htmlParts: [], - isUpdateable: false, - potentialCmpRefs: [], - - dependents: [], - dependencies: [], - directDependents: [], - directDependencies: [], - }; - - const visitComponentChildNode = (node: ts.Node, buildCtx: d.BuildCtx) => { - validateComponentMembers(node, buildCtx); - - if (ts.isCallExpression(node)) { - parseCallExpression(cmp, node, typeChecker); - } else if (ts.isStringLiteral(node)) { - parseStringLiteral(cmp, node); - } - node.forEachChild((child) => visitComponentChildNode(child, buildCtx)); - }; - visitComponentChildNode(cmpNode, buildCtx); - parseClassMethods(classMethods, cmp); - - cmp.hasModernPropertyDecls = detectModernPropDeclarations(cmpNode) || doesExtend; - cmp.htmlAttrNames = unique(cmp.htmlAttrNames); - cmp.htmlTagNames = unique(cmp.htmlTagNames); - cmp.hasSlot = cmp.hasSlot || cmp.htmlTagNames.includes('slot'); - cmp.potentialCmpRefs = unique(cmp.potentialCmpRefs); - setComponentBuildConditionals(cmp); - - if (transformOpts && transformOpts.componentMetadata === 'compilerstatic') { - cmpNode = addComponentMetaStatic(cmpNode, cmp); - } - - // add to module map - const foundIndex = moduleFile.cmps.findIndex( - (c) => c.tagName === cmp.tagName && c.sourceFilePath === cmp.sourceFilePath, - ); - if (foundIndex > -1) moduleFile.cmps[foundIndex] = cmp; - else moduleFile.cmps.push(cmp); - - // add to node map - compilerCtx.nodeMap.set(cmpNode, cmp); - - return cmpNode; -}; - -const validateComponentMembers = (node: ts.Node, buildCtx: d.BuildCtx) => { - /** - * validate if: - */ - if ( - /** - * the component has a getter called "shadowRoot" - */ - ts.isGetAccessorDeclaration(node) && - ts.isIdentifier(node.name) && - typeof node.name.escapedText === 'string' && - BLACKLISTED_COMPONENT_METHODS.includes(node.name.escapedText) && - /** - * the parent node is a class declaration - */ - node.parent && - ts.isClassDeclaration(node.parent) - ) { - const propName = node.name.escapedText; - const decorator = ts.getDecorators(node.parent)[0]; - /** - * the class is actually a Stencil component, has a decorator with a property named "tag" - */ - if ( - ts.isCallExpression(decorator.expression) && - decorator.expression.arguments.length === 1 && - ts.isObjectLiteralExpression(decorator.expression.arguments[0]) && - decorator.expression.arguments[0].properties.some( - (prop) => ts.isPropertyAssignment(prop) && prop.name.getText() === 'tag', - ) - ) { - const componentName = node.parent.name.getText(); - const err = buildWarn(buildCtx.diagnostics); - err.messageText = `The component "${componentName}" has a getter called "${propName}". This getter is reserved for use by Stencil components and should not be defined by the user.`; - augmentDiagnosticWithNode(err, node); - } - } -}; - -const parseVirtualProps = (docs: d.CompilerJsDoc) => { - return docs.tags - .filter(({ name }) => name === 'virtualProp') - .map(parseVirtualProp) - .filter((prop) => !!prop); -}; - -const parseVirtualProp = (tag: d.CompilerJsDocTagInfo): d.ComponentCompilerVirtualProperty => { - const results = /^\s*(?:\{([^}]+)\}\s+)?(\w+)\s+-\s+(.*)$/.exec(tag.text); - if (!results) { - return undefined; - } - const [, type, name, docs] = results; - return { - type: type == null ? 'any' : type.trim(), - name: name.trim(), - docs: docs.trim(), - }; -}; - -const parseAssetsDirs = (staticMembers: ts.ClassElement[], componentFilePath: string): d.AssetsMeta[] => { - const dirs: string[] = getStaticValue(staticMembers, 'assetsDirs') || []; - const componentDir = normalizePath(dirname(componentFilePath)); - - return dirs.map((dir) => { - // get the relative path from the component file to the assets directory - dir = normalizePath(dir.trim()); - - let absolutePath = dir; - let cmpRelativePath = dir; - if (isAbsolute(dir)) { - // if this is an absolute path already, let's convert it to be relative - cmpRelativePath = relative(componentDir, dir); - } else { - // create the absolute path to the asset dir - absolutePath = join(componentDir, dir); - } - return { - absolutePath, - cmpRelativePath, - originalComponentPath: dir, - }; - }); -}; diff --git a/src/compiler/transformers/static-to-meta/encapsulation.ts b/src/compiler/transformers/static-to-meta/encapsulation.ts deleted file mode 100644 index b08782c72bf..00000000000 --- a/src/compiler/transformers/static-to-meta/encapsulation.ts +++ /dev/null @@ -1,63 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { getStaticValue } from '../transform-utils'; - -/** - * Find and return the type of encapsulation that a component has based on the return value of its static getter of the - * same name. - * - * If no encapsulation static getter is found, or if the found encapsulation getter return value is not an accepted - * value, 'none' is returned. - * - * @param staticMembers a collection of static getters to search - * @returns the encapsulation mode to use for a component - */ -export const parseStaticEncapsulation = (staticMembers: ts.ClassElement[]): d.Encapsulation => { - let encapsulation: string = getStaticValue(staticMembers, 'encapsulation'); - - if (typeof encapsulation === 'string') { - encapsulation = encapsulation.toLowerCase().trim(); - if (encapsulation === 'shadow' || encapsulation === 'scoped') { - return encapsulation; - } - } - - return 'none'; -}; - -/** - * Find and return if `delegatesFocus` is enabled for a component based on the return value of its static getter of the - * same name. - * - * @param encapsulation the encapsulation mode to use for a component - * @param staticMembers a collection of static getters to search - * @returns when `encapsulation` is 'shadow', return `true` if the static getter returns true. If the static getter - * returns `false` or does not exist, return `false`. If `encapsulation` is not 'shadow', return `null`, regardless of - * the static getter's existence/return value. - */ -export const parseStaticShadowDelegatesFocus = ( - encapsulation: string, - staticMembers: ts.ClassElement[], -): boolean | null => { - if (encapsulation === 'shadow') { - const delegatesFocus: boolean = getStaticValue(staticMembers, 'delegatesFocus'); - return !!delegatesFocus; - } - return null; -}; - -/** - * Find and return the `slotAssignment` mode for a component. - * - * @param encapsulation the encapsulation mode to use for a component - * @param staticMembers a collection of static getters to search - * @returns `manual` if explicitly set. Otherwise `null`. - */ -export const parseStaticSlotAssignment = (encapsulation: string, staticMembers: ts.ClassElement[]): 'manual' | null => { - if (encapsulation === 'shadow') { - const slotAssignment: string = getStaticValue(staticMembers, 'slotAssignment'); - return slotAssignment === 'manual' ? 'manual' : null; - } - return null; -}; diff --git a/src/compiler/transformers/static-to-meta/events.ts b/src/compiler/transformers/static-to-meta/events.ts deleted file mode 100644 index bbdb985aaf2..00000000000 --- a/src/compiler/transformers/static-to-meta/events.ts +++ /dev/null @@ -1,24 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { getStaticValue, isInternal } from '../transform-utils'; - -export const parseStaticEvents = (staticMembers: ts.ClassElement[]): d.ComponentCompilerEvent[] => { - const parsedEvents: d.ComponentCompilerEvent[] = getStaticValue(staticMembers, 'events'); - if (!parsedEvents || parsedEvents.length === 0) { - return []; - } - - return parsedEvents.map((parsedEvent) => { - return { - name: parsedEvent.name, - method: parsedEvent.method, - bubbles: parsedEvent.bubbles, - cancelable: parsedEvent.cancelable, - composed: parsedEvent.composed, - docs: parsedEvent.docs, - complexType: parsedEvent.complexType, - internal: isInternal(parsedEvent.docs), - }; - }); -}; diff --git a/src/compiler/transformers/static-to-meta/methods.ts b/src/compiler/transformers/static-to-meta/methods.ts deleted file mode 100644 index 67be9286771..00000000000 --- a/src/compiler/transformers/static-to-meta/methods.ts +++ /dev/null @@ -1,25 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { getStaticValue, isInternal } from '../transform-utils'; - -export const parseStaticMethods = (staticMembers: ts.ClassElement[]): d.ComponentCompilerMethod[] => { - const parsedMethods: { [key: string]: d.ComponentCompilerStaticMethod } = getStaticValue(staticMembers, 'methods'); - if (!parsedMethods) { - return []; - } - - const methodNames = Object.keys(parsedMethods); - if (methodNames.length === 0) { - return []; - } - - return methodNames.map((methodName) => { - return { - name: methodName, - docs: parsedMethods[methodName].docs, - complexType: parsedMethods[methodName].complexType, - internal: isInternal(parsedMethods[methodName].docs), - }; - }); -}; diff --git a/src/compiler/transformers/static-to-meta/string-literal.ts b/src/compiler/transformers/static-to-meta/string-literal.ts deleted file mode 100644 index 9d6a56c7761..00000000000 --- a/src/compiler/transformers/static-to-meta/string-literal.ts +++ /dev/null @@ -1,14 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; - -export const parseStringLiteral = (m: d.Module | d.ComponentCompilerMeta, node: ts.StringLiteral) => { - if (typeof node.text === 'string' && node.text.includes(' { - const styles: d.StyleCompiler[] = []; - const styleUrlsProp = isCollectionDependency ? 'styleUrls' : 'originalStyleUrls'; - const parsedStyleUrls = getStaticValue(staticMembers, styleUrlsProp) as d.CompilerModeStyles; - - let parsedStyle = getStaticValue(staticMembers, 'styles'); - - if (parsedStyle) { - if (typeof parsedStyle === 'string') { - // styles: 'div { padding: 10px }' - parsedStyle = parsedStyle.trim(); - if (parsedStyle.length > 0) { - styles.push({ - modeName: DEFAULT_STYLE_MODE, - styleId: null, - styleStr: parsedStyle, - styleIdentifier: null, - externalStyles: [], - }); - compilerCtx.styleModeNames.add(DEFAULT_STYLE_MODE); - } - } else if ((parsedStyle as ConvertIdentifier).__identifier) { - styles.push(parseStyleIdentifier(parsedStyle, DEFAULT_STYLE_MODE)); - compilerCtx.styleModeNames.add(DEFAULT_STYLE_MODE); - } else if (typeof parsedStyle === 'object') { - Object.keys(parsedStyle).forEach((modeName) => { - const parsedStyleMode = parsedStyle[modeName]; - if (typeof parsedStyleMode === 'string') { - styles.push({ - modeName: modeName, - styleId: null, - styleStr: parsedStyleMode, - styleIdentifier: null, - externalStyles: [], - }); - } else { - styles.push(parseStyleIdentifier(parsedStyleMode, modeName)); - } - compilerCtx.styleModeNames.add(modeName); - }); - } - } - - if (parsedStyleUrls && typeof parsedStyleUrls === 'object') { - Object.keys(parsedStyleUrls).forEach((modeName) => { - const externalStyles: d.ExternalStyleCompiler[] = []; - const styleObj = parsedStyleUrls[modeName]; - styleObj.forEach((styleUrl) => { - if (typeof styleUrl === 'string' && styleUrl.trim().length > 0) { - externalStyles.push({ - absolutePath: null, - relativePath: null, - originalComponentPath: styleUrl.trim(), - }); - } - }); - - if (externalStyles.length > 0) { - const style: d.StyleCompiler = { - modeName: modeName, - styleId: null, - styleStr: null, - styleIdentifier: null, - externalStyles: externalStyles, - }; - - styles.push(style); - compilerCtx.styleModeNames.add(modeName); - } - }); - } - - normalizeStyles(tagName, componentFilePath, styles); - - return sortBy(styles, (s) => s.modeName); -}; - -const parseStyleIdentifier = (parsedStyle: ConvertIdentifier, modeName: string) => { - const style: d.StyleCompiler = { - modeName: modeName, - styleId: null, - styleStr: null, - styleIdentifier: parsedStyle.__escapedText, - externalStyles: [], - }; - return style; -}; diff --git a/src/compiler/transformers/static-to-meta/watchers.ts b/src/compiler/transformers/static-to-meta/watchers.ts deleted file mode 100644 index 02466ebbbae..00000000000 --- a/src/compiler/transformers/static-to-meta/watchers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../../declarations'; -import { getStaticValue } from '../transform-utils'; - -export const parseStaticWatchers = (staticMembers: ts.ClassElement[]): d.ComponentCompilerChangeHandler[] => { - const parsedWatchers: d.ComponentCompilerChangeHandler[] = getStaticValue(staticMembers, 'watchers'); - if (!parsedWatchers || parsedWatchers.length === 0) { - return []; - } - - return parsedWatchers.map((parsedWatch) => { - return { - propName: parsedWatch.propName, - methodName: parsedWatch.methodName, - handlerOptions: parsedWatch.handlerOptions, - }; - }); -}; diff --git a/src/compiler/transformers/test/add-component-meta-proxy.spec.ts b/src/compiler/transformers/test/add-component-meta-proxy.spec.ts deleted file mode 100644 index 1dadbd534a1..00000000000 --- a/src/compiler/transformers/test/add-component-meta-proxy.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import ts from 'typescript'; - -import { stubComponentCompilerMeta } from '../../../compiler/types/tests/ComponentCompilerMeta.stub'; -import type * as d from '../../../declarations'; -import * as FormatComponentRuntimeMeta from '../../../utils/format-component-runtime-meta'; -import { createClassMetadataProxy } from '../add-component-meta-proxy'; -import { HTML_ELEMENT } from '../core-runtime-apis'; -import * as TransformUtils from '../transform-utils'; - -describe('add-component-meta-proxy', () => { - describe('createClassMetadataProxy()', () => { - let classExpr: ts.ClassExpression; - let htmlElementHeritageClause: ts.HeritageClause; - let literalMetadata: ts.StringLiteral; - - let formatComponentRuntimeMetaSpy: jest.SpyInstance< - ReturnType, - Parameters - >; - let convertValueToLiteralSpy: jest.SpyInstance< - ReturnType, - Parameters - >; - - beforeEach(() => { - htmlElementHeritageClause = ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ - ts.factory.createExpressionWithTypeArguments(ts.factory.createIdentifier(HTML_ELEMENT), []), - ]); - - classExpr = ts.factory.createClassExpression( - undefined, - 'MyComponent', - undefined, - [htmlElementHeritageClause], - [], - ); - literalMetadata = ts.factory.createStringLiteral('MyComponent'); - - formatComponentRuntimeMetaSpy = jest.spyOn(FormatComponentRuntimeMeta, 'formatComponentRuntimeMeta'); - formatComponentRuntimeMetaSpy.mockImplementation( - (_compilerMeta: d.ComponentCompilerMeta, _includeMethods: boolean) => [0, 'tag-name'], - ); - - convertValueToLiteralSpy = jest.spyOn(TransformUtils, 'convertValueToLiteral'); - convertValueToLiteralSpy.mockImplementation((_compactMeta: d.ComponentRuntimeMetaCompact) => literalMetadata); - }); - - afterEach(() => { - formatComponentRuntimeMetaSpy.mockRestore(); - convertValueToLiteralSpy.mockRestore(); - }); - - it('returns a call expression', () => { - const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr); - - expect(ts.isCallExpression(result)).toBe(true); - }); - - it('wraps the initializer in PROXY_CUSTOM_ELEMENT', () => { - const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr); - - expect((result.expression as ts.Identifier).escapedText).toBe('___stencil_proxyCustomElement'); - }); - - it("doesn't add any type arguments to the call", () => { - const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr); - - expect(result.typeArguments).toHaveLength(0); - }); - - it('adds the correct arguments to the PROXY_CUSTOM_ELEMENT call', () => { - const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr); - - expect(result.arguments).toHaveLength(2); - expect(result.arguments[0]).toBe(classExpr); - expect(result.arguments[1]).toBe(literalMetadata); - }); - - it('includes the heritage clause', () => { - const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr); - - expect(result.arguments.length).toBeGreaterThanOrEqual(1); - const createdClassExpression = result.arguments[0]; - - expect(ts.isClassExpression(createdClassExpression)).toBe(true); - expect((createdClassExpression as ts.ClassExpression).heritageClauses).toHaveLength(1); - expect((createdClassExpression as ts.ClassExpression).heritageClauses[0]).toBe(htmlElementHeritageClause); - }); - }); -}); diff --git a/src/compiler/transformers/test/decorator-utils.spec.ts b/src/compiler/transformers/test/decorator-utils.spec.ts deleted file mode 100644 index ef9dfc109b1..00000000000 --- a/src/compiler/transformers/test/decorator-utils.spec.ts +++ /dev/null @@ -1,215 +0,0 @@ -import ts from 'typescript'; - -import { getDecoratorParameters } from '../decorators-to-static/decorator-utils'; - -describe('decorator utils', () => { - describe('getDecoratorParameters', () => { - it('should return an empty array for decorator with no arguments', () => { - const decorator: ts.Decorator = { - expression: ts.factory.createIdentifier('DecoratorName'), - } as unknown as ts.Decorator; - - const typeCheckerMock = {} as ts.TypeChecker; - const result = getDecoratorParameters(decorator, typeCheckerMock); - - expect(result).toEqual([]); - }); - - it('should return correct parameters for decorator with multiple string arguments', () => { - const decorator: ts.Decorator = { - expression: ts.factory.createCallExpression(ts.factory.createIdentifier('DecoratorName'), undefined, [ - ts.factory.createStringLiteral('arg1'), - ts.factory.createStringLiteral('arg2'), - ]), - } as unknown as ts.Decorator; - - const typeCheckerMock = {} as ts.TypeChecker; - const result = getDecoratorParameters(decorator, typeCheckerMock); - - expect(result).toEqual(['arg1', 'arg2']); - }); - - it('should return enum value for enum member used in decorator', () => { - const typeCheckerMock = { - getTypeAtLocation: jest.fn(() => ({ - value: 'arg1', - isLiteral: () => true, - })), - } as unknown as ts.TypeChecker; - - const decorator: ts.Decorator = { - expression: ts.factory.createCallExpression(ts.factory.createIdentifier('DecoratorName'), undefined, [ - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('EnumName'), - ts.factory.createIdentifier('EnumMemberName'), - ), - ]), - } as unknown as ts.Decorator; - - const result = getDecoratorParameters(decorator, typeCheckerMock); - - expect(result).toEqual(['arg1']); - }); - - describe('resolveVar', () => { - it('should resolve const variable with string literal', () => { - const myEventIdentifier = ts.factory.createIdentifier('MY_EVENT'); - const variableDeclaration = ts.factory.createVariableDeclaration( - myEventIdentifier, - undefined, - undefined, - ts.factory.createStringLiteral('myEvent'), - ); - - const symbolMock = { - valueDeclaration: variableDeclaration, - }; - - const typeCheckerMock = { - getSymbolAtLocation: jest.fn(() => symbolMock), - getTypeAtLocation: jest.fn(() => ({ - value: 'myEvent', - isLiteral: () => true, - })), - } as unknown as ts.TypeChecker; - - const decorator: ts.Decorator = { - expression: ts.factory.createCallExpression(ts.factory.createIdentifier('Listen'), undefined, [ - ts.factory.createCallExpression(ts.factory.createIdentifier('resolveVar'), undefined, [myEventIdentifier]), - ]), - } as unknown as ts.Decorator; - - const result = getDecoratorParameters(decorator, typeCheckerMock); - - expect(result).toEqual(['myEvent']); - }); - - it('should resolve const variable with as const assertion', () => { - const myEventIdentifier = ts.factory.createIdentifier('MY_EVENT'); - const variableDeclaration = ts.factory.createVariableDeclaration( - myEventIdentifier, - undefined, - undefined, - ts.factory.createStringLiteral('myEvent'), - ); - - const symbolMock = { - valueDeclaration: variableDeclaration, - }; - - const typeCheckerMock = { - getSymbolAtLocation: jest.fn(() => symbolMock), - getTypeAtLocation: jest.fn(() => ({ - value: 'myEvent', - isLiteral: () => true, - })), - } as unknown as ts.TypeChecker; - - const decorator: ts.Decorator = { - expression: ts.factory.createCallExpression(ts.factory.createIdentifier('Listen'), undefined, [ - ts.factory.createCallExpression(ts.factory.createIdentifier('resolveVar'), undefined, [myEventIdentifier]), - ]), - } as unknown as ts.Decorator; - - const result = getDecoratorParameters(decorator, typeCheckerMock); - - expect(result).toEqual(['myEvent']); - }); - - it('should resolve object property', () => { - const eventsIdentifier = ts.factory.createIdentifier('EVENTS'); - const myEventProperty = ts.factory.createPropertyAccessExpression(eventsIdentifier, 'MY_EVENT'); - - const propertySymbolMock = { - valueDeclaration: ts.factory.createPropertyDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)], - 'MY_EVENT', - undefined, - undefined, - ts.factory.createStringLiteral('myEvent'), - ), - }; - - const objectTypeMock = { - // Mock object type - }; - - const propertyTypeMock = { - value: 'myEvent', - isLiteral: () => true, - }; - - const typeCheckerMock = { - getTypeAtLocation: jest.fn((node) => { - if (node === eventsIdentifier) { - return objectTypeMock; - } - return propertyTypeMock; - }), - getPropertyOfType: jest.fn(() => propertySymbolMock), - getTypeOfSymbolAtLocation: jest.fn(() => propertyTypeMock), - } as unknown as ts.TypeChecker; - - const decorator: ts.Decorator = { - expression: ts.factory.createCallExpression(ts.factory.createIdentifier('Listen'), undefined, [ - ts.factory.createCallExpression(ts.factory.createIdentifier('resolveVar'), undefined, [myEventProperty]), - ]), - } as unknown as ts.Decorator; - - const result = getDecoratorParameters(decorator, typeCheckerMock); - - expect(result).toEqual(['myEvent']); - }); - - it('should throw error for non-resolvable variable', () => { - const myEventIdentifier = ts.factory.createIdentifier('MY_EVENT'); - - const typeCheckerMock = { - getSymbolAtLocation: jest.fn((): ts.Symbol | undefined => undefined), - } as unknown as ts.TypeChecker; - - const decorator: ts.Decorator = { - expression: ts.factory.createCallExpression(ts.factory.createIdentifier('Listen'), undefined, [ - ts.factory.createCallExpression(ts.factory.createIdentifier('resolveVar'), undefined, [myEventIdentifier]), - ]), - } as unknown as ts.Decorator; - - const diagnostics: any[] = []; - - expect(() => { - getDecoratorParameters(decorator, typeCheckerMock, diagnostics); - }).toThrow(); - - expect(diagnostics.length).toBeGreaterThan(0); - expect(diagnostics[0].level).toBe('error'); - }); - - it('should throw error for non-existent object property', () => { - const eventsIdentifier = ts.factory.createIdentifier('EVENTS'); - const myEventProperty = ts.factory.createPropertyAccessExpression(eventsIdentifier, 'MY_EVENT'); - - const objectTypeMock = {}; - - const typeCheckerMock = { - getTypeAtLocation: jest.fn(() => objectTypeMock), - getPropertyOfType: jest.fn((): ts.Symbol | undefined => undefined), - } as unknown as ts.TypeChecker; - - const decorator: ts.Decorator = { - expression: ts.factory.createCallExpression(ts.factory.createIdentifier('Listen'), undefined, [ - ts.factory.createCallExpression(ts.factory.createIdentifier('resolveVar'), undefined, [myEventProperty]), - ]), - } as unknown as ts.Decorator; - - const diagnostics: any[] = []; - - expect(() => { - getDecoratorParameters(decorator, typeCheckerMock, diagnostics); - }).toThrow(); - - expect(diagnostics.length).toBeGreaterThan(0); - expect(diagnostics[0].level).toBe('error'); - }); - }); - }); -}); diff --git a/src/compiler/transformers/test/map-imports-to-path-aliases.spec.ts b/src/compiler/transformers/test/map-imports-to-path-aliases.spec.ts deleted file mode 100644 index df11227c372..00000000000 --- a/src/compiler/transformers/test/map-imports-to-path-aliases.spec.ts +++ /dev/null @@ -1,200 +0,0 @@ -import type { OutputTargetDistCollection } from '@stencil/core/declarations'; -import { mockValidatedConfig } from '@stencil/core/testing'; -import ts, { Extension } from 'typescript'; - -import { ValidatedConfig } from '../../../internal'; -import { mapImportsToPathAliases } from '../map-imports-to-path-aliases'; -import { transpileModule } from './transpile'; - -describe('mapImportsToPathAliases', () => { - let module: ReturnType; - let config: ValidatedConfig; - let resolveModuleNameSpy: jest.SpyInstance< - ReturnType, - Parameters - >; - let outputTarget: OutputTargetDistCollection; - - beforeEach(() => { - config = mockValidatedConfig({ tsCompilerOptions: {} }); - - resolveModuleNameSpy = jest.spyOn(ts, 'resolveModuleName'); - - outputTarget = { - type: 'dist-collection', - dir: 'dist', - collectionDir: 'dist/collection', - transformAliasedImportPaths: true, - }; - }); - - afterEach(() => { - resolveModuleNameSpy.mockReset(); - }); - - it('does nothing if the config flag is `false`', () => { - outputTarget.transformAliasedImportPaths = false; - resolveModuleNameSpy.mockReturnValue({ - resolvedModule: { - isExternalLibraryImport: false, - extension: Extension.Ts, - resolvedFileName: 'utils.js', - }, - }); - const inputText = ` - import { utils } from "@utils/utils"; - - utils.test(); - `; - - module = transpileModule(inputText, config, null, [], [mapImportsToPathAliases(config, '', outputTarget)]); - - expect(module.outputText).toContain('import { utils } from "@utils/utils";'); - }); - - it('ignores relative imports', () => { - resolveModuleNameSpy.mockReturnValue({ - resolvedModule: { - isExternalLibraryImport: false, - extension: Extension.Ts, - resolvedFileName: 'utils.js', - }, - }); - const inputText = ` - import * as dateUtils from "../utils"; - - dateUtils.test(); - `; - - module = transpileModule(inputText, config, null, [], [mapImportsToPathAliases(config, '', outputTarget)]); - - expect(module.outputText).toContain('import * as dateUtils from "../utils";'); - }); - - it('ignores external imports', () => { - resolveModuleNameSpy.mockReturnValue({ - resolvedModule: { - isExternalLibraryImport: true, - extension: Extension.Ts, - resolvedFileName: 'utils.js', - }, - }); - const inputText = ` - import { utils } from "@stencil/core"; - - utils.test(); - `; - - module = transpileModule(inputText, config, null, [], [mapImportsToPathAliases(config, '', outputTarget)]); - - expect(module.outputText).toContain('import { utils } from "@stencil/core";'); - }); - - it('does nothing if there is no resolved module', () => { - resolveModuleNameSpy.mockReturnValue({ - resolvedModule: undefined, - }); - const inputText = ` - import { utils } from "@utils"; - - utils.test(); - `; - - module = transpileModule(inputText, config, null, [], [mapImportsToPathAliases(config, '', outputTarget)]); - - expect(module.outputText).toContain('import { utils } from "@utils";'); - }); - - // TODO(STENCIL-223): remove spy to test actual resolution behavior - it('replaces the path alias with the generated relative path', () => { - resolveModuleNameSpy.mockReturnValue({ - resolvedModule: { - isExternalLibraryImport: false, - extension: Extension.Ts, - resolvedFileName: 'utils.ts', - }, - }); - const inputText = ` - import { utils } from "@utils"; - - utils.test(); - `; - - module = transpileModule(inputText, config, null, [], [mapImportsToPathAliases(config, '', outputTarget)]); - - expect(module.outputText).toContain('import { utils } from "./utils";'); - }); - - it('is not greedy with extension regex replacement', () => { - resolveModuleNameSpy.mockReturnValue({ - resolvedModule: { - isExternalLibraryImport: false, - extension: Extension.Ts, - resolvedFileName: 'utils/something-ending-with-d.ts', - }, - }); - const inputText = ` - import { utils } from "@utils/something-ending-with-d"; - - utils.test(); - `; - - module = transpileModule(inputText, config, null, [], [mapImportsToPathAliases(config, '', outputTarget)]); - - expect(module.outputText).toContain('import { utils } from "./utils/something-ending-with-d";'); - }); - - // The resolved module is not part of the output directory - it('generates the correct relative path when the resolved module is outside the transpiled project', () => { - config.srcDir = '/test-dir'; - resolveModuleNameSpy.mockReturnValue({ - resolvedModule: { - isExternalLibraryImport: false, - extension: Extension.Ts, - resolvedFileName: '/some-compiled-dir/utils/utils.ts', - }, - }); - const inputText = ` - import { utils } from "@utils"; - - utils.test(); - `; - - module = transpileModule( - inputText, - config, - null, - [], - [mapImportsToPathAliases(config, '/dist/collection/test.js', outputTarget)], - ); - - expect(module.outputText).toContain(`import { utils } from "../../some-compiled-dir/utils/utils";`); - }); - - // Source module and resolved module are in the same output directory - it('generates the correct relative path when the resolved module is within the transpiled project', () => { - config.srcDir = '/test-dir'; - resolveModuleNameSpy.mockReturnValue({ - resolvedModule: { - isExternalLibraryImport: false, - extension: Extension.Ts, - resolvedFileName: '/test-dir/utils/utils.ts', - }, - }); - const inputText = ` - import { utils } from "@utils"; - - utils.test(); - `; - - module = transpileModule( - inputText, - config, - null, - [], - [mapImportsToPathAliases(config, 'dist/collection/test.js', outputTarget)], - ); - - expect(module.outputText).toContain(`import { utils } from "./utils/utils";`); - }); -}); diff --git a/src/compiler/transformers/test/parse-form-associated.spec.ts b/src/compiler/transformers/test/parse-form-associated.spec.ts deleted file mode 100644 index 553d3e70a32..00000000000 --- a/src/compiler/transformers/test/parse-form-associated.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { transpileModule } from './transpile'; - -describe('parse form associated', function () { - it('should set formAssociated if passed to decorator', async () => { - const t = transpileModule(` - @Component({ - tag: 'cmp-a', - formAssociated: true - }) - export class CmpA { - } - `); - expect(t.cmp!.formAssociated).toBe(true); - }); - - it('should not set formAssociated if not set', async () => { - const t = transpileModule(` - @Component({ - tag: 'cmp-a', - }) - export class CmpA { - } - `); - expect(t.cmp!.formAssociated).toBe(false); - }); -}); diff --git a/src/compiler/transformers/test/parse-styles.spec.ts b/src/compiler/transformers/test/parse-styles.spec.ts deleted file mode 100644 index c75d0f295ae..00000000000 --- a/src/compiler/transformers/test/parse-styles.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { getStaticGetter, transpileModule } from './transpile'; -import { formatCode } from './utils'; - -describe('parse styles', () => { - it('add static "styleUrl"', () => { - const t = transpileModule(` - @Component({ - tag: 'cmp-a', - styleUrl: 'style.css' - }) - export class CmpA {} - `); - - expect(getStaticGetter(t.outputText, 'styleUrls')).toEqual({ $: ['style.css'] }); - }); - - it('add static "styleUrls"', () => { - const t = transpileModule(` - @Component({ - tag: 'cmp-a', - styleUrls: ['style.css', 'style2.css'] - }) - export class CmpA {} - `); - - expect(getStaticGetter(t.outputText, 'styleUrls')).toEqual({ $: ['style.css', 'style2.css'] }); - }); - - it('add static "styles"', () => { - const t = transpileModule(` - @Component({ - tag: 'cmp-a', - styles: 'p{color:red}' - }) - export class CmpA {} - `); - - expect(getStaticGetter(t.outputText, 'styles')).toEqual('p{color:red}'); - }); - - it('add static "styles" as object', async () => { - const t = transpileModule(` - const md = 'p{color:red}'; - const ios = 'p{color:black}'; - @Component({ - tag: 'cmp-a', - styles: { - md: md, - ios: ios, - } - }) - export class CmpA {} - `); - expect(await formatCode(t.outputText)).toEqual( - await formatCode( - `const md = 'p{color:red}';const ios = 'p{color:black}';export class CmpA { static get is() { return "cmp-a"; } static get styles() { return { "md": md, "ios": ios }; }}`, - ), - ); - }); - - it('add static "styles" as object (2)', () => { - const t = transpileModule(` - @Component({ - tag: 'cmp-a', - styles: { - md: 'p{color:red}', - ios: 'p{color:black}', - } - }) - export class CmpA {} - `); - - expect(getStaticGetter(t.outputText, 'styles')).toEqual({ - ios: 'p{color:black}', - md: 'p{color:red}', - }); - }); - - it('add static "styles" const', async () => { - const t = transpileModule(` - const styles = 'p{color:red}'; - @Component({ - tag: 'cmp-a', - styles, - }) - export class CmpA {} - `); - expect(await formatCode(t.outputText)).toEqual( - await formatCode( - `const styles = 'p{color:red}';export class CmpA { static get is() { return "cmp-a"; } static get styles() { return styles; }}`, - ), - ); - }); -}); diff --git a/src/compiler/transformers/test/transpile.ts b/src/compiler/transformers/test/transpile.ts deleted file mode 100644 index 42400e85211..00000000000 --- a/src/compiler/transformers/test/transpile.ts +++ /dev/null @@ -1,224 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockBuildCtx, mockCompilerCtx, mockValidatedConfig } from '@stencil/core/testing'; -import ts from 'typescript'; - -import { performAutomaticKeyInsertion } from '../automatic-key-insertion'; -import { convertDecoratorsToStatic } from '../decorators-to-static/convert-decorators'; -import { updateModule } from '../static-to-meta/parse-static'; -import { convertStaticToMeta } from '../static-to-meta/visitor'; -import { getScriptTarget } from '../transform-utils'; - -/** - * Testing utility for transpiling provided string containing valid Stencil code - * - * @param input the code to transpile - * @param config a Stencil configuration to apply during the transpilation - * @param compilerCtx a compiler context to use in the transpilation process - * @param beforeTransformers TypeScript transformers that should be applied before the code is emitted - * @param afterTransformers TypeScript transformers that should be applied after the code is emitted - * @param afterDeclarations TypeScript transformers that should be applied - * after declarations are generated - * @param tsConfig optional typescript compiler options to use - * @param inputFileName a dummy filename to use for the module (defaults to `module.tsx`) - * @returns the result of the transpilation step - */ -export function transpileModule( - input: string, - config?: Partial | null, - compilerCtx?: d.CompilerCtx | null, - beforeTransformers: ts.TransformerFactory[] = [], - afterTransformers: ts.TransformerFactory[] = [], - afterDeclarations: ts.TransformerFactory[] = [], - tsConfig: ts.CompilerOptions = {}, - inputFileName = 'module.tsx', -) { - const options: ts.CompilerOptions = { - ...ts.getDefaultCompilerOptions(), - allowNonTsExtensions: true, - composite: undefined, - declaration: undefined, - declarationDir: undefined, - experimentalDecorators: true, - isolatedModules: true, - jsx: ts.JsxEmit.React, - jsxFactory: 'h', - jsxFragmentFactory: 'Fragment', - lib: undefined, - module: ts.ModuleKind.ESNext, - noEmit: undefined, - noEmitHelpers: true, - noEmitOnError: undefined, - noLib: true, - noResolve: true, - out: undefined, - outFile: undefined, - paths: undefined, - removeComments: false, - rootDirs: undefined, - suppressOutputPathCheck: true, - target: getScriptTarget(), - types: undefined, - // add in possible default config overrides - ...tsConfig, - }; - - const initConfig = mockValidatedConfig(); - const mergedConfig: d.ValidatedConfig = { ...initConfig, ...config }; - compilerCtx = compilerCtx || mockCompilerCtx(mergedConfig); - - const sourceFile = ts.createSourceFile(inputFileName, input, options.target); - - let outputText: string; - let declarationOutputText: string; - - const emitCallback: ts.WriteFileCallback = (emitFilePath, data, _w, _e, tsSourceFiles) => { - if (emitFilePath.endsWith('.js')) { - outputText = prettifyTSOutput(data); - updateModule(mergedConfig, compilerCtx, buildCtx, tsSourceFiles[0], data, emitFilePath, tsTypeChecker, null); - } - if (emitFilePath.endsWith('.d.ts')) { - declarationOutputText = prettifyTSOutput(data); - } - }; - - const compilerHost: ts.CompilerHost = { - getSourceFile: (fileName) => (fileName === inputFileName ? sourceFile : undefined), - writeFile: emitCallback, - getDefaultLibFileName: () => 'lib.d.ts', - useCaseSensitiveFileNames: () => false, - getCanonicalFileName: (fileName) => fileName, - getCurrentDirectory: () => '', - getNewLine: () => '', - fileExists: (fileName) => fileName === inputFileName, - readFile: () => '', - directoryExists: () => true, - getDirectories: () => [], - }; - - const tsProgram = ts.createProgram([inputFileName], options, compilerHost); - const tsTypeChecker = tsProgram.getTypeChecker(); - - const buildCtx = mockBuildCtx(mergedConfig, compilerCtx); - - const transformOpts: d.TransformOptions = { - coreImportPath: '@stencil/core', - componentExport: 'lazy', - componentMetadata: null, - currentDirectory: '/', - proxy: null, - style: 'static', - styleImportData: 'queryparams', - }; - - tsProgram.emit(undefined, undefined, undefined, undefined, { - before: [ - convertDecoratorsToStatic(mergedConfig, buildCtx.diagnostics, tsTypeChecker, tsProgram), - performAutomaticKeyInsertion, - ...beforeTransformers, - ], - after: [ - (context) => { - let newSource: ts.SourceFile; - const visitNode = (node: ts.Node): ts.Node => { - // just a patch for testing - source file resolution gets - // lost in the after transform phase - node.getSourceFile = () => newSource; - return ts.visitEachChild(node, visitNode, context); - }; - return (sourceFile: ts.SourceFile): ts.SourceFile => { - newSource = sourceFile; - return visitNode(sourceFile) as ts.SourceFile; - }; - }, - convertStaticToMeta(mergedConfig, compilerCtx, buildCtx, tsTypeChecker, null, transformOpts), - ...afterTransformers, - ], - afterDeclarations, - }); - - const moduleFile: d.Module = compilerCtx.moduleMap.values().next().value; - const cmps = moduleFile ? moduleFile.cmps : null; - const cmp = Array.isArray(cmps) && cmps.length > 0 ? cmps[0] : null; - const tagName = cmp ? cmp.tagName : null; - const componentClassName = cmp ? cmp.componentClassName : null; - const properties = cmp ? cmp.properties : null; - const virtualProperties = cmp ? cmp.virtualProperties : null; - const property = properties ? properties[0] : null; - const states = cmp ? cmp.states : null; - const state = states ? states[0] : null; - const listeners = cmp ? cmp.listeners : null; - const listener = listeners ? listeners[0] : null; - const events = cmp ? cmp.events : null; - const event = events ? events[0] : null; - const methods = cmp ? cmp.methods : null; - const method = methods ? methods[0] : null; - const elementRef = cmp ? cmp.elementRef : null; - const watchers = cmp ? cmp.watchers : null; - const isMixin = cmp ? moduleFile.isMixin : false; - const isExtended = cmp ? moduleFile.isExtended : false; - const serializers = cmp ? cmp.serializers : null; - const deserializers = cmp ? cmp.deserializers : null; - - if (buildCtx.hasError || buildCtx.hasWarning) { - throw new Error(buildCtx.diagnostics[0].messageText as string); - } - - return { - buildCtx, - cmp, - cmps, - compilerCtx, - componentClassName, - declarationOutputText, - deserializers, - diagnostics: buildCtx.diagnostics, - elementRef, - event, - events, - listener, - listeners, - method, - methods, - moduleFile, - outputText, - properties, - watchers, - property, - serializers, - state, - states, - tagName, - virtualProperties, - isMixin, - isExtended, - }; -} - -/** - * Rewrites any stretches of whitespace in the TypeScript output to take up a - * single space instead. This makes it a little more readable to write out strings - * in spec files for comparison. - * - * @param tsOutput the string to process - * @returns that string with any stretches of whitespace shrunk down to one space - */ -const prettifyTSOutput = (tsOutput: string): string => tsOutput.replace(/\s+/gm, ' '); - -/** - * Helper function for tests that converts stringified JavaScript to a runtime value. - * A value from the generated JavaScript is returned based on the provided property name. - * @param stringifiedJs the stringified JavaScript - * @param propertyName the property name to pull off the generated JavaScript - * @returns the value associated with the provided property name. Returns undefined if an error occurs while converting - * the stringified JS to JavaScript, or if the property does not exist on the generated JavaScript. - */ -export function getStaticGetter(stringifiedJs: string, propertyName: string): string | void { - const toEvaluate = `return ${stringifiedJs.replace('export', '')}`; - try { - const Obj = new Function(toEvaluate); - return Obj()[propertyName]; - } catch (e) { - console.error(e); - console.error(toEvaluate); - } -} diff --git a/src/compiler/transformers/test/tsconfig.json b/src/compiler/transformers/test/tsconfig.json deleted file mode 100644 index 0ff0be83173..00000000000 --- a/src/compiler/transformers/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/transformers/test/utils.ts b/src/compiler/transformers/test/utils.ts deleted file mode 100644 index 889893060b1..00000000000 --- a/src/compiler/transformers/test/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import ionicConfig from '@ionic/prettier-config'; -import { format } from 'prettier'; - -/** - * Use the ionic-wide configuration to format some code and return the result. - * Useful for making assertions in tests that involve code strings more robust. - * - * @param code the string to format - * @returns a Promise wrapping the formatted code - */ -export const formatCode = (code: string): Promise => format(code, { ...ionicConfig, parser: 'typescript' }); - -/** - * c for compact, c for class declaration, make of it what you will! - * - * a little util to take a multiline template literal and convert it to a - * single line, with any whitespace substrings converting to single spaces. - * this can help us compare with the output of `transpileModule`. - * - * @param strings an array of strings from a template literal - * @returns a formatted string! - */ -export function c(strings: TemplateStringsArray) { - return formatCode(strings.join('')); -} diff --git a/src/compiler/transpile.ts b/src/compiler/transpile.ts deleted file mode 100644 index 27bbae4ca87..00000000000 --- a/src/compiler/transpile.ts +++ /dev/null @@ -1,172 +0,0 @@ -import rollupPluginUtils from '@rollup/pluginutils'; -import type { - TransformCssToEsmInput, - TransformOptions, - TranspileOptions, - TranspileResults, - ValidatedConfig, -} from '@stencil/core/internal'; -import { catchError, getInlineSourceMappingUrlLinker, isString } from '@utils'; - -import { getTranspileConfig, getTranspileCssConfig, getTranspileResults } from './config/transpile-options'; -import { validateConfig } from './config/validate-config'; -import { transformCssToEsm, transformCssToEsmSync } from './style/css-to-esm'; -import { patchTypescript } from './sys/typescript/typescript-sys'; -import { getPublicCompilerMeta } from './transformers/add-component-meta-static'; -import { transpileModule } from './transpile/transpile-module'; - -/** - * The `transpile()` function inputs source code as a string, with various options - * within the second argument. The function is stateless and returns a `Promise` of the - * results, including diagnostics and the transpiled code. The `transpile()` function - * does not handle any bundling, minifying, or precompiling any CSS preprocessing like - * Sass or Less. The `transpileSync()` equivalent is available so the same function - * it can be called synchronously. However, TypeScript must be already loaded within - * the global for it to work, where as the async `transpile()` function will load - * TypeScript automatically. - * - * Since TypeScript is used, the source code will transpile from TypeScript to JavaScript, - * and does not require Babel presets. Additionally, the results includes an `imports` - * array of all the import paths found in the source file. The transpile options can be - * used to set the `module` format, such as `cjs`, and JavaScript `target` version, such - * as `es2017`. - * - * @param code the code to transpile - * @param opts options for the transpilation process - * @returns a Promise wrapping the results of the transpilation - */ -export const transpile = async (code: string, opts: TranspileOptions = {}): Promise => { - const { importData, results } = getTranspileResults(code, opts); - - try { - if (shouldTranspileModule(results.inputFileExtension)) { - const { config, compileOpts, transformOpts } = getTranspileConfig(opts); - const validatedConfig = validateConfig(config, {}).config; - patchTypescript(validatedConfig, null); - transpileCode(validatedConfig, compileOpts, transformOpts, results); - } else if (results.inputFileExtension === 'd.ts') { - results.code = ''; - } else if (results.inputFileExtension === 'css') { - const transformInput = getTranspileCssConfig(opts, importData, results); - await transpileCss(transformInput, results); - } else if (results.inputFileExtension === 'json') { - transpileJson(results); - } - } catch (e: any) { - catchError(results.diagnostics, e); - } - - return results; -}; - -/** - * Synchronous equivalent of the `transpile()` function. When used in a browser - * environment, TypeScript must already be available globally, where as the async - * `transpile()` function will load TypeScript automatically. - * - * @param code the code to transpile - * @param opts options for the transpilation process - * @returns the results of the transpilation - */ -export const transpileSync = (code: string, opts: TranspileOptions = {}): TranspileResults => { - const { importData, results } = getTranspileResults(code, opts); - - try { - if (shouldTranspileModule(results.inputFileExtension)) { - const { config, compileOpts, transformOpts } = getTranspileConfig(opts); - const validatedConfig = validateConfig(config, {}).config; - patchTypescript(validatedConfig, null); - transpileCode(validatedConfig, compileOpts, transformOpts, results); - } else if (results.inputFileExtension === 'd.ts') { - results.code = ''; - } else if (results.inputFileExtension === 'css') { - const transformInput = getTranspileCssConfig(opts, importData, results); - transpileCssSync(transformInput, results); - } else if (results.inputFileExtension === 'json') { - transpileJson(results); - } - } catch (e: any) { - catchError(results.diagnostics, e); - } - - return results; -}; - -const transpileCode = ( - config: ValidatedConfig, - transpileOpts: TranspileOptions, - transformOpts: TransformOptions, - results: TranspileResults, -) => { - const transpileResults = transpileModule(config, results.code, transformOpts); - - results.diagnostics.push(...transpileResults.diagnostics); - - if (typeof transpileResults.code === 'string') { - results.code = transpileResults.code; - results.map = transpileResults.map; - - if (transpileOpts.sourceMap === 'inline') { - try { - const mapObject = JSON.parse(transpileResults.map); - mapObject.file = transpileOpts.file; - mapObject.sources = [transpileOpts.file]; - delete mapObject.sourceRoot; - - const sourceMapComment = results.code.lastIndexOf('//#'); - results.code = - results.code.slice(0, sourceMapComment) + getInlineSourceMappingUrlLinker(JSON.stringify(mapObject)); - } catch (e) { - console.error(e); - } - } - } - - if (isString(transpileResults.sourceFilePath)) { - results.inputFilePath = transpileResults.sourceFilePath; - } - - const moduleFile = transpileResults.moduleFile; - if (moduleFile) { - results.outputFilePath = moduleFile.jsFilePath; - - moduleFile.cmps.forEach((cmp) => { - results.data.push(getPublicCompilerMeta(cmp)); - }); - - moduleFile.originalImports.forEach((originalImport) => { - results.imports.push({ - path: originalImport, - }); - }); - } -}; - -const transpileCss = async (transformInput: TransformCssToEsmInput, results: TranspileResults) => { - const cssResults = await transformCssToEsm(transformInput); - results.code = cssResults.output; - results.map = cssResults.map; - results.imports = cssResults.imports.map((p) => ({ path: p.importPath })); - results.diagnostics.push(...cssResults.diagnostics); -}; - -const transpileCssSync = (transformInput: TransformCssToEsmInput, results: TranspileResults) => { - const cssResults = transformCssToEsmSync(transformInput); - results.code = cssResults.output; - results.map = cssResults.map; - results.imports = cssResults.imports.map((p) => ({ path: p.importPath })); - results.diagnostics.push(...cssResults.diagnostics); -}; - -const transpileJson = (results: TranspileResults) => { - results.code = rollupPluginUtils.dataToEsm(JSON.parse(results.code), { - preferConst: true, - compact: false, - indent: ' ', - }); - results.map = { mappings: '' }; -}; - -// NOTE: if you change this, also change jest configuration files in `src/testing/jest/jest*`. -// Search for 'mod_extensions_jest' to find comments like this. -const shouldTranspileModule = (ext: string) => ['tsx', 'ts', 'mjs', 'jsx', 'js'].includes(ext); diff --git a/src/compiler/transpile/create-watch-program.ts b/src/compiler/transpile/create-watch-program.ts deleted file mode 100644 index df2206c3bd6..00000000000 --- a/src/compiler/transpile/create-watch-program.ts +++ /dev/null @@ -1,117 +0,0 @@ -import ts from 'typescript'; - -import type * as d from '../../declarations'; -import { getTsOptionsToExtend } from './ts-config'; - -/** - * This method creates the {@link ts.EmitAndSemanticDiagnosticsBuilderProgram} that is responsible for - * rebuilding a Stencil project after file changes have been detected (via TS's polling-based file watcher). - * - * We mostly use a traditional approach to create the program as documented by the TS team: - * {@link https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#writing-an-incremental-program-watcher} - * However, we do override a few methods on the {@link ts.System} object. - * - * @param config The validated config for the Stencil project. - * @param buildCallback A function that will be executed after the TS program is created and on subsequent - * project rebuilds. - * @returns An object containing the {@link ts.EmitAndSemanticDiagnosticsBuilderProgram} and callback - * function to trigger a project rebuild. - */ -export const createTsWatchProgram = async ( - config: d.ValidatedConfig, - buildCallback: (tsBuilder: ts.BuilderProgram) => Promise, -) => { - let isRunning = false; - let lastTsBuilder: any; - let timeoutId: any; - - // Get the pre-baked TS options we want to use for our builder program - const optionsToExtend = getTsOptionsToExtend(config); - - const tsWatchSys: ts.System = { - ...ts.sys, - - /** - * Override the default `setTimeout` implementation in the {@link ts.System}. The reasoning - * behind this change is not explicitly clear, but this appears to be related to debouncing - * the build processes. Stencil currently has an issue where multiple file changes are detected - * at the same time for a single change. So, this override appears to prevent us from actually rebuilding - * the project in rapid succession for the detected changes. - * - * @param callback A method that will execute after the specified time duration - * @param time The time to wait before executing the callback - * @returns A {@link NodeJs.Timer} instance - */ - setTimeout(callback, time) { - clearTimeout(timeoutId); - const delay = config.sys.watchTimeout || time; - const tick = () => { - if (!isRunning) { - callback(); - timeoutId = null; - } else { - timeoutId = setTimeout(tick, delay); - } - }; - timeoutId = setTimeout(tick, delay); - return timeoutId; - }, - }; - - // Whenever the system teardown happens, we need to make sure there is no timeout running - config.sys.addDestroy(() => tsWatchSys.clearTimeout(timeoutId)); - - // Use the TS API to create our own watch compiler host that will be - // used to instantiate our watch program - const tsWatchHost = ts.createWatchCompilerHost( - // Use the TS config from the Stencil project - config.tsconfig, - optionsToExtend, - tsWatchSys, - // We use the `createEmitAndSemanticDiagnosticsBuilderProgram` as opposed to the - // `createSemanticDiagnosticsBuilderProgram` because we need our program to emit - // output files in addition to checking for errors - ts.createEmitAndSemanticDiagnosticsBuilderProgram, - // Add a callback to log out diagnostics as the program runs - (reportDiagnostic) => { - config.logger.debug('watch reportDiagnostic:' + reportDiagnostic.messageText); - }, - // Add a callback to log out statuses of the the watch program - (reportWatchStatus) => { - config.logger.debug(reportWatchStatus.messageText); - }, - // We don't want to allow users to mess with the watch method, so - // we only strip out the excludeFiles and excludeDirectories properties - // to allow the user to still have control over which files get excluded from the watcher - config.tsWatchOptions - ? { - excludeFiles: config.tsWatchOptions.excludeFiles, - excludeDirectories: config.tsWatchOptions.excludeDirectories, - } - : undefined, - ); - - // Add a callback that will execute whenever a new instance - // is created using the definition we have constructed for `tsWatchHost`. - // This is what will be used to kick-off the actual Stencil build process via the `buildCallback()` - tsWatchHost.afterProgramCreate = async (tsBuilder) => { - lastTsBuilder = tsBuilder; - isRunning = true; - await buildCallback(tsBuilder); - isRunning = false; - }; - - return { - // Create the watch builder program instance and make it available on the - // returned object. This provides us an easy way to teardown the program - // down-the-road. - program: ts.createWatchProgram(tsWatchHost), - // This will be called via a callback on the watch build whenever a file - // change is detected - rebuild: () => { - if (lastTsBuilder) { - tsWatchSys.setTimeout(() => tsWatchHost.afterProgramCreate(lastTsBuilder), 300); - } - }, - }; -}; diff --git a/src/compiler/transpile/run-program.ts b/src/compiler/transpile/run-program.ts deleted file mode 100644 index 73fd6812667..00000000000 --- a/src/compiler/transpile/run-program.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { - filterExcludedComponents, - getComponentsFromModules, - isOutputTargetDistTypes, - join, - loadTypeScriptDiagnostics, - normalizePath, - relative, -} from '@utils'; -import { basename } from 'path'; -import ts from 'typescript'; - -import type * as d from '../../declarations'; -import { updateComponentBuildConditionals } from '../app-core/app-data'; -import { resolveComponentDependencies } from '../entries/resolve-component-dependencies'; -import { performAutomaticKeyInsertion } from '../transformers/automatic-key-insertion'; -import { convertDecoratorsToStatic } from '../transformers/decorators-to-static/convert-decorators'; -import { rewriteAliasedDTSImportPaths } from '../transformers/rewrite-aliased-paths'; -import { updateModule } from '../transformers/static-to-meta/parse-static'; -import { generateAppTypes } from '../types/generate-app-types'; -import { updateStencilTypesImports } from '../types/stencil-types'; -import { validateTranspiledComponents } from './validate-components'; - -export const runTsProgram = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - tsBuilder: ts.BuilderProgram, -): Promise => { - const tsSyntactic = loadTypeScriptDiagnostics(tsBuilder.getSyntacticDiagnostics()); - const tsGlobal = loadTypeScriptDiagnostics(tsBuilder.getGlobalDiagnostics()); - const tsOptions = loadTypeScriptDiagnostics(tsBuilder.getOptionsDiagnostics()); - buildCtx.diagnostics.push(...tsSyntactic); - buildCtx.diagnostics.push(...tsGlobal); - buildCtx.diagnostics.push(...tsOptions); - - if (buildCtx.hasError) { - return []; - } - - const tsProgram = tsBuilder.getProgram(); - - const tsTypeChecker = tsProgram.getTypeChecker(); - const typesOutputTarget = config.outputTargets.filter(isOutputTargetDistTypes); - const emittedDts: string[] = []; - - const emitCallback: ts.WriteFileCallback = (emitFilePath, data, _w, _e, tsSourceFiles) => { - if ( - emitFilePath.includes('e2e.') || - emitFilePath.includes('spec.') || - emitFilePath.endsWith('e2e.d.ts') || - emitFilePath.endsWith('spec.d.ts') - ) { - // we don't want to write these to disk! - return; - } - - if (emitFilePath.endsWith('.js') || emitFilePath.endsWith('js.map')) { - updateModule(config, compilerCtx, buildCtx, tsSourceFiles[0], data, emitFilePath, tsTypeChecker, null); - } else if (emitFilePath.endsWith('.d.ts')) { - const srcDtsPath = normalizePath(tsSourceFiles[0].fileName); - const relativeEmitFilepath = getRelativeDts(config, srcDtsPath, emitFilePath); - - emittedDts.push(srcDtsPath); - typesOutputTarget.forEach((o) => { - const distPath = normalizePath(join(normalizePath(o.typesDir), normalizePath(relativeEmitFilepath))); - data = updateStencilTypesImports(o.typesDir, distPath, data); - compilerCtx.fs.writeFile(distPath, data); - }); - } - }; - - const transformers: ts.CustomTransformers = { - before: [ - convertDecoratorsToStatic(config, buildCtx.diagnostics, tsTypeChecker, tsProgram), - performAutomaticKeyInsertion, - ], - afterDeclarations: [], - }; - - if (config.transformAliasedImportPaths) { - /** - * Generate a collection of transformations that are to be applied as a part of the `afterDeclarations` step in the - * TypeScript compilation process. - * - * TypeScript handles the generation of JS and `.d.ts` files through different pipelines. One (possibly surprising) - * consequence of this is that if you modify a source file using a transformer, it will not automatically result in - * changes to the corresponding `.d.ts` file. Instead, if you want to, for instance, rewrite some import specifiers - * in both the source file _and_ its typedef you'll need to run a transformer for both of them. - * - * See here: https://github.com/itsdouges/typescript-transformer-handbook#transforms - * and here: https://github.com/microsoft/TypeScript/pull/23946 - * - * This quirk is not terribly well documented, unfortunately. - */ - transformers.afterDeclarations.push(rewriteAliasedDTSImportPaths); - } - - // Emit files that changed - const emitResult = tsBuilder.emit(undefined, emitCallback, undefined, false, transformers); - - // Check for emit diagnostics - if (emitResult.diagnostics.length > 0) { - const emitDiagnostics = loadTypeScriptDiagnostics(emitResult.diagnostics); - - // Enhance error messages for TS4094 to be more helpful for mixin users; - // These occur when mixins return classes with private/protected members that TypeScript cannot emit - emitDiagnostics.forEach((diagnostic) => { - if (diagnostic.code === '4094') { - diagnostic.level = 'warn'; - diagnostic.messageText = - `${diagnostic.messageText}\n\n` + - `This commonly occurs when using mixins that return classes with private or protected members. ` + - `TypeScript cannot emit declaration files for anonymous classes with non-public members.\n\n` + - `Possible solutions:\n` + - ` 1. Add explicit type annotations to your mixin's return type\n` + - ` 2. Use public members in your mixin classes\n` + - ` 3. Use JavaScript private fields (#field) instead of TypeScript's private keyword`; - } - }); - - buildCtx.diagnostics.push(...emitDiagnostics); - } - - const changedmodules = Array.from(compilerCtx.changedModules.keys()); - buildCtx.debug('Transpiled modules: ' + JSON.stringify(changedmodules, null, '\n')); - - // Finalize components metadata - buildCtx.moduleFiles = Array.from(compilerCtx.moduleMap.values()); - const allComponents = getComponentsFromModules(buildCtx.moduleFiles); - - // Filter out excluded components based on config patterns - const { components: filteredComponents, excludedComponents } = filterExcludedComponents(allComponents, config); - buildCtx.components = filteredComponents; - - // Queue deletion of .d.ts files for excluded components in the in-memory FS - // These deletions will be committed when compilerCtx.fs.commit() is called during writeBuild - if (excludedComponents.length > 0 && typesOutputTarget.length > 0) { - excludedComponents.forEach((cmp) => { - const srcPath = normalizePath(cmp.sourceFilePath); - const relativeToSrc = relative(config.srcDir, srcPath); - const dtsRelativePath = relativeToSrc.replace(/\.tsx?$/, '.d.ts'); - - typesOutputTarget.forEach((outputTarget) => { - const outputDtsPath = join(outputTarget.typesDir, dtsRelativePath); - - // The file may have been queued for writing during emit - // We need to cancel the write and queue it for deletion instead - const item = compilerCtx.fs.getItem(outputDtsPath); - item.queueWriteToDisk = false; - item.queueDeleteFromDisk = true; - }); - }); - } - - updateComponentBuildConditionals(compilerCtx.moduleMap, buildCtx.components); - resolveComponentDependencies(buildCtx.components); - - validateTranspiledComponents(config, buildCtx); - - if (buildCtx.hasError) { - return []; - } - - return emittedDts; -}; - -export interface ValidateTypesResult { - hasTypesChanged: boolean; - needsRebuild: boolean; -} - -/** - * Generate types and run semantic validation AFTER components.d.ts exists on disk - */ -export const validateTypesAfterGeneration = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - tsBuilder: ts.BuilderProgram, - emittedDts: string[], -): Promise => { - const tsProgram = tsBuilder.getProgram(); - const typesOutputTarget = config.outputTargets.filter(isOutputTargetDistTypes); - - // Check if components.d.ts already exists - const componentsDtsPath = join(config.srcDir, 'components.d.ts'); - const componentsDtsExistedBefore = await compilerCtx.fs.access(componentsDtsPath); - - // If components.d.ts doesn't exist yet, generate it and signal that a rebuild is needed. - // The current TS program was created without components.d.ts, so it can't provide - // accurate type checking. We need a fresh TS program that includes components.d.ts. - if (!componentsDtsExistedBefore) { - await generateAppTypes(config, compilerCtx, buildCtx, 'src'); - // Signal that we need to rebuild with a fresh TS program - return { hasTypesChanged: true, needsRebuild: true }; - } - - // components.d.ts existed, so the TS program has full type information. - // Run semantic validation on user source files. - if (config.validateTypes) { - const sourceFiles = tsProgram.getSourceFiles().filter((sf) => { - const fileName = normalizePath(sf.fileName); - return ( - !fileName.includes('node_modules') && - !fileName.endsWith('.d.ts') && - fileName.startsWith(normalizePath(config.srcDir)) - ); - }); - - for (const sourceFile of sourceFiles) { - const sourceSemanticDiagnostics = tsProgram.getSemanticDiagnostics(sourceFile); - const tsSemantic = loadTypeScriptDiagnostics(sourceSemanticDiagnostics); - - if (config.devMode) { - tsSemantic.forEach((semanticDiagnostic) => { - if (semanticDiagnostic.code === '6133' || semanticDiagnostic.code === '6192') { - semanticDiagnostic.level = 'warn'; - } - }); - } - buildCtx.diagnostics.push(...tsSemantic); - } - } - - // Update components.d.ts in case components changed - const hasTypesChanged = await generateAppTypes(config, compilerCtx, buildCtx, 'src'); - if (typesOutputTarget.length > 0) { - // copy src dts files that do not get emitted by the compiler - // but we still want to ship them in the dist directory - const srcRootDtsFiles = tsProgram - .getRootFileNames() - .filter((f) => f.endsWith('.d.ts') && !f.endsWith('components.d.ts')) - .map((s) => normalizePath(s)) - .filter((f) => !emittedDts.includes(f)) - .map((srcRootDtsFilePath) => { - const relativeEmitFilepath = relative(config.srcDir, srcRootDtsFilePath); - return Promise.all( - typesOutputTarget.map(async (o) => { - const distPath = join(o.typesDir, relativeEmitFilepath); - let dtsContent = await compilerCtx.fs.readFile(srcRootDtsFilePath); - dtsContent = updateStencilTypesImports(o.typesDir, distPath, dtsContent); - await compilerCtx.fs.writeFile(distPath, dtsContent); - }), - ); - }); - - await Promise.all(srcRootDtsFiles); - } - - return { hasTypesChanged, needsRebuild: false }; -}; - -/** - * Calculate a relative path for a `.d.ts` file, giving the location within - * the typedef output directory where we'd like to write it to disk. - * - * The correct relative path for a `.d.ts` file is basically given by the - * relative location of the _source_ file associated with the `.d.ts` file - * within the Stencil project's source directory. - * - * Thus, in order to calculate this, we take the path to the source file, the - * emit path calculated by typescript (which is going to be right next to the - * emit location for the JavaScript that the compiler emits for the source file) - * and we do a pairwise walk up the two paths, assembling path components as - * we go, until the source file path is equal to the configured source - * directory. Then the path components from the `emitDtsPath` can be reversed - * and re-assembled into a suitable relative path. - * - * @param config a Stencil configuration object - * @param srcPath the path to the source file for the `.d.ts` file of interest - * @param emitDtsPath the emit path for the `.d.ts` file calculated by - * TypeScript - * @returns a relative path to a suitable location where the typedef file can be - * written - */ -export const getRelativeDts = (config: d.ValidatedConfig, srcPath: string, emitDtsPath: string): string => { - const parts: string[] = []; - for (let i = 0; i < 30; i++) { - if (normalizePath(config.srcDir) === srcPath) { - break; - } - const b = basename(emitDtsPath); - parts.push(b); - - emitDtsPath = normalizePath(join(emitDtsPath, '..')); - srcPath = normalizePath(join(normalizePath(srcPath), '..')); - } - return normalizePath(join(...parts.reverse())); -}; diff --git a/src/compiler/transpile/test/create-watch-program.spec.ts b/src/compiler/transpile/test/create-watch-program.spec.ts deleted file mode 100644 index 6fc7f8e24dc..00000000000 --- a/src/compiler/transpile/test/create-watch-program.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import ts from 'typescript'; - -import { ValidatedConfig } from '../../../declarations'; -import { mockValidatedConfig } from '../../../testing/mocks'; -import { createTsWatchProgram } from '../create-watch-program'; - -describe('createWatchProgram', () => { - let config: ValidatedConfig; - - beforeEach(() => { - config = mockValidatedConfig(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('includes watchOptions in the watch program creation', async () => { - config.tsWatchOptions = { - fallbackPolling: 3, - excludeFiles: ['src/components/my-component/my-component.tsx'], - excludeDirectories: ['src/components/my-other-component'], - } as ts.WatchOptions; - config.tsconfig = ''; - const tsSpy = jest.spyOn(ts, 'createWatchCompilerHost').mockReturnValue({} as any); - jest.spyOn(ts, 'createWatchProgram').mockReturnValue({} as any); - - await createTsWatchProgram(config, () => new Promise(() => {})); - - expect(tsSpy.mock.calls[0][6]).toEqual({ - excludeFiles: ['src/components/my-component/my-component.tsx'], - excludeDirectories: ['src/components/my-other-component'], - }); - }); - - it('omits watchOptions when not provided', async () => { - config.tsWatchOptions = undefined; - config.tsconfig = ''; - const tsSpy = jest.spyOn(ts, 'createWatchCompilerHost').mockReturnValue({} as any); - jest.spyOn(ts, 'createWatchProgram').mockReturnValue({} as any); - - await createTsWatchProgram(config, () => new Promise(() => {})); - - expect(tsSpy.mock.calls[0][6]).toEqual(undefined); - }); -}); diff --git a/src/compiler/transpile/transpile-module.ts b/src/compiler/transpile/transpile-module.ts deleted file mode 100644 index 3827d20b78c..00000000000 --- a/src/compiler/transpile/transpile-module.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { createNodeLogger } from '@sys-api-node'; -import { isNumber, isString, loadTypeScriptDiagnostics, normalizePath } from '@utils'; -import ts from 'typescript'; - -import type * as d from '../../declarations'; -import { BuildContext } from '../build/build-ctx'; -import { CompilerContext } from '../build/compiler-ctx'; -import { performAutomaticKeyInsertion } from '../transformers/automatic-key-insertion'; -import { lazyComponentTransform } from '../transformers/component-lazy/transform-lazy-component'; -import { nativeComponentTransform } from '../transformers/component-native/tranform-to-native-component'; -import { convertDecoratorsToStatic } from '../transformers/decorators-to-static/convert-decorators'; -import { - rewriteAliasedDTSImportPaths, - rewriteAliasedSourceFileImportPaths, -} from '../transformers/rewrite-aliased-paths'; -import { convertStaticToMeta } from '../transformers/static-to-meta/visitor'; -import { updateStencilCoreImports } from '../transformers/update-stencil-core-import'; - -/** - * Stand-alone compiling of a single string - * - * @param config the Stencil configuration to use in the compilation process - * @param input the string to compile - * @param transformOpts a configuration object for how the string is compiled - * @returns the results of compiling the provided input string - */ -export const transpileModule = ( - config: d.ValidatedConfig, - input: string, - transformOpts: d.TransformOptions, -): d.TranspileModuleResults => { - if (!config.logger) { - config = { - ...config, - logger: createNodeLogger(), - }; - } - const compilerCtx = new CompilerContext(); - const buildCtx = new BuildContext(config, compilerCtx); - const tsCompilerOptions: ts.CompilerOptions = { - ...config.tsCompilerOptions, - }; - - let sourceFilePath = transformOpts.file; - if (isString(sourceFilePath)) { - sourceFilePath = normalizePath(sourceFilePath); - } else { - sourceFilePath = tsCompilerOptions.jsx ? `module.tsx` : `module.ts`; - } - - const results: d.TranspileModuleResults = { - sourceFilePath: sourceFilePath, - code: null, - map: null, - diagnostics: [], - moduleFile: null, - }; - - if (transformOpts.module === 'cjs') { - tsCompilerOptions.module = ts.ModuleKind.CommonJS; - } else { - tsCompilerOptions.module = ts.ModuleKind.ESNext; - } - - tsCompilerOptions.target = getScriptTargetKind(transformOpts); - - if ((sourceFilePath.endsWith('.tsx') || sourceFilePath.endsWith('.jsx')) && tsCompilerOptions.jsx == null) { - // ensure we're setup for JSX in typescript - tsCompilerOptions.jsx = ts.JsxEmit.React; - } - - // Only set jsxFactory and jsxFragmentFactory for classic React mode - // For ReactJSX and ReactJSXDev modes (automatic runtime), these should not be set - const isAutomaticRuntime = - tsCompilerOptions.jsx === ts.JsxEmit.ReactJSX || tsCompilerOptions.jsx === ts.JsxEmit.ReactJSXDev; - - if (tsCompilerOptions.jsx != null && !isAutomaticRuntime && !isString(tsCompilerOptions.jsxFactory)) { - tsCompilerOptions.jsxFactory = 'h'; - } - - if (tsCompilerOptions.jsx != null && !isAutomaticRuntime && !isString(tsCompilerOptions.jsxFragmentFactory)) { - tsCompilerOptions.jsxFragmentFactory = 'Fragment'; - } - - if (tsCompilerOptions.paths && !isString(tsCompilerOptions.baseUrl)) { - tsCompilerOptions.baseUrl = '.'; - } - - const sourceFile = ts.createSourceFile(sourceFilePath, input, tsCompilerOptions.target); - - // Create a compilerHost object to allow the compiler to read and write files - const compilerHost: ts.CompilerHost = { - getSourceFile: (fileName) => { - return normalizePath(fileName) === normalizePath(sourceFilePath) ? sourceFile : undefined; - }, - writeFile: (name, text) => { - if (name.endsWith('.js.map') || name.endsWith('.mjs.map')) { - results.map = text; - } else if (name.endsWith('.js') || name.endsWith('.mjs')) { - // if the source file is an ES module w/ `.mjs` extension then - // TypeScript will output a `.mjs` file - results.code = text; - } - }, - getDefaultLibFileName: () => `lib.d.ts`, - useCaseSensitiveFileNames: () => false, - getCanonicalFileName: (fileName) => fileName, - getCurrentDirectory: () => transformOpts.currentDirectory || process.cwd(), - getNewLine: () => ts.sys.newLine || '\n', - fileExists: (fileName) => normalizePath(fileName) === normalizePath(sourceFilePath), - readFile: () => '', - directoryExists: () => true, - getDirectories: () => [], - }; - - const program = ts.createProgram([sourceFilePath], tsCompilerOptions, compilerHost); - const typeChecker = program.getTypeChecker(); - - const transformers = { - before: [ - convertDecoratorsToStatic(config, buildCtx.diagnostics, typeChecker, program), - performAutomaticKeyInsertion, - updateStencilCoreImports(transformOpts.coreImportPath), - ], - after: [convertStaticToMeta(config, compilerCtx, buildCtx, typeChecker, null, transformOpts)], - afterDeclarations: [] as (ts.CustomTransformerFactory | ts.TransformerFactory)[], - } satisfies ts.CustomTransformers; - - if (config.transformAliasedImportPaths) { - transformers.before.push(rewriteAliasedSourceFileImportPaths); - // TypeScript handles the generation of JS and `.d.ts` files through - // different pipelines. One (possibly surprising) consequence of this is - // that if you modify a source file using a transforming it will not - // automatically result in changes to the corresponding `.d.ts` file. - // Instead, if you want to, for instance, rewrite some import specifiers in - // both the source file _and_ its typedef you'll need to run a transformer - // for both of them. - // - // See here: https://github.com/itsdouges/typescript-transformer-handbook#transforms - // and here: https://github.com/microsoft/TypeScript/pull/23946 - // - // This quirk is not terribly well documented unfortunately. - transformers.afterDeclarations.push(rewriteAliasedDTSImportPaths); - } - - if (transformOpts.componentExport === 'customelement' || transformOpts.componentExport === 'module') { - transformers.after.push(nativeComponentTransform(compilerCtx, transformOpts, buildCtx)); - } else { - transformers.after.push(lazyComponentTransform(compilerCtx, transformOpts, buildCtx)); - } - - program.emit(undefined, undefined, undefined, false, transformers); - - const tsDiagnostics = [...program.getSyntacticDiagnostics()] as ts.Diagnostic[]; - - if (config.validateTypes) { - tsDiagnostics.push(...program.getOptionsDiagnostics()); - } - - buildCtx.diagnostics.push(...loadTypeScriptDiagnostics(tsDiagnostics)); - - results.diagnostics.push(...buildCtx.diagnostics); - - results.moduleFile = compilerCtx.moduleMap.get(results.sourceFilePath)!; - - return results; -}; - -const getScriptTargetKind = (transformOpts: d.TransformOptions) => { - const target = transformOpts.target && transformOpts.target.toUpperCase(); - if (isNumber((ts.ScriptTarget as any)[target as string])) { - return (ts.ScriptTarget as any)[target as string]; - } - // ESNext and Latest are the same - return ts.ScriptTarget.Latest; -}; diff --git a/src/compiler/transpile/ts-config.ts b/src/compiler/transpile/ts-config.ts deleted file mode 100644 index d6db5491d86..00000000000 --- a/src/compiler/transpile/ts-config.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { isOutputTargetDistTypes } from '@utils'; -import ts from 'typescript'; - -import type * as d from '../../declarations'; - -/** - * Derive a {@link ts.CompilerOptions} object from the options currently set - * on the user-supplied configuration object. - * - * Some of these options (like the `module` setting) are hardcoded here, but - * the following are derived from the configuration object: - * - * - if one of the output targets requires type declaration output (i.e. the - * {@link d.OutputTargetDistCustomElements.generateTypeDeclarations} option - * is set to `true`) then we'll set `declaration: true` - * - the `outDir` is set to the configured cache directory - * - the `sourceMap` and `inlineSources` options are set based on the user's - * {@link d.Config.sourceMap} configuration - * - * @param config the current user-supplied configuration - * @returns an object containing TypeScript compiler options - */ -export const getTsOptionsToExtend = (config: d.ValidatedConfig): ts.CompilerOptions => { - const tsOptions: ts.CompilerOptions = { - experimentalDecorators: true, - // if the `DIST_TYPES` output target is present then we'd like to emit - // declaration files - declaration: config.outputTargets.some(isOutputTargetDistTypes), - module: config.tsCompilerOptions?.module || ts.ModuleKind.ESNext, - moduleResolution: - config.tsCompilerOptions?.moduleResolution === ts.ModuleResolutionKind.Bundler - ? ts.ModuleResolutionKind.Bundler - : ts.ModuleResolutionKind.NodeJs, - noEmitOnError: config.tsCompilerOptions?.noEmitOnError || false, - outDir: config.cacheDir || config.sys.tmpDirSync(), - sourceMap: config.sourceMap, - inlineSources: config.sourceMap, - }; - return tsOptions; -}; diff --git a/src/compiler/types/generate-types.ts b/src/compiler/types/generate-types.ts deleted file mode 100644 index dc936002322..00000000000 --- a/src/compiler/types/generate-types.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { isDtsFile, join, relative } from '@utils'; - -import type * as d from '../../declarations'; -import { generateCustomElementsTypes } from '../output-targets/dist-custom-elements/custom-elements-types'; -import { generateAppTypes } from './generate-app-types'; -import { copyStencilCoreDts, updateStencilTypesImports } from './stencil-types'; - -/** - * For a single output target, generate types, then copy the Stencil core type declaration file - * @param config the Stencil configuration associated with the project being compiled - * @param compilerCtx the current compiler context - * @param buildCtx the context associated with the current build - * @param outputTarget the output target to generate types for - */ -export const generateTypes = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTarget: d.OutputTargetDistTypes, -): Promise => { - if (!buildCtx.hasError) { - await generateTypesOutput(config, compilerCtx, buildCtx, outputTarget); - await copyStencilCoreDts(config, compilerCtx); - } -}; - -/** - * Generate type definition files and write them to a dist directory - * @param config the Stencil configuration associated with the project being compiled - * @param compilerCtx the current compiler context - * @param buildCtx the context associated with the current build - * @param outputTarget the output target to generate types for - */ -const generateTypesOutput = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTarget: d.OutputTargetDistTypes, -): Promise => { - // get all type declaration files in a project's src/ directory - const srcDirItems = await compilerCtx.fs.readdir(config.srcDir, { recursive: false }); - const srcDtsFiles = srcDirItems.filter((srcItem) => srcItem.isFile && isDtsFile(srcItem.absPath)); - - // Copy .d.ts files from src to dist - // In addition, all references to @stencil/core are replaced - const copiedDTSFilePaths = await Promise.all( - srcDtsFiles.map(async (srcDtsFile) => { - const relPath = relative(config.srcDir, srcDtsFile.absPath); - const distPath = join(outputTarget.typesDir, relPath); - - const originalDtsContent = await compilerCtx.fs.readFile(srcDtsFile.absPath); - const distDtsContent = updateStencilTypesImports(outputTarget.typesDir, distPath, originalDtsContent); - - await compilerCtx.fs.writeFile(distPath, distDtsContent); - return distPath; - }), - ); - const distDtsFilePath = copiedDTSFilePaths.slice(-1)[0]; - - const distPath = outputTarget.typesDir; - await generateAppTypes(config, compilerCtx, buildCtx, distPath); - const { typesDir } = outputTarget; - - if (distDtsFilePath) { - await generateCustomElementsTypes(config, compilerCtx, buildCtx, typesDir); - } -}; diff --git a/src/compiler/types/package-json-log-utils.ts b/src/compiler/types/package-json-log-utils.ts deleted file mode 100644 index e9013fe7e37..00000000000 --- a/src/compiler/types/package-json-log-utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { buildJsonFileError } from '@utils'; - -import type * as d from '../../declarations'; - -/** - * Build a diagnostic for an error resulting from a particular field in a - * package.json file - * - * @param config the stencil config - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param msg an error string - * @param jsonField the key for the field which caused the error, used for - * finding the error line in the original JSON file - * @returns a diagnostic object - */ -export const packageJsonError = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - msg: string, - jsonField: string, -): d.Diagnostic => { - const err = buildJsonFileError(compilerCtx, buildCtx.diagnostics, config.packageJsonFilePath, msg, jsonField); - err.header = `Package Json`; - return err; -}; - -/** - * Build a diagnostic for a warning resulting from a particular field in a - * package.json file - * - * @param config the stencil config - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param msg an error string - * @param jsonField the key for the field which caused the error, used for - * finding the error line in the original JSON file - * @returns a diagnostic object - */ -export const packageJsonWarn = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - msg: string, - jsonField: string, -): d.Diagnostic => { - const warn = buildJsonFileError(compilerCtx, buildCtx.diagnostics, config.packageJsonFilePath, msg, jsonField); - warn.header = `Package Json`; - warn.level = 'warn'; - return warn; -}; diff --git a/src/compiler/types/stencil-types.ts b/src/compiler/types/stencil-types.ts deleted file mode 100644 index 4637a9aa7c4..00000000000 --- a/src/compiler/types/stencil-types.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { isOutputTargetDistTypes, join, normalizePath, relative, resolve } from '@utils'; -import { dirname } from 'path'; - -import type * as d from '../../declarations'; -import { FsWriteResults } from '../sys/in-memory-fs'; - -/** - * Update a type declaration file's import declarations using the module `@stencil/core` - * @param typesDir the directory where type declaration files are expected to exist - * @param dtsFilePath the path of the type declaration file being updated, used to derive the correct import declaration - * module - * @param dtsContent the content of a type declaration file to update - * @returns the updated type declaration file contents - */ -export const updateStencilTypesImports = (typesDir: string, dtsFilePath: string, dtsContent: string): string => { - const dir = dirname(dtsFilePath); - // determine the relative path between the directory of the .d.ts file and the types directory. this value may result - // in '.' if they are the same - const relPath = relative(dir, typesDir); - - let coreDtsPath = join(relPath, CORE_FILENAME); - if (!coreDtsPath.startsWith('.')) { - coreDtsPath = `./${coreDtsPath}`; - } - - coreDtsPath = normalizePath(coreDtsPath); - if (dtsContent.includes('@stencil/core')) { - dtsContent = dtsContent.replace(/(from\s*(:?'|"))@stencil\/core\/internal('|")/g, `$1${coreDtsPath}$2`); - dtsContent = dtsContent.replace(/(from\s*(:?'|"))@stencil\/core('|")/g, `$1${coreDtsPath}$2`); - } - return dtsContent; -}; - -/** - * Utility for ensuring that naming collisions do not appear in type declaration files for a component's class members - * decorated with @Prop, @Event, and @Method - * @param typeReferences all type names used by a component class member - * @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions - * @param sourceFilePath the path to the source file of a component using the type being inspected - * @param initialType the name of the type that may be updated - * @returns the updated type name, which may be the same as the initial type name provided as an argument to this - * function - */ -export const updateTypeIdentifierNames = ( - typeReferences: d.ComponentCompilerTypeReferences, - typeImportData: d.TypesImportData, - sourceFilePath: string, - initialType: string, -): string => { - let currentTypeName = initialType; - - // iterate over each of the type references, as there may be >1 reference to inspect - for (const typeReference of Object.values(typeReferences)) { - const importResolvedFile = getTypeImportPath(typeReference.path, sourceFilePath); - - if (typeof importResolvedFile !== 'string') { - continue; - } - - if (!typeImportData.hasOwnProperty(importResolvedFile)) { - continue; - } - - for (const typesImportDatumElement of typeImportData[importResolvedFile]) { - currentTypeName = updateTypeName(currentTypeName, typesImportDatumElement); - } - } - return currentTypeName; -}; - -/** - * Determine the path of a given type reference, relative to the path of a source file - * @param importResolvedFile the path to the file containing the resolve type. may be absolute or relative - * @param sourceFilePath the component source file path to resolve against - * @returns the path of the type import - */ -const getTypeImportPath = (importResolvedFile: string | undefined, sourceFilePath: string): string | undefined => { - if (importResolvedFile && importResolvedFile.startsWith('.')) { - // the path to the type reference is relative, resolve it relative to the provided source path - importResolvedFile = resolve(dirname(sourceFilePath), importResolvedFile); - } - - return importResolvedFile; -}; - -/** - * Determine whether the string representation of a type should be replaced with an alias - * @param currentTypeName the current string representation of a type - * @param typeAlias a type member and a potential different name associated with the type member - * @returns the updated string representation of a type. If the type is not updated, the original type name is returned - */ -const updateTypeName = (currentTypeName: string, typeAlias: d.TypesMemberNameData): string => { - if (!typeAlias.importName) { - return currentTypeName; - } - - // TODO(STENCIL-419): Update this functionality to no longer use a regex - // negative lookahead specifying that quotes that designate a string in JavaScript cannot follow some expression - const endingStrChar = '(?!("|\'|`))'; - /** - * A regular expression that looks at type names along a [word boundary](https://www.regular-expressions.info/wordboundaries.html). - * This is used as the best approximation for replacing type collisions, as this stage of compilation has only - * 'flattened' type information in the form of a String. - * - * This regex should be expected to capture types that are found in generics, unions, intersections, etc., but not - * those in string literals. We do not check for a starting quote (" | ' | `) here as some browsers do not support - * negative lookbehind. This works "well enough" until STENCIL-419 is completed. - */ - const typeNameRegex = new RegExp(`\\b${typeAlias.localName}\\b${endingStrChar}`, 'g'); - return currentTypeName.replace(typeNameRegex, typeAlias.importName); -}; - -/** - * Writes Stencil core typings file to disk for a dist-* output target - * @param config the Stencil configuration associated with the project being compiled - * @param compilerCtx the current compiler context - * @returns the results of writing one or more type declaration files to disk - */ -export const copyStencilCoreDts = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, -): Promise> => { - const typesOutputTargets = config.outputTargets.filter(isOutputTargetDistTypes).filter((o) => o.typesDir); - - const srcStencilDtsPath = join(config.sys.getCompilerExecutingPath(), '..', '..', 'internal', CORE_DTS); - const srcStencilCoreDts = await compilerCtx.fs.readFile(srcStencilDtsPath); - - return Promise.all( - typesOutputTargets.map((o) => { - const coreDtsFilePath = join(o.typesDir, CORE_DTS); - return compilerCtx.fs.writeFile(coreDtsFilePath, srcStencilCoreDts, { outputTargetType: o.type }); - }), - ); -}; - -const CORE_FILENAME = `stencil-public-runtime`; -const CORE_DTS = `${CORE_FILENAME}.d.ts`; diff --git a/src/compiler/types/tests/__snapshots__/generate-app-types.spec.ts.snap b/src/compiler/types/tests/__snapshots__/generate-app-types.spec.ts.snap deleted file mode 100644 index 42f9d10bbfe..00000000000 --- a/src/compiler/types/tests/__snapshots__/generate-app-types.spec.ts.snap +++ /dev/null @@ -1,1266 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`generateAppTypes attr: and prop: prefix generation should not generate attr: or prop: prefixes for props without attributes 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"internalData\\": InternalDataType; - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"internalData\\"?: InternalDataType; - } - interface IntrinsicElements { - \\"my-component\\": MyComponent; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom event types should generate a type declaration file with custom event types 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedEventType } from \\"./some/stubbed/path/resources\\"; -export { UserImplementedEventType } from \\"./some/stubbed/path/resources\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - } -} -export interface MyComponentCustomEvent extends CustomEvent { - detail: T; - target: HTMLMyComponentElement; -} -declare global { - interface HTMLMyComponentElementEventMap { - \\"myEvent\\": UserImplementedEventType; - } - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"onMyEvent\\"?: (event: MyComponentCustomEvent) => void; - } - interface IntrinsicElements { - \\"my-component\\": MyComponent; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom event types should generate a type declaration file with multiple components using the same custom event type 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedEventType } from \\"./some/stubbed/path/resources\\"; -export { UserImplementedEventType } from \\"./some/stubbed/path/resources\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - } - /** - * docs - */ - interface MyNewComponent { - } -} -export interface MyComponentCustomEvent extends CustomEvent { - detail: T; - target: HTMLMyComponentElement; -} -export interface MyNewComponentCustomEvent extends CustomEvent { - detail: T; - target: HTMLMyNewComponentElement; -} -declare global { - interface HTMLMyComponentElementEventMap { - \\"myEvent\\": UserImplementedEventType; - } - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLMyNewComponentElementEventMap { - \\"myEvent\\": UserImplementedEventType; - } - /** - * docs - */ - interface HTMLMyNewComponentElement extends Components.MyNewComponent, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMyNewComponentElement, ev: MyNewComponentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMyNewComponentElement, ev: MyNewComponentCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMyNewComponentElement: { - prototype: HTMLMyNewComponentElement; - new (): HTMLMyNewComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - \\"my-new-component\\": HTMLMyNewComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"onMyEvent\\"?: (event: MyComponentCustomEvent) => void; - } - /** - * docs - */ - interface MyNewComponent { - \\"onMyEvent\\"?: (event: MyNewComponentCustomEvent) => void; - } - interface IntrinsicElements { - \\"my-component\\": MyComponent; - \\"my-new-component\\": MyNewComponent; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - /** - * docs - */ - \\"my-new-component\\": LocalJSX.IntrinsicElements[\\"my-new-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom event types should generate a type declaration file with multiple custom events from the same location 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { SecondUserImplementedEventType, UserImplementedEventType } from \\"./some/stubbed/path/resources\\"; -export { SecondUserImplementedEventType, UserImplementedEventType } from \\"./some/stubbed/path/resources\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - } -} -export interface MyComponentCustomEvent extends CustomEvent { - detail: T; - target: HTMLMyComponentElement; -} -declare global { - interface HTMLMyComponentElementEventMap { - \\"myEvent\\": UserImplementedEventType; - \\"mySecondEvent\\": SecondUserImplementedEventType; - } - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"onMyEvent\\"?: (event: MyComponentCustomEvent) => void; - \\"onMySecondEvent\\"?: (event: MyComponentCustomEvent) => void; - } - interface IntrinsicElements { - \\"my-component\\": MyComponent; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom event types should handle custom event type name collisions when defined in separate files 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedEventType } from \\"./some/stubbed/path/a/resources\\"; -import { UserImplementedEventType as UserImplementedEventType1 } from \\"./some/stubbed/path/b/resources\\"; -export { UserImplementedEventType } from \\"./some/stubbed/path/a/resources\\"; -export { UserImplementedEventType as UserImplementedEventType1 } from \\"./some/stubbed/path/b/resources\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - } - /** - * docs - */ - interface MyNewComponent { - } -} -export interface MyComponentCustomEvent extends CustomEvent { - detail: T; - target: HTMLMyComponentElement; -} -export interface MyNewComponentCustomEvent extends CustomEvent { - detail: T; - target: HTMLMyNewComponentElement; -} -declare global { - interface HTMLMyComponentElementEventMap { - \\"myEvent\\": UserImplementedEventType; - } - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLMyNewComponentElementEventMap { - \\"myEvent\\": UserImplementedEventType1; - } - /** - * docs - */ - interface HTMLMyNewComponentElement extends Components.MyNewComponent, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMyNewComponentElement, ev: MyNewComponentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMyNewComponentElement, ev: MyNewComponentCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMyNewComponentElement: { - prototype: HTMLMyNewComponentElement; - new (): HTMLMyNewComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - \\"my-new-component\\": HTMLMyNewComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"onMyEvent\\"?: (event: MyComponentCustomEvent) => void; - } - /** - * docs - */ - interface MyNewComponent { - \\"onMyEvent\\"?: (event: MyNewComponentCustomEvent) => void; - } - interface IntrinsicElements { - \\"my-component\\": MyComponent; - \\"my-new-component\\": MyNewComponent; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - /** - * docs - */ - \\"my-new-component\\": LocalJSX.IntrinsicElements[\\"my-new-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom event types should handle custom event type name collisions when defined in the component files 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedEventType } from \\"./some/stubbed/path/a/my-component\\"; -import { UserImplementedEventType as UserImplementedEventType1 } from \\"./some/stubbed/path/b/my-new-component\\"; -export { UserImplementedEventType } from \\"./some/stubbed/path/a/my-component\\"; -export { UserImplementedEventType as UserImplementedEventType1 } from \\"./some/stubbed/path/b/my-new-component\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - } - /** - * docs - */ - interface MyNewComponent { - } -} -export interface MyComponentCustomEvent extends CustomEvent { - detail: T; - target: HTMLMyComponentElement; -} -export interface MyNewComponentCustomEvent extends CustomEvent { - detail: T; - target: HTMLMyNewComponentElement; -} -declare global { - interface HTMLMyComponentElementEventMap { - \\"myEvent\\": UserImplementedEventType; - } - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLMyNewComponentElementEventMap { - \\"myEvent\\": UserImplementedEventType1; - } - /** - * docs - */ - interface HTMLMyNewComponentElement extends Components.MyNewComponent, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMyNewComponentElement, ev: MyNewComponentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMyNewComponentElement, ev: MyNewComponentCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMyNewComponentElement: { - prototype: HTMLMyNewComponentElement; - new (): HTMLMyNewComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - \\"my-new-component\\": HTMLMyNewComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"onMyEvent\\"?: (event: MyComponentCustomEvent) => void; - } - /** - * docs - */ - interface MyNewComponent { - \\"onMyEvent\\"?: (event: MyNewComponentCustomEvent) => void; - } - interface IntrinsicElements { - \\"my-component\\": MyComponent; - \\"my-new-component\\": MyNewComponent; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - /** - * docs - */ - \\"my-new-component\\": LocalJSX.IntrinsicElements[\\"my-new-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom prop types should export prop types too 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedPropType } from \\"./some/stubbed/path/resources\\"; -export { UserImplementedPropType } from \\"./some/stubbed/path/resources\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"name\\": UserImplementedPropType; - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"name\\"?: UserImplementedPropType; - } - - interface MyComponentAttributes { - \\"name\\": UserImplementedPropType; - } - - interface IntrinsicElements { - \\"my-component\\": Omit & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`attr:\${K}\`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`prop:\${K}\`]?: MyComponent[K] }; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom prop types should generate a type declaration file with multiple components using the same custom prop type 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedPropType } from \\"./some/stubbed/path/resources\\"; -export { UserImplementedPropType } from \\"./some/stubbed/path/resources\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"name\\": UserImplementedPropType; - } - /** - * docs - */ - interface MyNewComponent { - \\"fullName\\": UserImplementedPropType; - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - /** - * docs - */ - interface HTMLMyNewComponentElement extends Components.MyNewComponent, HTMLStencilElement { - } - var HTMLMyNewComponentElement: { - prototype: HTMLMyNewComponentElement; - new (): HTMLMyNewComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - \\"my-new-component\\": HTMLMyNewComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"name\\"?: UserImplementedPropType; - } - /** - * docs - */ - interface MyNewComponent { - \\"fullName\\"?: UserImplementedPropType; - } - - interface MyComponentAttributes { - \\"name\\": UserImplementedPropType; - } - interface MyNewComponentAttributes { - \\"fullName\\": UserImplementedPropType; - } - - interface IntrinsicElements { - \\"my-component\\": Omit & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`attr:\${K}\`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`prop:\${K}\`]?: MyComponent[K] }; - \\"my-new-component\\": Omit & { [K in keyof MyNewComponent & keyof MyNewComponentAttributes]?: MyNewComponent[K] } & { [K in keyof MyNewComponent & keyof MyNewComponentAttributes as \`attr:\${K}\`]?: MyNewComponentAttributes[K] } & { [K in keyof MyNewComponent & keyof MyNewComponentAttributes as \`prop:\${K}\`]?: MyNewComponent[K] }; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - /** - * docs - */ - \\"my-new-component\\": LocalJSX.IntrinsicElements[\\"my-new-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom prop types should generate a type declaration file with multiple custom prop types from the same location 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { SecondUserImplementedPropType, UserImplementedPropType } from \\"./some/stubbed/path/resources\\"; -export { SecondUserImplementedPropType, UserImplementedPropType } from \\"./some/stubbed/path/resources\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"email\\": SecondUserImplementedPropType; - \\"name\\": UserImplementedPropType; - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"email\\"?: SecondUserImplementedPropType; - \\"name\\"?: UserImplementedPropType; - } - - interface MyComponentAttributes { - \\"name\\": UserImplementedPropType; - \\"email\\": SecondUserImplementedPropType; - } - - interface IntrinsicElements { - \\"my-component\\": Omit & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`attr:\${K}\`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`prop:\${K}\`]?: MyComponent[K] }; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom prop types should handle custom prop type name collisions when defined in separate files 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedPropType } from \\"./some/stubbed/path/a/resources\\"; -import { UserImplementedPropType as UserImplementedPropType1 } from \\"./some/stubbed/path/b/resources\\"; -export { UserImplementedPropType } from \\"./some/stubbed/path/a/resources\\"; -export { UserImplementedPropType as UserImplementedPropType1 } from \\"./some/stubbed/path/b/resources\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"name\\": UserImplementedPropType; - } - /** - * docs - */ - interface MyNewComponent { - \\"newName\\": UserImplementedPropType1; - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - /** - * docs - */ - interface HTMLMyNewComponentElement extends Components.MyNewComponent, HTMLStencilElement { - } - var HTMLMyNewComponentElement: { - prototype: HTMLMyNewComponentElement; - new (): HTMLMyNewComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - \\"my-new-component\\": HTMLMyNewComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"name\\"?: UserImplementedPropType; - } - /** - * docs - */ - interface MyNewComponent { - \\"newName\\"?: UserImplementedPropType1; - } - - interface MyComponentAttributes { - \\"name\\": UserImplementedPropType; - } - interface MyNewComponentAttributes { - \\"newName\\": UserImplementedPropType; - } - - interface IntrinsicElements { - \\"my-component\\": Omit & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`attr:\${K}\`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`prop:\${K}\`]?: MyComponent[K] }; - \\"my-new-component\\": Omit & { [K in keyof MyNewComponent & keyof MyNewComponentAttributes]?: MyNewComponent[K] } & { [K in keyof MyNewComponent & keyof MyNewComponentAttributes as \`attr:\${K}\`]?: MyNewComponentAttributes[K] } & { [K in keyof MyNewComponent & keyof MyNewComponentAttributes as \`prop:\${K}\`]?: MyNewComponent[K] }; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - /** - * docs - */ - \\"my-new-component\\": LocalJSX.IntrinsicElements[\\"my-new-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes custom prop types should handle custom prop type name collisions when defined in the component files 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedPropType } from \\"./some/stubbed/path/a/my-component\\"; -import { UserImplementedPropType as UserImplementedPropType1 } from \\"./some/stubbed/path/b/my-new-component\\"; -export { UserImplementedPropType } from \\"./some/stubbed/path/a/my-component\\"; -export { UserImplementedPropType as UserImplementedPropType1 } from \\"./some/stubbed/path/b/my-new-component\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"name\\": UserImplementedPropType; - } - /** - * docs - */ - interface MyNewComponent { - \\"name\\": UserImplementedPropType1; - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - /** - * docs - */ - interface HTMLMyNewComponentElement extends Components.MyNewComponent, HTMLStencilElement { - } - var HTMLMyNewComponentElement: { - prototype: HTMLMyNewComponentElement; - new (): HTMLMyNewComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - \\"my-new-component\\": HTMLMyNewComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"name\\"?: UserImplementedPropType; - } - /** - * docs - */ - interface MyNewComponent { - \\"name\\"?: UserImplementedPropType1; - } - - interface MyComponentAttributes { - \\"name\\": UserImplementedPropType; - } - interface MyNewComponentAttributes { - \\"name\\": UserImplementedPropType; - } - - interface IntrinsicElements { - \\"my-component\\": Omit & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`attr:\${K}\`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`prop:\${K}\`]?: MyComponent[K] }; - \\"my-new-component\\": Omit & { [K in keyof MyNewComponent & keyof MyNewComponentAttributes]?: MyNewComponent[K] } & { [K in keyof MyNewComponent & keyof MyNewComponentAttributes as \`attr:\${K}\`]?: MyNewComponentAttributes[K] } & { [K in keyof MyNewComponent & keyof MyNewComponentAttributes as \`prop:\${K}\`]?: MyNewComponent[K] }; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - /** - * docs - */ - \\"my-new-component\\": LocalJSX.IntrinsicElements[\\"my-new-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes should generate a type declaration file without custom types 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - } - interface IntrinsicElements { - \\"my-component\\": MyComponent; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes should handle type import aliases 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { MyType as UserImplementedPropType } from \\"@utils\\"; -import { Fragment } from \\"@stencil/core\\"; -export { MyType as UserImplementedPropType } from \\"@utils\\"; -export { Fragment } from \\"@stencil/core\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"name\\": UserImplementedPropType; - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"name\\"?: UserImplementedPropType; - } - - interface MyComponentAttributes { - \\"name\\": UserImplementedPropType; - } - - interface IntrinsicElements { - \\"my-component\\": Omit & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`attr:\${K}\`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`prop:\${K}\`]?: MyComponent[K] }; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes should not transform aliased paths if transformAliasedImportPaths is false 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedPropType } from \\"@utils\\"; -export { UserImplementedPropType } from \\"@utils\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"name\\": UserImplementedPropType; - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"name\\"?: UserImplementedPropType; - } - - interface MyComponentAttributes { - \\"name\\": UserImplementedPropType; - } - - interface IntrinsicElements { - \\"my-component\\": Omit & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`attr:\${K}\`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`prop:\${K}\`]?: MyComponent[K] }; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes should transform aliased paths if transformAliasedImportPaths is true 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedPropType } from \\"./some/stubbed/path/utils/utils\\"; -export { UserImplementedPropType } from \\"./some/stubbed/path/utils/utils\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"name\\": UserImplementedPropType; - } -} -declare global { - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"name\\"?: UserImplementedPropType; - } - - interface MyComponentAttributes { - \\"name\\": UserImplementedPropType; - } - - interface IntrinsicElements { - \\"my-component\\": Omit & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`attr:\${K}\`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`prop:\${K}\`]?: MyComponent[K] }; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; - -exports[`generateAppTypes should work with both event and prop types 1`] = ` -"/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from \\"@stencil/core/internal\\"; -import { UserImplementedEventType, UserImplementedPropType } from \\"./some/stubbed/path/a/resources\\"; -export { UserImplementedEventType, UserImplementedPropType } from \\"./some/stubbed/path/a/resources\\"; -export namespace Components { - /** - * docs - */ - interface MyComponent { - \\"name\\": UserImplementedPropType; - } -} -export interface MyComponentCustomEvent extends CustomEvent { - detail: T; - target: HTMLMyComponentElement; -} -declare global { - interface HTMLMyComponentElementEventMap { - \\"myEvent\\": UserImplementedEventType; - } - /** - * docs - */ - interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMyComponentElement: { - prototype: HTMLMyComponentElement; - new (): HTMLMyComponentElement; - }; - interface HTMLElementTagNameMap { - \\"my-component\\": HTMLMyComponentElement; - } -} -declare namespace LocalJSX { - /** - * docs - */ - interface MyComponent { - \\"name\\"?: UserImplementedPropType; - \\"onMyEvent\\"?: (event: MyComponentCustomEvent) => void; - } - - interface MyComponentAttributes { - \\"name\\": UserImplementedPropType; - } - - interface IntrinsicElements { - \\"my-component\\": Omit & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`attr:\${K}\`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as \`prop:\${K}\`]?: MyComponent[K] }; - } -} -export { LocalJSX as JSX }; -declare module \\"@stencil/core\\" { - export namespace JSX { - interface IntrinsicElements { - /** - * docs - */ - \\"my-component\\": LocalJSX.IntrinsicElements[\\"my-component\\"] & JSXBase.HTMLAttributes; - } - } -} -" -`; diff --git a/src/compiler/types/tests/tsconfig.json b/src/compiler/types/tests/tsconfig.json deleted file mode 100644 index 0ff0be83173..00000000000 --- a/src/compiler/types/tests/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../testing/tsconfig.internal.json" -} diff --git a/src/compiler/types/tests/validate-package-json.spec.ts b/src/compiler/types/tests/validate-package-json.spec.ts deleted file mode 100644 index 5f6e14ed42c..00000000000 --- a/src/compiler/types/tests/validate-package-json.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockBuildCtx, mockCompilerCtx, mockValidatedConfig } from '@stencil/core/testing'; -import { normalizePath } from '@utils'; -import path from 'path'; - -import * as v from '../validate-build-package-json'; - -describe('validate-package-json', () => { - let config: d.ValidatedConfig; - let compilerCtx: d.CompilerCtx; - let buildCtx: d.BuildCtx; - let collectionOutputTarget: d.OutputTargetDistCollection; - const root = path.resolve('/'); - - beforeEach(async () => { - collectionOutputTarget = { - type: 'dist-collection', - dir: '/dist', - collectionDir: '/dist/collection', - }; - - const namespace = 'SomeNamespace'; - config = mockValidatedConfig({ - devMode: false, - fsNamespace: namespace.toLowerCase(), - namespace, - packageJsonFilePath: path.join(root, 'package.json'), - }); - compilerCtx = mockCompilerCtx(config); - buildCtx = mockBuildCtx(config, compilerCtx); - buildCtx.packageJson = {}; - await compilerCtx.fs.writeFile(config.packageJsonFilePath, JSON.stringify(buildCtx.packageJson)); - }); - - describe('files', () => { - it('should validate files "dist/"', async () => { - const distPath = path.join(root, 'dist'); - await compilerCtx.fs.emptyDirs([distPath]); - await compilerCtx.fs.commit(); - buildCtx.packageJson.files = ['dist/']; - await v.validatePackageFiles(config, compilerCtx, buildCtx, collectionOutputTarget); - expect(buildCtx.diagnostics).toHaveLength(0); - }); - - it('should validate files "./dist/"', async () => { - const distPath = path.join(root, 'dist'); - await compilerCtx.fs.emptyDirs([distPath]); - await compilerCtx.fs.commit(); - buildCtx.packageJson.files = ['./dist/']; - await v.validatePackageFiles(config, compilerCtx, buildCtx, collectionOutputTarget); - expect(buildCtx.diagnostics).toHaveLength(0); - }); - - it('should validate files "./dist"', async () => { - const distPath = path.join(root, 'dist'); - await compilerCtx.fs.emptyDirs([distPath]); - await compilerCtx.fs.commit(); - buildCtx.packageJson.files = ['./dist']; - await v.validatePackageFiles(config, compilerCtx, buildCtx, collectionOutputTarget); - expect(buildCtx.diagnostics).toHaveLength(0); - }); - - it('should validate files "dist"', async () => { - const distPath = path.join(root, 'dist'); - await compilerCtx.fs.emptyDirs([distPath]); - await compilerCtx.fs.commit(); - buildCtx.packageJson.files = ['dist']; - await v.validatePackageFiles(config, compilerCtx, buildCtx, collectionOutputTarget); - expect(buildCtx.diagnostics).toHaveLength(0); - }); - - it('should error when files array misses dist/', async () => { - buildCtx.packageJson.files = []; - await v.validatePackageFiles(config, compilerCtx, buildCtx, collectionOutputTarget); - expect(buildCtx.diagnostics[0].messageText).toMatch(/array must contain the distribution directory/); - expect(buildCtx.diagnostics[0].messageText).toMatch(/"dist\/"/); - }); - }); - - describe('main', () => { - it('main cannot be the old loader', async () => { - compilerCtx.fs.writeFile(path.join(root, 'dist', 'somenamespace.js'), ''); - compilerCtx.fs.writeFile(path.join(root, 'dist', 'index.cjs.js'), ''); - buildCtx.packageJson.main = 'dist/somenamespace.js'; - v.validateMain(config, compilerCtx, buildCtx, collectionOutputTarget); - expect(buildCtx.diagnostics).toHaveLength(1); - }); - - it('validate main', async () => { - compilerCtx.fs.writeFile(path.join(root, 'dist', 'index.cjs.js'), ''); - buildCtx.packageJson.main = 'dist/index.cjs.js'; - v.validateMain(config, compilerCtx, buildCtx, collectionOutputTarget); - expect(buildCtx.diagnostics).toHaveLength(0); - }); - - it('missing main', async () => { - v.validateMain(config, compilerCtx, buildCtx, collectionOutputTarget); - expect(buildCtx.diagnostics).toHaveLength(1); - }); - }); - - describe('collection', () => { - it('should produce a warning when missing collection property', async () => { - v.validateCollection(config, compilerCtx, buildCtx, collectionOutputTarget); - - expect(buildCtx.diagnostics[0].messageText).toMatch(/package.json "collection" property is required/); - expect(buildCtx.diagnostics[0].level).toBe('warn'); - }); - - it('should produce a warning if the supplied path does not match the recommended path', () => { - buildCtx.packageJson.collection = 'bad/path'; - - v.validateCollection(config, compilerCtx, buildCtx, collectionOutputTarget); - - expect(buildCtx.diagnostics[0].messageText).toBe( - `package.json "collection" property is required when generating a distribution and must be set to: ${normalizePath( - 'dist/collection/collection-manifest.json', - false, - )}`, - ); - expect(buildCtx.diagnostics[0].level).toBe('warn'); - }); - - it('should not produce a warning if the normalized paths are the same', () => { - buildCtx.packageJson.collection = './dist/collection/collection-manifest.json'; - - v.validateCollection(config, compilerCtx, buildCtx, collectionOutputTarget); - - expect(buildCtx.diagnostics.length).toEqual(0); - }); - }); -}); diff --git a/src/compiler/types/tests/validate-primary-package-output-target.spec.ts b/src/compiler/types/tests/validate-primary-package-output-target.spec.ts deleted file mode 100644 index 1b5aa16bfb7..00000000000 --- a/src/compiler/types/tests/validate-primary-package-output-target.spec.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { mockBuildCtx, mockCompilerCtx, mockValidatedConfig } from '@stencil/core/testing'; - -import type * as d from '../../../declarations'; -import { - PRIMARY_PACKAGE_TARGET_CONFIGS, - PrimaryPackageOutputTargetRecommendedConfig, - validateModulePath, - validatePrimaryPackageOutputTarget, - validateTypesPath, -} from '../validate-primary-package-output-target'; - -describe('validatePrimaryPackageOutputTarget', () => { - let config: d.ValidatedConfig; - let compilerCtx: d.CompilerCtx; - let buildCtx: d.BuildCtx; - - beforeEach(() => { - config = mockValidatedConfig({ - validatePrimaryPackageOutputTarget: true, - outputTargets: [ - { - type: 'dist', - isPrimaryPackageOutputTarget: true, - dir: '/dist', - typesDir: '/dist/types', - }, - ], - }); - - compilerCtx = mockCompilerCtx(config); - compilerCtx.fs.accessSync = () => true; - - buildCtx = mockBuildCtx(config, compilerCtx); - buildCtx.packageJson.module = 'dist/index.js'; - buildCtx.packageJson.types = 'dist/types/index.d.ts'; - }); - - describe('check basic Stencil config scenarios', () => { - it('should log a warning if `validatePrimaryPackageOutputTarget` is `false` but primary targets are set', () => { - config.validatePrimaryPackageOutputTarget = false; - - validatePrimaryPackageOutputTarget(config, compilerCtx, buildCtx); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('warn'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - 'Your Stencil project has designated a primary package output target without enabling primary package validation for your project. Either set `validatePrimaryPackageOutputTarget: true` in your Stencil config or remove `isPrimaryPackageOutputTarget: true` from all output targets. You can read more about primary package output targets in the Stencil docs: https://stenciljs.com/docs/output-targets#primary-package-output-target-validation', - ); - }); - - it('should log a warning if any non-eligible targets were marked as `isPrimaryPackageOutputTarget`', () => { - config.outputTargets = [ - { - type: 'copy', - isPrimaryPackageOutputTarget: true, - }, - ] as any[]; - - validatePrimaryPackageOutputTarget(config, compilerCtx, buildCtx); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('warn'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - `Your Stencil project has assigned one or more ineligible output targets as the primary package output target. No validation will take place. Please remove the 'isPrimaryPackageOutputTarget' flag from the following output targets in your Stencil config: copy. You can read more about primary package output targets in the Stencil docs: https://stenciljs.com/docs/output-targets#primary-package-output-target-validation`, - ); - }); - - it('should log a warning if no eligible targets were marked as `isPrimaryPackageOutputTarget`', () => { - config.outputTargets = [ - { - type: 'dist', - }, - ]; - - validatePrimaryPackageOutputTarget(config, compilerCtx, buildCtx); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('warn'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - `Your Stencil project has not assigned a primary package output target. Stencil recommends that you assign a primary output target so it can validate values for fields in your project's 'package.json'. You can read more about primary package output targets in the Stencil docs: https://stenciljs.com/docs/output-targets#primary-package-output-target-validation`, - ); - }); - - it('should log a warning if multiple targets were marked as `isPrimaryPackageOutputTarget`', () => { - config.outputTargets = [ - ...config.outputTargets, - { - type: 'dist-custom-elements', - isPrimaryPackageOutputTarget: true, - }, - ]; - - validatePrimaryPackageOutputTarget(config, compilerCtx, buildCtx); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('warn'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - `Your Stencil config has multiple output targets with 'isPrimaryPackageOutputTarget: true'. Stencil does not support validating 'package.json' fields for multiple output targets. Please remove the 'isPrimaryPackageOutputTarget' flag from all but one of the following output targets: dist, dist-custom-elements. For now, Stencil will use the first primary target it finds. You can read more about primary package output targets in the Stencil docs: https://stenciljs.com/docs/output-targets#primary-package-output-target-validation`, - ); - }); - }); - - describe('validateModulePath', () => { - it('should log a warning if no module path is provided', () => { - delete buildCtx.packageJson.module; - - const targetToValidate: d.EligiblePrimaryPackageOutputTarget = { - type: 'dist', - dir: '/dist', - }; - const recommendedOutputTargetConfig = PRIMARY_PACKAGE_TARGET_CONFIGS[targetToValidate.type]; - - validateModulePath(config, compilerCtx, buildCtx, recommendedOutputTargetConfig, targetToValidate); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('warn'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - `package.json "module" property is required when generating a distribution. It's recommended to set the "module" property to: ./dist/index.js`, - ); - }); - - describe.each<[d.EligiblePrimaryPackageOutputTarget & { toString(): string }, string]>([ - [ - { - type: 'dist', - dir: '/dist', - toString: () => 'dist', - }, - './dist/index.js', - ], - [ - { - type: 'dist-collection', - dir: '/dist', - collectionDir: '/dist/collection', - toString: () => 'dist-collection', - }, - './dist/index.js', - ], - [ - { - type: 'dist-custom-elements', - dir: '/dist/components', - toString: () => 'dist-custom-elements', - }, - './dist/components/index.js', - ], - ])('output target type - %s', (outputTarget, recommendedPath) => { - it('should log a warning if the set module path does not match the recommended path', () => { - buildCtx.packageJson.module = '/dist/tmp/index.js'; - - validateModulePath( - config, - compilerCtx, - buildCtx, - PRIMARY_PACKAGE_TARGET_CONFIGS[outputTarget.type], - outputTarget, - ); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('warn'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - `package.json "module" property is set to "${buildCtx.packageJson.module}". It's recommended to set the "module" property to: ${recommendedPath}`, - ); - }); - - it('should not log a warning if the recommended path is used', () => { - buildCtx.packageJson.module = recommendedPath; - - validateModulePath( - config, - compilerCtx, - buildCtx, - PRIMARY_PACKAGE_TARGET_CONFIGS[outputTarget.type], - outputTarget, - ); - - expect(buildCtx.diagnostics.length).toBe(0); - }); - }); - }); - - describe('validateTypesPath', () => { - let targetToValidate: d.EligiblePrimaryPackageOutputTarget; - let recommendedOutputTargetConfig: PrimaryPackageOutputTargetRecommendedConfig; - - beforeEach(() => { - targetToValidate = { - type: 'dist-types', - dir: '/dist/types', - typesDir: '/dist/types', - }; - recommendedOutputTargetConfig = PRIMARY_PACKAGE_TARGET_CONFIGS[targetToValidate.type]; - }); - - it('should log a warning if no types path is provided', () => { - delete buildCtx.packageJson.types; - - validateTypesPath(config, compilerCtx, buildCtx, recommendedOutputTargetConfig, targetToValidate); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('warn'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - `package.json "types" property is required when generating a distribution. It's recommended to set the "types" property to: ./dist/types/index.d.ts`, - ); - }); - - it('should log a warning if the types path does not have a ".d.ts" extension', () => { - buildCtx.packageJson.types = '/dist/types/index.ts'; - - validateTypesPath(config, compilerCtx, buildCtx, recommendedOutputTargetConfig, targetToValidate); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('warn'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - `package.json "types" file must have a ".d.ts" extension. The "types" property is currently set to: /dist/types/index.ts`, - ); - }); - - it('should log a error if the types file cannot be accessed', () => { - compilerCtx.fs.accessSync = () => false; - - validateTypesPath(config, compilerCtx, buildCtx, recommendedOutputTargetConfig, targetToValidate); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('error'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - `package.json "types" property is set to "dist/types/index.d.ts" but cannot be found.`, - ); - }); - - describe.each<[d.EligiblePrimaryPackageOutputTarget & { toString(): string }, string]>([ - [ - { - type: 'dist', - dir: '/dist', - typesDir: '/dist/types', - toString: () => 'dist', - }, - './dist/types/index.d.ts', - ], - [ - { - type: 'dist-types', - dir: '/dist', - typesDir: '/dist/types', - toString: () => 'dist-types', - }, - './dist/types/index.d.ts', - ], - [ - { - type: 'dist-custom-elements', - dir: '/dist/components', - generateTypeDeclarations: true, - toString: () => 'dist-custom-elements', - }, - './dist/components/index.d.ts', - ], - ])('output target type - %s', (outputTarget, recommendedPath) => { - it('should log a warning if the set types path does not match the recommended path', () => { - buildCtx.packageJson.types = '/dist/tmp/index.d.ts'; - - validateTypesPath( - config, - compilerCtx, - buildCtx, - PRIMARY_PACKAGE_TARGET_CONFIGS[outputTarget.type], - outputTarget, - ); - - expect(buildCtx.diagnostics.length).toBe(1); - expect(buildCtx.diagnostics[0].level).toEqual('warn'); - expect(buildCtx.diagnostics[0].messageText).toEqual( - `package.json "types" property is set to "${buildCtx.packageJson.types}". It's recommended to set the "types" property to: ${recommendedPath}`, - ); - }); - - it('should not log anything if the recommended path is used and accessible', () => { - buildCtx.packageJson.types = recommendedPath; - - validateTypesPath( - config, - compilerCtx, - buildCtx, - PRIMARY_PACKAGE_TARGET_CONFIGS[outputTarget.type], - targetToValidate, - ); - - expect(buildCtx.diagnostics.length).toBe(0); - }); - }); - }); -}); diff --git a/src/compiler/types/validate-build-package-json.ts b/src/compiler/types/validate-build-package-json.ts deleted file mode 100644 index 715813bc955..00000000000 --- a/src/compiler/types/validate-build-package-json.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { - COLLECTION_MANIFEST_FILE_NAME, - isGlob, - isOutputTargetDistCollection, - isString, - join, - normalizePath, - relative, -} from '@utils'; -import { dirname } from 'path'; - -import type * as d from '../../declarations'; -import { packageJsonError, packageJsonWarn } from './package-json-log-utils'; -import { validatePrimaryPackageOutputTarget } from './validate-primary-package-output-target'; - -/** - * Validate the package.json file for a project, checking that various fields - * are set correctly for the currently-configured output targets. - * - * @param config the project's Stencil config - * @param compilerCtx the compiler context - * @param buildCtx the build context - * @returns an empty Promise - */ -export const validateBuildPackageJson = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -): Promise => { - if (config.watch || buildCtx.packageJson == null) { - return; - } - - // Validate any output target that the user has designated as the "primary" - // target that is bundled with their distribution - validatePrimaryPackageOutputTarget(config, compilerCtx, buildCtx); - - const distCollectionOutputTargets = config.outputTargets.filter(isOutputTargetDistCollection); - await Promise.all( - distCollectionOutputTargets.map((distCollectionOT) => - validateDistCollectionPkgJson(config, compilerCtx, buildCtx, distCollectionOT), - ), - ); -}; - -/** - * Validate package.json contents for the `DIST_COLLECTION` output target, - * checking that various fields like `files`, `main`, and so on are set - * correctly. - * - * @param config the stencil config - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param outputTarget a DIST_COLLECTION output target - */ -const validateDistCollectionPkgJson = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTarget: d.OutputTargetDistCollection, -) => { - await Promise.all([ - validatePackageFiles(config, compilerCtx, buildCtx, outputTarget), - validateMain(config, compilerCtx, buildCtx, outputTarget), - validateCollection(config, compilerCtx, buildCtx, outputTarget), - validateBrowser(config, compilerCtx, buildCtx), - ]); -}; - -/** - * Validate that the `files` field in `package.json` contains directories and - * files that are necessary for the `DIST_COLLECTION` output target. - * - * @param config the stencil config - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param outputTarget a DIST_COLLECTION output target - */ -export const validatePackageFiles = async ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTarget: d.OutputTargetDistCollection, -) => { - if (!config.devMode && Array.isArray(buildCtx.packageJson.files)) { - const actualDistDir = normalizePath(relative(config.rootDir, outputTarget.dir)); - - const validPaths = [`${actualDistDir}`, `${actualDistDir}/`, `./${actualDistDir}`, `./${actualDistDir}/`]; - - const containsDistDir = buildCtx.packageJson.files.some((userPath) => - validPaths.some((validPath) => normalizePath(userPath) === validPath), - ); - - if (!containsDistDir) { - const msg = `package.json "files" array must contain the distribution directory "${actualDistDir}/" when generating a distribution.`; - packageJsonWarn(config, compilerCtx, buildCtx, msg, `"files"`); - return; - } - - await Promise.all( - buildCtx.packageJson.files.map(async (pkgFile) => { - if (!isGlob(pkgFile)) { - const packageJsonDir = dirname(config.packageJsonFilePath); - const absPath = join(packageJsonDir, pkgFile); - - const hasAccess = await compilerCtx.fs.access(absPath); - if (!hasAccess) { - const msg = `Unable to find "${pkgFile}" within the package.json "files" array.`; - packageJsonError(config, compilerCtx, buildCtx, msg, `"${pkgFile}"`); - } - } - }), - ); - } -}; - -/** - * Check that the `main` field is set correctly in `package.json` for the - * `DIST_COLLECTION` output target. - * - * @param config the stencil config - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param outputTarget a DIST_COLLECTION output target - */ -export const validateMain = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTarget: d.OutputTargetDistCollection, -) => { - const mainAbs = join(outputTarget.dir, 'index.cjs.js'); - const mainRel = relative(config.rootDir, mainAbs); - - if (!isString(buildCtx.packageJson.main) || buildCtx.packageJson.main === '') { - const msg = `package.json "main" property is required when generating a distribution. It's recommended to set the "main" property to: ${mainRel}`; - packageJsonWarn(config, compilerCtx, buildCtx, msg, `"main"`); - } else if (normalizePath(buildCtx.packageJson.main) !== normalizePath(mainRel)) { - const msg = `package.json "main" property is set to "${buildCtx.packageJson.main}". It's recommended to set the "main" property to: ${mainRel}`; - packageJsonWarn(config, compilerCtx, buildCtx, msg, `"main"`); - } -}; - -/** - * Check that the `collection` field is set correctly in `package.json` for the - * `DIST_COLLECTION` output target. - * - * @param config the stencil config - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - * @param outputTarget a DIST_COLLECTION output target - */ -export const validateCollection = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - outputTarget: d.OutputTargetDistCollection, -) => { - if (outputTarget.collectionDir) { - const collectionRel = normalizePath( - join(relative(config.rootDir, outputTarget.collectionDir), COLLECTION_MANIFEST_FILE_NAME), - false, - ); - if (!buildCtx.packageJson.collection || normalizePath(buildCtx.packageJson.collection, false) !== collectionRel) { - const msg = `package.json "collection" property is required when generating a distribution and must be set to: ${collectionRel}`; - packageJsonWarn(config, compilerCtx, buildCtx, msg, `"collection"`); - } - } -}; - -/** - * Check that the `browser` field is set correctly in `package.json` for the - * `DIST_COLLECTION` output target. - * - * @param config the stencil config - * @param compilerCtx the current compiler context - * @param buildCtx the current build context - */ -export const validateBrowser = (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - if (isString(buildCtx.packageJson.browser)) { - const msg = `package.json "browser" property is set to "${buildCtx.packageJson.browser}". However, for maximum compatibility with all bundlers it's recommended to not set the "browser" property and instead ensure both "module" and "main" properties are set.`; - packageJsonWarn(config, compilerCtx, buildCtx, msg, `"browser"`); - } -}; diff --git a/src/compiler/types/validate-primary-package-output-target.ts b/src/compiler/types/validate-primary-package-output-target.ts deleted file mode 100644 index df6e0609efc..00000000000 --- a/src/compiler/types/validate-primary-package-output-target.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { buildWarn, isEligiblePrimaryPackageOutputTarget, isString, join, normalizePath, relative } from '@utils'; - -import type * as d from '../../declarations'; -import { packageJsonError, packageJsonWarn } from './package-json-log-utils'; - -/** - * Contains utility methods that can be used to generate recommended values for a - * project's `package.json` fields that get validated for output targets designated - * as `isPrimaryPackageOutputTarget`. - */ -export type PrimaryPackageOutputTargetRecommendedConfig = { - /** - * Generates the recommended path for the `module` property based on the output target type, - * the project's root directory, and the output target's designated output location. - * - * @param rootDir The Stencil project's root directory pulled from the validated config. - * @param outputTargetDir The output directory for the output target's compiled code. - * @returns The recommended path for the `module` property in a project's `package.json` - */ - getModulePath: (rootDir: string, outputTargetDir: string) => string | null; - /** - * Generates the recommended path for the `types` property based on the output target type, - * the project's root directory, and the output target's configuration. - * - * `outputTargetConfig` is typed as `any` because downstream consumers may run into type conflicts - * with the `type` property of all the different "eligible" output targets. - * - * @param rootDir The Stencil project's root directory pulled from the validated config. - * @param outputTargetConfig The output target's config. - * @returns The recommended path for the `types` property in a project's `package.json` - */ - getTypesPath: (rootDir: string, outputTargetConfig: any) => string | null; - /** - * Generates the recommended path for the `main` property based on the output target type, - * the project's root directory, and the output target's designated output location. - * - * Only used for generate export maps. - * - * @param rootDir The Stencil project's root directory pulled from the validated config. - * @param outputTargetDir The output directory for the output target's compiled code. - * @returns The recommended path for the `main` property in a project's `package.json` - */ - getMainPath: (rootDir: string, outputTargetDir: string) => string | null; -}; - -/** - * Contains a `PrimaryPackageOutputTargetRecommendedConfig` for each output target - * that can be marked as `isPrimaryPackageOutputTarget`. Each config defines how - * it will generate recommended values for certain `package.json` fields. - */ -export const PRIMARY_PACKAGE_TARGET_CONFIGS = { - dist: { - getModulePath: (rootDir: string, outputTargetDir: string) => - normalizePath(relative(rootDir, join(outputTargetDir, 'index.js'))), - getTypesPath: (rootDir: string, outputTargetConfig: any) => - normalizePath(relative(rootDir, join(outputTargetConfig.typesDir!, 'index.d.ts'))), - getMainPath: (rootDir: string, outputTargetDir: string) => - normalizePath(relative(rootDir, join(outputTargetDir, 'index.cjs.js'))), - }, - 'dist-collection': { - getModulePath: (rootDir: string, outputTargetDir: string) => - normalizePath(relative(rootDir, join(outputTargetDir, 'index.js'))), - getTypesPath: () => null, - getMainPath: () => null, - }, - 'dist-custom-elements': { - getModulePath: (rootDir: string, outputTargetDir: string) => - normalizePath(relative(rootDir, join(outputTargetDir, 'index.js'))), - getTypesPath: (rootDir: string, outputTargetConfig: any) => { - return outputTargetConfig.generateTypeDeclarations - ? normalizePath(relative(rootDir, join(outputTargetConfig.dir!, 'index.d.ts'))) - : null; - }, - getMainPath: () => null, - }, - 'dist-types': { - getModulePath: () => null, - getTypesPath: (rootDir: string, outputTargetConfig: any) => - normalizePath(relative(rootDir, join(outputTargetConfig.typesDir, 'index.d.ts'))), - getMainPath: () => null, - }, -} satisfies Record; - -/** - * Performs validation for specified fields in a Stencil project's - * `package.json` based on output targets being designated as - * `isPrimaryPackageOutputTarget`. - * - * @param config The Stencil project's config. - * @param compilerCtx The project's compiler context. - * @param buildCtx The project's build context. - */ -export const validatePrimaryPackageOutputTarget = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, -) => { - if (config.validatePrimaryPackageOutputTarget) { - const eligiblePrimaryTargets: d.EligiblePrimaryPackageOutputTarget[] = []; - const nonPrimaryTargets: d.OutputTarget[] = []; - - // Push each output target in the config into its respective - // classification for validation messages - // Using a `foreach` prevents us from iterating over - // the array multiple times - config.outputTargets.forEach((ref) => { - if (isEligiblePrimaryPackageOutputTarget(ref)) { - eligiblePrimaryTargets.push(ref); - } else { - nonPrimaryTargets.push(ref); - } - }); - - // If there are no output targets designated as "primary", then we should warn the user - // to designate one. In this case, we aren't gonna do any validation - if (eligiblePrimaryTargets.length) { - const targetsMarkedToValidate = eligiblePrimaryTargets.filter((ref) => ref.isPrimaryPackageOutputTarget); - - if (targetsMarkedToValidate.length) { - // A user should only designate one target to validate against - // Log a warning if they try to have more than one, but we'll only validate - // the first one in the array - if (targetsMarkedToValidate.length > 1) { - logValidationWarning( - buildCtx, - `Your Stencil config has multiple output targets with 'isPrimaryPackageOutputTarget: true'. Stencil does not support validating 'package.json' fields for multiple output targets. Please remove the 'isPrimaryPackageOutputTarget' flag from all but one of the following output targets: ${targetsMarkedToValidate - .map((ref) => ref.type) - .join( - ', ', - )}. For now, Stencil will use the first primary target it finds. You can read more about primary package output targets in the Stencil docs: https://stenciljs.com/docs/output-targets#primary-package-output-target-validation`, - ); - } - - // Validate shared fields - // Currently, this is only `module` and `types` - // We only validate the first target that is designated - const targetToValidate = targetsMarkedToValidate[0]; - const recommendedConfig = PRIMARY_PACKAGE_TARGET_CONFIGS[targetToValidate.type]; - if (recommendedConfig != null) { - validateModulePath(config, compilerCtx, buildCtx, recommendedConfig, targetToValidate); - validateTypesPath(config, compilerCtx, buildCtx, recommendedConfig, targetToValidate); - } - } else { - logValidationWarning( - buildCtx, - `Your Stencil project has not assigned a primary package output target. Stencil recommends that you assign a primary output target so it can validate values for fields in your project's 'package.json'. You can read more about primary package output targets in the Stencil docs: https://stenciljs.com/docs/output-targets#primary-package-output-target-validation`, - ); - } - } - - // Log a warning if any targets that cannot be validated were marked as "primary" - if (nonPrimaryTargets.length && nonPrimaryTargets.some((ref: any) => ref.isPrimaryPackageOutputTarget)) { - logValidationWarning( - buildCtx, - `Your Stencil project has assigned one or more ineligible output targets as the primary package output target. No validation will take place. Please remove the 'isPrimaryPackageOutputTarget' flag from the following output targets in your Stencil config: ${nonPrimaryTargets - .filter((ref: any) => ref.isPrimaryPackageOutputTarget === true) - .map((ref) => ref.type) - .join( - ', ', - )}. You can read more about primary package output targets in the Stencil docs: https://stenciljs.com/docs/output-targets#primary-package-output-target-validation`, - ); - } - } else { - if (config.outputTargets.some((ref: any) => ref.isPrimaryPackageOutputTarget)) { - logValidationWarning( - buildCtx, - 'Your Stencil project has designated a primary package output target without enabling primary package validation for your project. Either set `validatePrimaryPackageOutputTarget: true` in your Stencil config or remove `isPrimaryPackageOutputTarget: true` from all output targets. You can read more about primary package output targets in the Stencil docs: https://stenciljs.com/docs/output-targets#primary-package-output-target-validation', - ); - } - } -}; - -/** - * Validates the `module` field in a Stencil project's `package.json`. This function performs - * basic checks for a value to be set for `module` as well as checks that the specified path - * matches Stencil's recommended value based on the output target the user designated to be used - * for validation (i.e. `isPrimaryPackageOutputTarget: true`). - * - * If a value does not exist or does not match the recommended path, a _warning_ will be logged to - * the console at build time. - * - * @param config The Stencil project's config. - * @param compilerCtx The project's compiler context. - * @param buildCtx The project's build context. - * @param recommendedOutputTargetConfig The config object containing the function to generate the Stencil - * recommended value for the `module` path based on the output target type. - * @param targetToValidate The output target to validate against. - */ -export const validateModulePath = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - recommendedOutputTargetConfig: PrimaryPackageOutputTargetRecommendedConfig, - targetToValidate: d.EligiblePrimaryPackageOutputTarget, -) => { - const currentModulePath = buildCtx.packageJson.module; - const recommendedModulePath = recommendedOutputTargetConfig.getModulePath - ? recommendedOutputTargetConfig.getModulePath(config.rootDir, targetToValidate.dir!) - : null; - - let warningMessage: string | null = null; - if (!isString(currentModulePath) || currentModulePath === '') { - warningMessage = 'package.json "module" property is required when generating a distribution.'; - - if (recommendedModulePath != null) { - warningMessage += ` It's recommended to set the "module" property to: ${recommendedModulePath}`; - } - } else if (recommendedModulePath != null && recommendedModulePath !== normalizePath(currentModulePath)) { - warningMessage = `package.json "module" property is set to "${currentModulePath}". It's recommended to set the "module" property to: ${recommendedModulePath}`; - } - - if (warningMessage?.length) { - packageJsonWarn(config, compilerCtx, buildCtx, warningMessage, `"module"`); - } -}; - -/** - * Validates the `types` field in a Stencil project's `package.json`. This function performs - * basic checks for a value to be set for `types` as well as checks that the specified path - * matches Stencil's recommended value based on the output target the user designated to be used - * for validation (i.e. `isPrimaryPackageOutputTarget: true`). - * - * If a value does not exist or does not match the recommended path, a warning _or_ error will be logged to - * the console at build time. - * - * @param config The Stencil project's config. - * @param compilerCtx The project's compiler context. - * @param buildCtx The project's build context. - * @param recommendedOutputTargetConfig The config object containing the function to generate the Stencil - * recommended value for the `types` path based on the output target type. - * @param targetToValidate The output target to validate against. - */ -export const validateTypesPath = ( - config: d.ValidatedConfig, - compilerCtx: d.CompilerCtx, - buildCtx: d.BuildCtx, - recommendedOutputTargetConfig: PrimaryPackageOutputTargetRecommendedConfig, - targetToValidate: d.EligiblePrimaryPackageOutputTarget, -) => { - const currentTypesPath = buildCtx.packageJson.types; - const recommendedTypesPath = recommendedOutputTargetConfig.getTypesPath - ? recommendedOutputTargetConfig.getTypesPath(config.rootDir, targetToValidate) - : null; - - let warningMessage: string | null = null; - let errorMessage: string | null = null; - if (!isString(currentTypesPath) || currentTypesPath === '') { - warningMessage = `package.json "types" property is required when generating a distribution. It's recommended to set the "types" property to: ${recommendedTypesPath}`; - } else if (!currentTypesPath.endsWith('.d.ts')) { - warningMessage = `package.json "types" file must have a ".d.ts" extension. The "types" property is currently set to: ${currentTypesPath}`; - } else if (recommendedTypesPath != null && recommendedTypesPath !== normalizePath(currentTypesPath)) { - warningMessage = `package.json "types" property is set to "${currentTypesPath}". It's recommended to set the "types" property to: ${recommendedTypesPath}`; - } else { - const typesFile = join(config.rootDir, currentTypesPath); - const typesFileExists = compilerCtx.fs.accessSync(typesFile); - if (!typesFileExists) { - errorMessage = `package.json "types" property is set to "${currentTypesPath}" but cannot be found.`; - } - } - - if (errorMessage?.length) { - packageJsonError(config, compilerCtx, buildCtx, errorMessage, `"types"`); - } else if (warningMessage?.length) { - packageJsonWarn(config, compilerCtx, buildCtx, warningMessage, `"types"`); - } -}; - -const logValidationWarning = (buildCtx: d.BuildCtx, message: string) => { - const warning = buildWarn(buildCtx.diagnostics); - warning.header = 'Stencil Config'; - warning.messageText = message; -}; diff --git a/src/declarations/readme.md b/src/declarations/readme.md deleted file mode 100644 index b8471d99896..00000000000 --- a/src/declarations/readme.md +++ /dev/null @@ -1,33 +0,0 @@ -# Declarations - - -## `index.ts` - -Index of every declaration within Stencil's source for convenience. Exports both public and private declarations. Meant to only be used by Stencil's source code so `* as d from './declarations` is easy to use. - - -## `stencil-private` - -Declarations like `CompilerCtx` and `BuildCtx` would be in here. Declarations in this file should always be safe to refactor and are never meant to be used by external code. - - -## `stencil-public-compiler` - -Build time declarations for the compiler that can be publicly exposed, but this file itself is never directly imported by user code. Declarations like `Config` and `OutputTarget` would be in here. - - -## `stencil-public-runtime` - -Client-side declarations for the runtime that can be publicly exposed, but this file itself is never directly imported by user code. Declarations like `HTMLStencilElement`, `JSXBase`, and `Component` would be in here. - -This is also the file that would be copied to distribution `dist/types` directories. For example, a dist `dist/types/components.d.ts` file would start with `import { HTMLStencilElement, JSXBase } from './stencil.public';`, so the `stencil.public.runtime.d.ts` file should be a sibling. A distribution copy of Stencil Core declarations should not have a dependency of `@stencil/core`. - - -## `stencil-core` - -The actual public declarations when `@stencil/core` is imported by developer code. This should be a minimal list that exports with specific declarations from `stencil.public.compiler` and `stencil.public.runtime`. - - -## `stencil-ext-modules` - -The TypeScript declaration file used so that TypeScript can import `.svg` or `.css` files without throwing errors. Build steps will manually copy this to the correct location. \ No newline at end of file diff --git a/src/declarations/stencil-ext-modules.d.ts b/src/declarations/stencil-ext-modules.d.ts deleted file mode 100644 index ffec762996c..00000000000 --- a/src/declarations/stencil-ext-modules.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -declare module '*.css' { - const src: () => string; - export default src; -} - -declare module '*.svg' { - const src: string; - export default src; -} - -declare module '*.txt' { - const src: string; - export default src; -} - -declare module '*.frag' { - const src: string; - export default src; -} - -declare module '*.vert' { - const src: string; - export default src; -} - -declare module '*?worker' { - export const worker: Worker; - export const workerMsgId: string; - export const workerName: string; - export const workerPath: string; -} - -declare module '*?format=url' { - const src: string; - export default src; -} - -declare module '*?format=text' { - const content: string; - export default content; -} diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts deleted file mode 100644 index a4269ea886f..00000000000 --- a/src/declarations/stencil-private.ts +++ /dev/null @@ -1,2861 +0,0 @@ -import { result } from '@utils'; - -import type { InMemoryFileSystem } from '../compiler/sys/in-memory-fs'; -import type { CPSerializable } from './child_process'; -import type { - BuildEvents, - BuildLog, - BuildResultsComponentGraph, - CompilerBuildResults, - CompilerFsStats, - CompilerRequestResponse, - CompilerSystem, - Config, - CopyResults, - DevServerConfig, - DevServerEditor, - Diagnostic, - Logger, - LoggerLineUpdater, - LoggerTimeSpan, - OptimizeCssInput, - OptimizeCssOutput, - OutputTarget, - OutputTargetWww, - PageReloadStrategy, - PrerenderConfig, - StyleDoc, - TaskCommand, - ValidatedConfig, -} from './stencil-public-compiler'; -import type { JsonDocMethodParameter } from './stencil-public-docs'; -import type { ComponentInterface, ListenTargetOptions, VNode } from './stencil-public-runtime'; - -export interface DocData { - hostIds: number; - rootLevelIds: number; - staticComponents: Set; -} -export type StencilDocument = Document & { _stencilDocData: DocData }; - -export interface SourceMap { - file: string; - mappings: string; - names: string[]; - sourceRoot?: string; - sources: string[]; - sourcesContent?: (string | null)[]; - version: number; -} - -export interface PrintLine { - lineIndex: number; - lineNumber: number; - text: string; - errorCharStart: number; - errorLength?: number; -} - -export interface AssetsMeta { - absolutePath: string; - cmpRelativePath: string; - originalComponentPath: string; -} - -export interface ParsedImport { - importPath: string; - basename: string; - ext: string; - data: ImportData; -} - -export interface ImportData { - tag?: string; - encapsulation?: string; - mode?: string; -} - -export interface SerializeImportData extends ImportData { - importeePath: string; - importerPath?: string; - /** - * True if this is a node module import (e.g. using ~ prefix like ~foo/style.css) - * These should be treated as bare module specifiers and not have ./ prepended - */ - isNodeModule?: boolean; -} - -export interface BuildFeatures { - // encapsulation - style: boolean; - mode: boolean; - formAssociated: boolean; - - // dom - shadowDom: boolean; - shadowDelegatesFocus: boolean; - shadowSlotAssignmentManual: boolean; - scoped: boolean; - - // render - /** - * Every component has a render function - */ - allRenderFn: boolean; - /** - * At least one component has a render function - */ - hasRenderFn: boolean; - - // vdom - vdomRender: boolean; - vdomAttribute: boolean; - vdomClass: boolean; - vdomFunctional: boolean; - vdomKey: boolean; - vdomListener: boolean; - vdomPropOrAttr: boolean; - vdomRef: boolean; - vdomStyle: boolean; - vdomText: boolean; - vdomXlink: boolean; - slotRelocation: boolean; - - // elements - slot: boolean; - svg: boolean; - - // decorators - element: boolean; - event: boolean; - hostListener: boolean; - hostListenerTargetWindow: boolean; - hostListenerTargetDocument: boolean; - hostListenerTargetBody: boolean; - /** - * @deprecated Prevented from new apps, but left in for older collections - */ - hostListenerTargetParent: boolean; - hostListenerTarget: boolean; - method: boolean; - prop: boolean; - propChangeCallback: boolean; - propMutable: boolean; - state: boolean; - member: boolean; - updatable: boolean; - propBoolean: boolean; - propNumber: boolean; - propString: boolean; - serializer: boolean; - deserializer: boolean; - - // lifecycle events - lifecycle: boolean; - asyncLoading: boolean; - - // attr - observeAttribute: boolean; - reflect: boolean; - - taskQueue: boolean; -} - -export interface BuildConditionals extends Partial { - hotModuleReplacement?: boolean; - isDebug?: boolean; - isTesting?: boolean; - isDev?: boolean; - devTools?: boolean; - invisiblePrehydration?: boolean; - hydrateServerSide?: boolean; - hydrateClientSide?: boolean; - lifecycleDOMEvents?: boolean; - cssAnnotations?: boolean; - lazyLoad?: boolean; - profile?: boolean; - constructableCSS?: boolean; - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - appendChildSlotFix?: boolean; - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - slotChildNodesFix?: boolean; - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - scopedSlotTextContentFix?: boolean; - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - cloneNodeFix?: boolean; - hydratedAttribute?: boolean; - hydratedClass?: boolean; - hydratedSelectorName?: string; - initializeNextTick?: boolean; - // TODO(STENCIL-1305): remove this option - scriptDataOpts?: boolean; - // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field - shadowDomShim?: boolean; - asyncQueue?: boolean; - // TODO: deprecated in favour of `setTagTransformer` and `transformTag`. Remove in 5.0 - transformTagName?: boolean; - additionalTagTransformers?: boolean | 'prod'; - attachStyles?: boolean; - - // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior - experimentalSlotFixes?: boolean; - // TODO(STENCIL-1086): remove this option when it's the default behavior - experimentalScopedSlotChanges?: boolean; - addGlobalStyleToComponents?: boolean | 'client'; -} - -export type ModuleFormat = - | 'amd' - | 'cjs' - | 'es' - | 'iife' - | 'system' - | 'umd' - | 'commonjs' - | 'esm' - | 'module' - | 'systemjs'; - -export interface RollupResultModule { - id: string; -} -export interface RollupResults { - modules: RollupResultModule[]; -} - -export interface UpdatedLazyBuildCtx { - name: 'esm-browser' | 'esm' | 'cjs' | 'system'; - buildCtx: BuildCtx; -} - -export interface BuildCtx { - buildId: number; - buildResults: CompilerBuildResults; - buildStats?: result.Result; - buildMessages: string[]; - bundleBuildCount: number; - collections: CollectionCompilerMeta[]; - compilerCtx: CompilerCtx; - esmBrowserComponentBundle: ReadonlyArray; - esmComponentBundle: ReadonlyArray; - es5ComponentBundle: ReadonlyArray; - systemComponentBundle: ReadonlyArray; - commonJsComponentBundle: ReadonlyArray; - components: ComponentCompilerMeta[]; - componentGraph: Map; - config: ValidatedConfig; - createTimeSpan(msg: string, debug?: boolean): LoggerTimeSpan; - data: any; - debug: (msg: string) => void; - diagnostics: Diagnostic[]; - dirsAdded: string[]; - dirsDeleted: string[]; - entryModules: EntryModule[]; - filesAdded: string[]; - filesChanged: string[]; - filesDeleted: string[]; - filesUpdated: string[]; - filesWritten: string[]; - globalStyle: string | undefined; - hasConfigChanges: boolean; - hasError: boolean; - hasFinished: boolean; - hasHtmlChanges: boolean; - hasPrintedResults: boolean; - hasServiceWorkerChanges: boolean; - hasScriptChanges: boolean; - hasStyleChanges: boolean; - hasWarning: boolean; - hydrateAppFilePath: string; - indexBuildCount: number; - indexDoc: Document; - isRebuild: boolean; - /** - * A collection of Stencil's intermediate representation of components, tied to the current build - */ - moduleFiles: Module[]; - packageJson: PackageJsonData; - pendingCopyTasks: Promise[]; - progress(task: BuildTask): void; - requiresFullBuild: boolean; - rollupResults?: RollupResults; - scriptsAdded: string[]; - scriptsDeleted: string[]; - startTime: number; - styleBuildCount: number; - /** - * A promise that resolves to the global styles for the current build. - */ - stylesPromise: Promise; - stylesUpdated: BuildStyleUpdate[]; - timeSpan: LoggerTimeSpan; - timestamp: string; - transpileBuildCount: number; - validateTypesBuild?(): Promise; - validateTypesHandler?: (results: any) => Promise; - validateTypesPromise?: Promise; -} - -export interface BuildStyleUpdate { - styleTag: string; - styleText: string; - styleMode: string; -} - -export type BuildTask = any; - -export interface CompilerBuildStats { - timestamp: string; - compiler: { - name: string; - version: string; - }; - app: { - namespace: string; - fsNamespace: string; - components: number; - entries: number; - bundles: number; - outputs: any; - }; - options: { - minifyJs: boolean; - minifyCss: boolean; - hashFileNames: boolean; - hashedFileNameLength: number; - buildEs5: boolean | 'prod'; - }; - formats: { - esmBrowser: ReadonlyArray; - esm: ReadonlyArray; - es5: ReadonlyArray; - system: ReadonlyArray; - commonjs: ReadonlyArray; - }; - components: BuildComponent[]; - entries: EntryModule[]; - rollupResults: RollupResults; - sourceGraph?: BuildSourceGraph; - componentGraph: BuildResultsComponentGraph; - collections: CompilerBuildStatCollection[]; -} - -export interface CompilerBuildStatCollection { - name: string; - source: string; - tags: string[][]; -} - -export interface CompilerBuildStatBundle { - key: string; - components: string[]; - bundleId: string; - fileName: string; - imports: string[]; - originalByteSize: number; -} - -export interface BuildSourceGraph { - [filePath: string]: string[]; -} - -export interface BuildComponent { - tag: string; - dependencyOf?: string[]; - dependencies?: string[]; -} - -export type SourceTarget = 'es5' | 'es2017' | 'latest'; - -/** - * A note regarding Rollup types: - * As of this writing, there is no great way to import external types for packages that are directly embedded in the - * Stencil source. As a result, some types are duplicated here for Rollup that will be used within the codebase. - * Updates to rollup may require these typings to be updated. - */ - -export type RollupResult = RollupChunkResult | RollupAssetResult; - -export interface RollupAssetResult { - type: 'asset'; - fileName: string; - content: string; -} - -export interface RollupChunkResult { - type: 'chunk'; - entryKey: string; - fileName: string; - code: string; - isEntry: boolean; - isComponent: boolean; - isCore: boolean; - isIndex: boolean; - isBrowserLoader: boolean; - imports: string[]; - moduleFormat: ModuleFormat; - map?: RollupSourceMap; -} - -export interface RollupSourceMap { - file: string; - mappings: string; - names: string[]; - sources: string[]; - sourcesContent: string[]; - version: number; - toString(): string; - toUrl(): string; -} - -/** - * Result of Stencil compressing, mangling, and otherwise 'minifying' JavaScript - */ -export type OptimizeJsResult = { - output: string; - diagnostics: Diagnostic[]; - sourceMap?: SourceMap; -}; - -export interface BundleModule { - entryKey: string; - rollupResult: RollupChunkResult; - cmps: ComponentCompilerMeta[]; - output: BundleModuleOutput; -} - -export interface BundleModuleOutput { - bundleId: string; - fileName: string; - code: string; -} - -export interface Cache { - get(key: string): Promise; - put(key: string, value: string): Promise; - has(key: string): Promise; - createKey(domain: string, ...args: any[]): Promise; - commit(): Promise; - clear(): void; - clearDiskCache(): Promise; - getMemoryStats(): string; - initCacheDir(): Promise; -} - -export interface CollectionCompilerMeta { - collectionName: string; - moduleId?: string; - moduleDir: string; - moduleFiles: Module[]; - global?: Module; - compiler?: CollectionCompilerVersion; - isInitialized?: boolean; - hasExports?: boolean; - dependencies?: string[]; - bundles?: { - components: string[]; - }[]; -} - -export interface CollectionCompilerVersion { - name: string; - version: string; - typescriptVersion?: string; -} - -export interface CollectionManifest { - entries?: CollectionComponentEntryPath[]; - /** - * Paths to mixin/abstract class modules that can be extended by consuming projects. - * These are modules that contain classes with Stencil static members (properties, states, etc.) - * but are not components themselves (no @Component decorator / tag name). - */ - mixins?: CollectionComponentEntryPath[]; - collections?: CollectionDependencyManifest[]; - global?: string; - compiler?: CollectionCompilerVersion; - bundles?: CollectionBundleManifest[]; -} - -export type CollectionComponentEntryPath = string; - -export interface CollectionBundleManifest { - components: string[]; -} - -export interface CollectionDependencyManifest { - name: string; - tags: string[]; -} - -export interface CollectionCompiler { - name: string; - version: string; - typescriptVersion?: string; -} - -export interface CollectionDependencyData { - name: string; - tags: string[]; -} - -export interface CompilerCtx { - version: number; - activeBuildId: number; - activeDirsAdded: string[]; - activeDirsDeleted: string[]; - activeFilesAdded: string[]; - activeFilesDeleted: string[]; - activeFilesUpdated: string[]; - addWatchDir: (path: string, recursive: boolean) => void; - addWatchFile: (path: string) => void; - cache: Cache; - cssModuleImports: Map; - cachedGlobalStyle: string; - collections: CollectionCompilerMeta[]; - compilerOptions: any; - events: BuildEvents; - fs: InMemoryFileSystem; - hasSuccessfulBuild: boolean; - isActivelyBuilding: boolean; - lastBuildResults: CompilerBuildResults; - /** - * A mapping of a file path to a Stencil {@link Module} - */ - moduleMap: ModuleMap; - nodeMap: NodeMap; - resolvedCollections: Set; - rollupCacheHydrate: any; - rollupCacheLazy: any; - rollupCacheNative: any; - styleModeNames: Set; - changedModules: Set; - changedFiles: Set; - worker?: CompilerWorkerContext; - - rollupCache: Map; - - reset(): void; -} - -export type NodeMap = WeakMap; - -/** - * Record, for a specific component, whether or not it has various features - * which need to be handled correctly in the compilation pipeline. - * - * Note: this must be serializable to JSON. - */ -export interface ComponentCompilerFeatures { - hasAttribute: boolean; - hasAttributeChangedCallbackFn: boolean; - hasComponentWillLoadFn: boolean; - hasComponentDidLoadFn: boolean; - hasComponentShouldUpdateFn: boolean; - hasComponentWillUpdateFn: boolean; - hasComponentDidUpdateFn: boolean; - hasComponentWillRenderFn: boolean; - hasComponentDidRenderFn: boolean; - hasConnectedCallbackFn: boolean; - hasDeserializer: boolean; - hasDisconnectedCallbackFn: boolean; - hasElement: boolean; - hasEvent: boolean; - hasLifecycle: boolean; - hasListener: boolean; - hasListenerTarget: boolean; - hasListenerTargetWindow: boolean; - hasListenerTargetDocument: boolean; - hasListenerTargetBody: boolean; - /** - * @deprecated Prevented from new apps, but left in for older collections - */ - hasListenerTargetParent: boolean; - hasMember: boolean; - hasMethod: boolean; - hasMode: boolean; - hasModernPropertyDecls: boolean; - hasProp: boolean; - hasPropBoolean: boolean; - hasPropNumber: boolean; - hasPropString: boolean; - hasPropMutable: boolean; - hasReflect: boolean; - hasRenderFn: boolean; - hasSerializer: boolean; - hasSlot: boolean; - hasState: boolean; - hasStyle: boolean; - hasVdomAttribute: boolean; - hasVdomClass: boolean; - hasVdomFunctional: boolean; - hasVdomKey: boolean; - hasVdomListener: boolean; - hasVdomPropOrAttr: boolean; - hasVdomRef: boolean; - hasVdomRender: boolean; - hasVdomStyle: boolean; - hasVdomText: boolean; - hasVdomXlink: boolean; - hasWatchCallback: boolean; - htmlAttrNames: string[]; - htmlTagNames: string[]; - htmlParts: string[]; - isUpdateable: boolean; - /** - * A plain component is one that doesn't have: - * - any members decorated with `@Prop()`, `@State()`, `@Element()`, `@Method()` - * - any methods decorated with `@Listen()` - * - any styles - * - any lifecycle methods, including `render()` - */ - isPlain: boolean; - /** - * A collection of tag names of web components that a component references in its JSX/h() function - */ - potentialCmpRefs: string[]; -} - -/** - * Metadata about a given component - * - * Note: must be serializable to JSON! - */ -export interface ComponentCompilerMeta extends ComponentCompilerFeatures { - assetsDirs: CompilerAssetDir[]; - /** - * The name to which an `ElementInternals` object (the return value of - * `HTMLElement.attachInternals`) should be attached at runtime. If this is - * `null` then `attachInternals` should not be called. - */ - attachInternalsMemberName: string | null; - /** - * Custom states to initialize on the ElementInternals.states CustomStateSet. - * These are defined via @AttachInternals({ states: {...} }). - */ - attachInternalsCustomStates: ComponentCompilerCustomState[]; - componentClassName: string; - /** - * A list of web component tag names that are either: - * - directly referenced in a Stencil component's JSX/h() function - * - are referenced by a web component that is directly referenced in a Stencil component's JSX/h() function - */ - dependencies: string[]; - /** - * A list of web component tag names that either: - * - directly reference the current component directly in their JSX/h() function - * - indirectly/transitively reference the current component directly in their JSX/h() function - */ - dependents: string[]; - deserializers: ComponentCompilerChangeHandler[]; - /** - * A list of web component tag names that are directly referenced in a Stencil component's JSX/h() function - */ - directDependencies: string[]; - /** - * A list of web component tag names that the current component directly in their JSX/h() function - */ - directDependents: string[]; - docs: CompilerJsDoc; - doesExtend: boolean; - elementRef: string; - encapsulation: Encapsulation; - events: ComponentCompilerEvent[]; - excludeFromCollection: boolean; - /** - * Whether or not the component is form-associated - */ - formAssociated: boolean; - internal: boolean; - isCollectionDependency: boolean; - jsFilePath: string; - listeners: ComponentCompilerListener[]; - methods: ComponentCompilerMethod[]; - properties: ComponentCompilerProperty[]; - serializers: ComponentCompilerChangeHandler[]; - shadowDelegatesFocus: boolean; - /** - * Slot assignment mode for shadow DOM. 'manual', enables imperative slotting - * using HTMLSlotElement.assign(). Only applicable when encapsulation is 'shadow'. - */ - slotAssignment: 'manual' | null; - sourceFilePath: string; - sourceMapPath: string; - states: ComponentCompilerState[]; - styleDocs: CompilerStyleDoc[]; - styles: StyleCompiler[]; - tagName: string; - virtualProperties: ComponentCompilerVirtualProperty[]; - watchers: ComponentCompilerChangeHandler[]; -} - -/** - * The supported style encapsulation modes on a Stencil component: - * 1. 'shadow' - native Shadow DOM - * 2. 'scoped' - encapsulated styles and polyfilled slots - * 3. 'none' - a basic HTML element - */ -export type Encapsulation = 'shadow' | 'scoped' | 'none'; - -/** - * Intermediate Representation (IR) of a static property on a Stencil component - */ -export interface ComponentCompilerStaticProperty { - mutable: boolean; - optional: boolean; - required: boolean; - type: ComponentCompilerPropertyType; - complexType: ComponentCompilerPropertyComplexType; - attribute?: string; - reflect?: boolean; - docs: CompilerJsDoc; - defaultValue?: string; - getter: boolean; - setter: boolean; - ogPropName?: string; -} - -/** - * Intermediate Representation (IR) of a property on a Stencil component - */ -export interface ComponentCompilerProperty extends ComponentCompilerStaticProperty { - name: string; - internal: boolean; -} - -export interface ComponentCompilerVirtualProperty { - name: string; - type: string; - docs: string; -} - -export type ComponentCompilerPropertyType = 'any' | 'string' | 'boolean' | 'number' | 'unknown'; - -/** - * Information about a type used in a Stencil component or exported - * from a Stencil project. - */ -export interface ComponentCompilerPropertyComplexType { - /** - * The string of the original type annotation in the Stencil source code - */ - original: string; - /** - * A 'resolved' type, where e.g. imported types have been resolved and inlined - * - * For instance, an annotation like `(foo: Foo) => string;` will be - * converted to `(foo: { foo: string }) => string;`. - */ - resolved: string; - /** - * A record of the types which were referenced in the assorted type - * annotation in the original source file. - */ - references: ComponentCompilerTypeReferences; -} - -/** - * A record of `ComponentCompilerTypeReference` entities. - * - * Each key in this record is intended to be the names of the types used by a component. However, this is not enforced - * by the type system (I.E. any string can be used as a key). - * - * Note any key can be a user defined type or a TypeScript standard type. - */ -export type ComponentCompilerTypeReferences = Record; - -/** - * Describes a reference to a type used by a component. - */ -export interface ComponentCompilerTypeReference { - /** - * A type may be defined: - * - locally (in the same file as the component that uses it) - * - globally - * - by importing it into a file (and is defined elsewhere) - */ - location: 'local' | 'global' | 'import'; - /** - * The path to the type reference, if applicable (global types should not need a path associated with them) - */ - path?: string; - /** - * An ID for this type which is unique within a Stencil project. - */ - id: string; - /** - * Whether this type was imported as a default import (e.g., `import MyEnum from './my-enum'`) - * vs a named import (e.g., `import { MyType } from './my-type'`) - */ - isDefault?: boolean; - /** - * The name used in the import statement (before any user-defined alias). - * For `import { XAxisOption as moo }`, this would be "XAxisOption". - * This is the name exported by the source module. - */ - referenceLocation?: string; -} - -/** - * Information about a type which is referenced by another type on a Stencil - * component, for instance a {@link ComponentCompilerPropertyComplexType} or a - * {@link ComponentCompilerEventComplexType}. - */ -export interface ComponentCompilerReferencedType { - /** - * The path to the module where the type is declared. - */ - path: string; - /** - * The string of the original type annotation in the Stencil source code - */ - declaration: string; - /** - * An extracted docstring - */ - docstring: string; -} - -export interface ComponentCompilerStaticEvent { - name: string; - method: string; - bubbles: boolean; - cancelable: boolean; - composed: boolean; - docs: CompilerJsDoc; - complexType: ComponentCompilerEventComplexType; -} - -export interface ComponentCompilerEvent extends ComponentCompilerStaticEvent { - internal: boolean; -} - -export interface ComponentCompilerEventComplexType { - original: string; - resolved: string; - references: ComponentCompilerTypeReferences; -} - -export interface ComponentCompilerListener { - name: string; - method: string; - capture: boolean; - passive: boolean; - target: ListenTargetOptions | undefined; -} - -export interface ComponentCompilerStaticMethod { - docs: CompilerJsDoc; - complexType: ComponentCompilerMethodComplexType; -} - -export interface ComponentCompilerMethodComplexType { - signature: string; - parameters: JsonDocMethodParameter[]; - references: ComponentCompilerTypeReferences; - return: string; -} - -export interface ComponentCompilerChangeHandler { - propName: string; - methodName: string; - handlerOptions?: { - immediate?: boolean; - }; -} - -export interface ComponentCompilerMethod extends ComponentCompilerStaticMethod { - name: string; - internal: boolean; -} - -export interface ComponentCompilerState { - name: string; -} - -/** - * Metadata about a custom state defined via @AttachInternals({ states: {...} }) - * - * Custom states are exposed via the ElementInternals.states CustomStateSet - * and can be targeted with the CSS :state() pseudo-class. - */ -export interface ComponentCompilerCustomState { - /** - * The name of the custom state (without dashes) - */ - name: string; - /** - * The initial value of the state - */ - initialValue: boolean; - /** - * Optional JSDoc description for the state - */ - docs: string; -} - -/** - * Representation of JSDoc that is pulled off a node in the AST - */ -export interface CompilerJsDoc { - /** - * The text associated with the JSDoc - */ - text: string; - /** - * Tags included in the JSDoc - */ - tags: CompilerJsDocTagInfo[]; -} - -/** - * Representation of a tag that exists in a JSDoc - */ -export interface CompilerJsDocTagInfo { - /** - * The name of the tag - e.g. `@deprecated` - */ - name: string; - /** - * Additional text that is associated with the tag - e.g. `@deprecated use v2 of this API` - */ - text?: string; -} - -/** - * The (internal) representation of a CSS block comment in a CSS, Sass, etc. file. This data structure is used during - * the initial compilation phases of Stencil, as a piece of {@link ComponentCompilerMeta}. - */ -export interface CompilerStyleDoc { - /** - * The name of the CSS property - */ - name: string; - /** - * The user-defined description of the CSS property - */ - docs: string; - /** - * The JSDoc-style annotation (e.g. `@prop`) that was used in the block comment to detect the comment. - * Used to inform Stencil where the start of a new property's description starts (and where the previous description - * ends). - */ - annotation: 'prop'; - /** - * The Stencil style-mode that is associated with this property. - */ - mode: string; -} - -export interface CompilerAssetDir { - absolutePath?: string; - cmpRelativePath?: string; - originalComponentPath?: string; -} - -export interface ComponentCompilerData { - exportLine: string; - filePath: string; - cmp: ComponentCompilerMeta; - uniqueComponentClassName?: string; - importLine?: string; -} - -export interface ComponentConstructor { - is?: string; - properties?: ComponentConstructorProperties; - watchers?: ComponentConstructorChangeHandlers; - events?: ComponentConstructorEvent[]; - listeners?: ComponentConstructorListener[]; - style?: string; - styleId?: string; - encapsulation?: ComponentConstructorEncapsulation; - observedAttributes?: string[]; - cmpMeta?: ComponentRuntimeMeta; - isProxied?: boolean; - isStyleRegistered?: boolean; - serializers?: ComponentConstructorChangeHandlers; - deserializers?: ComponentConstructorChangeHandlers; -} - -/** - * A mapping from class member names to a list of methods which are watching - * them. - */ -export interface ComponentConstructorChangeHandlers { - [propName: string]: { [methodName: string]: number }[]; -} - -export interface ComponentTestingConstructor extends ComponentConstructor { - COMPILER_META: ComponentCompilerMeta; - prototype?: { - componentWillLoad?: Function; - componentWillUpdate?: Function; - componentWillRender?: Function; - __componentWillLoad?: Function | null; - __componentWillUpdate?: Function | null; - __componentWillRender?: Function | null; - }; -} - -export interface ComponentNativeConstructor extends ComponentConstructor { - cmpMeta: ComponentRuntimeMeta; -} - -export type ComponentConstructorEncapsulation = 'shadow' | 'scoped' | 'none'; - -export interface ComponentConstructorProperties { - [propName: string]: ComponentConstructorProperty; -} - -export interface ComponentConstructorProperty { - attribute?: string; - elementRef?: boolean; - method?: boolean; - mutable?: boolean; - reflect?: boolean; - state?: boolean; - type?: ComponentConstructorPropertyType; - watchCallbacks?: string[]; -} - -export type ComponentConstructorPropertyType = - | StringConstructor - | BooleanConstructor - | NumberConstructor - | 'string' - | 'boolean' - | 'number'; - -export interface ComponentConstructorEvent { - name: string; - method: string; - bubbles: boolean; - cancelable: boolean; - composed: boolean; -} - -export interface ComponentConstructorListener { - name: string; - method: string; - capture?: boolean; - passive?: boolean; -} - -export interface DevClientWindow extends Window { - ['s-dev-server']: boolean; - ['s-initial-load']: boolean; - ['s-build-id']: number; - WebSocket: new (socketUrl: string, protos: string[]) => WebSocket; - devServerConfig?: DevClientConfig; -} - -export interface DevClientConfig { - basePath: string; - editors: DevServerEditor[]; - reloadStrategy: PageReloadStrategy; - socketUrl?: string; -} - -export interface HttpRequest { - method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS'; - acceptHeader: string; - url: URL; - searchParams: URLSearchParams; - pathname?: string; - filePath?: string; - stats?: CompilerFsStats; - headers?: { [name: string]: string }; - host?: string; -} - -export interface DevServerMessage { - startServer?: DevServerConfig; - closeServer?: boolean; - serverStarted?: DevServerConfig; - serverClosed?: boolean; - buildStart?: boolean; - buildLog?: BuildLog; - buildResults?: CompilerBuildResults; - requestBuildResults?: boolean; - error?: { message?: string; type?: string; stack?: any }; - isActivelyBuilding?: boolean; - compilerRequestPath?: string; - compilerRequestResults?: CompilerRequestResponse; - requestLog?: { - method: string; - url: string; - status: number; - }; -} - -export type DevServerSendMessage = (msg: DevServerMessage) => void; - -export interface DevServerContext { - connectorHtml: string; - dirTemplate: string; - getBuildResults: () => Promise; - getCompilerRequest: (path: string) => Promise; - isServerListening: boolean; - logRequest: (req: HttpRequest, status: number) => void; - prerenderConfig: PrerenderConfig; - serve302: (req: any, res: any, pathname?: string) => void; - serve404: (req: any, res: any, xSource: string, content?: string) => void; - serve500: (req: any, res: any, error: any, xSource: string) => void; - sys: CompilerSystem; -} - -export type InitServerProcess = (sendMsg: (msg: DevServerMessage) => void) => (msg: DevServerMessage) => void; - -export interface DevResponseHeaders { - 'cache-control'?: string; - expires?: string; - 'content-type'?: string; - 'content-length'?: number; - date?: string; - 'access-control-allow-origin'?: string; - 'access-control-expose-headers'?: string; - 'content-encoding'?: 'gzip'; - vary?: 'Accept-Encoding'; - server?: string; - 'x-directory-index'?: string; - 'x-source'?: string; -} - -export interface OpenInEditorData { - file?: string; - line?: number; - column?: number; - open?: string; - editor?: string; - exists?: boolean; - error?: string; -} - -export interface EntryModule { - entryKey: string; - cmps: ComponentCompilerMeta[]; -} - -/** - * An interface extending `HTMLElement` which describes the fields added onto - * host HTML elements by the Stencil runtime. - */ -export interface HostElement extends HTMLElement { - // web component APIs - connectedCallback?: () => void; - attributeChangedCallback?: (attribName: string, oldVal: string, newVal: string, namespace: string) => void; - disconnectedCallback?: () => void; - host?: Element; - forceUpdate?: () => void; - - __stencil__getHostRef?: () => HostRef; - - // "s-" prefixed properties should not be property renamed - // and should be common between all versions of stencil - - /** - * Unique stencil id for this element - */ - ['s-id']?: string; - - /** - * Content Reference: - * Reference to the HTML Comment that's placed inside of the - * host element's original content. This comment is used to - * always represent where host element's light dom is. - */ - ['s-cr']?: RenderNode; - - /** - * Lifecycle ready - */ - ['s-lr']?: boolean; - - /** - * A reference to the `ElementInternals` object for the current host - * - * This is used for maintaining a reference to the object between HMR - * refreshes in the lazy build. - * - * "stencil-element-internals" - */ - ['s-ei']?: ElementInternals; - - /** - * On Render Callbacks: - * Array of callbacks to fire off after it has rendered. - */ - ['s-rc']?: (() => void)[]; - - /** - * Scope Id - * The scope id of this component when using scoped css encapsulation - * or using shadow dom but the browser doesn't support it - */ - ['s-sc']?: string; - - /** - * Scope Ids - * All the possible scope ids of this component when using scoped css encapsulation - * or using shadow dom but the browser doesn't support it - */ - ['s-scs']?: string[]; - - /** - * Hot Module Replacement, dev mode only - * - * This function should be defined by the HMR-supporting runtime and should - * do the work of actually updating the component in-place. - */ - ['s-hmr']?: (versionId: string) => void; - - /** - * A list of nested nested hydration promises that - * must be resolved for the top, ancestor component to be fully hydrated - */ - ['s-p']?: Promise[]; - - componentOnReady?: () => Promise; -} - -export interface HydrateResults { - buildId: string; - diagnostics: Diagnostic[]; - url: string; - host: string | null; - hostname: string | null; - href: string | null; - port: string | null; - pathname: string | null; - search: string | null; - hash: string | null; - html: string | null; - components: HydrateComponent[]; - anchors: HydrateAnchorElement[]; - imgs: HydrateImgElement[]; - scripts: HydrateScriptElement[]; - styles: HydrateStyleElement[]; - staticData: HydrateStaticData[]; - title: string | null; - hydratedCount: number; - httpStatus: number | null; -} - -export interface HydrateComponent { - tag: string; - mode: string; - count: number; - depth: number; -} - -export interface HydrateElement { - [attrName: string]: string | undefined; -} - -export interface HydrateAnchorElement extends HydrateElement { - href?: string; - target?: string; -} - -export interface HydrateImgElement extends HydrateElement { - src?: string; -} - -export interface HydrateScriptElement extends HydrateElement { - src?: string; - type?: string; -} - -export interface HydrateStyleElement extends HydrateElement { - id?: string; - href?: string; - content?: string; -} - -export interface HydrateStaticData { - id: string; - type: string; - content: string; -} - -export interface JsDoc { - name: string; - documentation: string; - type: string; - tags: JSDocTagInfo[]; - default?: string; - parameters?: JsDoc[]; - returns?: { - type: string; - documentation: string; - }; -} - -export interface JSDocTagInfo { - name: string; - text?: string; -} - -/** - * A mapping from a TypeScript or JavaScript source file path on disk, to a Stencil {@link Module}. - * - * It is advised that the key (path) be normalized before storing/retrieving the `Module` to avoid unnecessary lookup - * failures. - */ -export type ModuleMap = Map; - -/** - * Stencil's Intermediate Representation (IR) of a module, bundling together - * various pieces of information like the classes declared within it, the path - * to the original source file, HTML tag names defined in the file, and so on. - * - * Note that this gets serialized/parsed as JSON and therefore cannot contain a - * `Map` or a `Set`. - */ -export interface Module { - cmps: ComponentCompilerMeta[]; - isMixin: boolean; - isExtended: boolean; - /** - * Indicates this module contains mixin/abstract classes that can be extended by other projects. - * These are classes with Stencil static members (properties, states, etc.) but no @Component decorator. - */ - hasExportableMixins: boolean; - /** - * A collection of modules that a component will need. The modules in this list must have import statements generated - * in order for the component to function. - */ - coreRuntimeApis: string[]; - /** - * A collection of modules that a component will need for a specific output target. The modules in this list must - * have import statements generated in order for the component to function, but only for a specific output target. - */ - outputTargetCoreRuntimeApis: Partial>; - collectionName: string; - dtsFilePath: string; - excludeFromCollection: boolean; - externalImports: string[]; - htmlAttrNames: string[]; - htmlTagNames: string[]; - htmlParts: string[]; - isCollectionDependency: boolean; - isLegacy: boolean; - jsFilePath: string; - localImports: string[]; - /** - * Source file paths of functional components that are used in JSX/h() calls. - * This is used to ensure htmlTagNames are properly propagated from functional - * component dependencies even when they're accessed indirectly (e.g., via barrel files). - */ - functionalComponentDeps: string[]; - originalImports: string[]; - originalCollectionComponentPath: string; - potentialCmpRefs: string[]; - sourceFilePath: string; - staticSourceFile: any; - staticSourceFileText: string; - sourceMapPath: string; - sourceMapFileText: string; - - // build features - hasVdomAttribute: boolean; - hasVdomClass: boolean; - hasVdomFunctional: boolean; - hasVdomKey: boolean; - hasVdomListener: boolean; - hasVdomPropOrAttr: boolean; - hasVdomRef: boolean; - hasVdomRender: boolean; - hasVdomStyle: boolean; - hasVdomText: boolean; - hasVdomXlink: boolean; -} - -export interface Plugin { - name?: string; - pluginType?: string; - load?: (id: string, context: PluginCtx) => Promise | string; - resolveId?: (importee: string, importer: string, context: PluginCtx) => Promise | string; - transform?: ( - sourceText: string, - id: string, - context: PluginCtx, - ) => Promise | PluginTransformResults; -} - -export type PluginTransformResults = PluginTransformationDescriptor | string | null; - -export interface PluginTransformationDescriptor { - code?: string; - map?: string; - id?: string; - diagnostics?: Diagnostic[]; - dependencies?: string[]; -} - -export interface PluginCtx { - config: Config; - sys: CompilerSystem; - fs: InMemoryFileSystem; - cache: Cache; - diagnostics: Diagnostic[]; -} - -export interface PrerenderUrlResults { - anchorUrls: string[]; - diagnostics: Diagnostic[]; - filePath: string; -} - -export interface PrerenderUrlRequest { - appDir: string; - buildId: string; - baseUrl: string; - componentGraphPath: string; - devServerHostUrl: string; - hydrateAppFilePath: string; - isDebug: boolean; - prerenderConfigPath: string; - staticSite: boolean; - templateId: string; - url: string; - writeToFilePath: string; -} - -export interface PrerenderManager { - config: Config; - prerenderUrlWorker: (prerenderRequest: PrerenderUrlRequest) => Promise; - devServerHostUrl: string; - diagnostics: Diagnostic[]; - hydrateAppFilePath: string; - isDebug: boolean; - logCount: number; - outputTarget: OutputTargetWww; - prerenderConfig: PrerenderConfig; - prerenderConfigPath: string; - progressLogger?: LoggerLineUpdater; - resolve: Function; - staticSite: boolean; - templateId: string; - componentGraphPath: string; - urlsProcessing: Set; - urlsPending: Set; - urlsCompleted: Set; - maxConcurrency: number; -} - -/** - * Generic node that represents all of the - * different types of nodes we'd see when rendering - */ -export interface RenderNode extends HostElement { - /** - * Shadow root's host - */ - host?: Element; - - /** - * On Ref Function: - * Callback function to be called when the slotted node ref is ready. - */ - ['s-rf']?: (elm: Element) => unknown; - - /** - * Is initially hidden - * Whether this node was originally rendered with the `hidden` attribute. - * - * Used to reset the `hidden` state of a node during slot relocation. - */ - ['s-ih']?: boolean; - - /** - * Is Content Reference Node: - * This node is a content reference node. - */ - ['s-cn']?: boolean; - - /** - * Is a `slot` node when `shadow: false` (or `scoped: true`). - * - * This is a node (either empty text-node or `` element) - * that represents where a `` is located in the original JSX. - */ - ['s-sr']?: boolean; - - /** - * Slot name of either the slot itself or the slotted node - */ - ['s-sn']?: string; - - /** - * Host element tag name: - * The tag name of the host element that this - * node was created in. - */ - ['s-hn']?: string; - - /** - * Slot host tag name: - * This is the tag name of the element where this node - * has been moved to during slot relocation. - * - * This allows us to check if the node has been moved and prevent - * us from thinking a node _should_ be moved when it may already be in - * its final destination. - * - * This value is set to `undefined` whenever the node is put back into its original location. - */ - ['s-sh']?: string; - - /** - * Original Location Reference: - * A reference pointing to the comment - * which represents the original location - * before it was moved to its slot. - */ - ['s-ol']?: RenderNode; - - /** - * Node reference: - * This is a reference from an original location node - * back to the node that's been moved around. - */ - ['s-nr']?: PatchedSlotNode | RenderNode; - - /** - * Original Order: - * During SSR; a number representing the order of a slotted node - */ - ['s-oo']?: number; - - /** - * Scope Id - */ - ['s-si']?: string; - - /** - * Host Id (hydrate only) - */ - ['s-host-id']?: number; - - /** - * Node Id (hydrate only) - */ - ['s-node-id']?: number; - - /** - * Used to know the components encapsulation. - * empty "" for shadow, "c" from scoped - */ - ['s-en']?: '' | /*shadow*/ 'c' /*scoped*/; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the internal `childNodes` of the component - */ - readonly __childNodes?: NodeListOf; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the internal `children` of the component - */ - readonly __children?: HTMLCollectionOf; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the internal `firstChild` of the component - */ - readonly __firstChild?: ChildNode; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the internal `lastChild` of the component - */ - readonly __lastChild?: ChildNode; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the internal `textContent` of the component - */ - __textContent?: string; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * gives access to the original `append` method - */ - __append?: (...nodes: (Node | string)[]) => void; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * gives access to the original `prepend` method - */ - __prepend?: (...nodes: (Node | string)[]) => void; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * gives access to the original `appendChild` method - */ - __appendChild?: (newChild: T) => T; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * gives access to the original `insertBefore` method - */ - __insertBefore?: (node: T, child: Node | null) => T; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * gives access to the original `removeChild` method - */ - __removeChild?: (child: T) => T; -} - -export interface PatchedSlotNode extends Node { - /** - * Slot name - */ - ['s-sn']?: string; - - /** - * Original Location Reference: - * A reference pointing to the comment - * which represents the original location - * before it was moved to its slot. - */ - ['s-ol']?: RenderNode; - - /** - * Slot host tag name: - * This is the tag name of the element where this node - * has been moved to during slot relocation. - * - * This allows us to check if the node has been moved and prevent - * us from thinking a node _should_ be moved when it may already be in - * its final destination. - * - * This value is set to `undefined` whenever the node is put back into its original location. - */ - ['s-sh']?: string; - - /** - * Is a `slot` node when `shadow: false` (or `scoped: true`). - * - * This is a node (either empty text-node or `` element) - * that represents where a `` is located in the original JSX. - */ - ['s-sr']?: boolean; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the actual `parentNode` of the component - */ - __parentNode?: RenderNode; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the actual `nextSibling` of the component - */ - __nextSibling?: RenderNode; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the actual `previousSibling` of the component - */ - __previousSibling?: RenderNode; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the actual `nextElementSibling` of the component - */ - __nextElementSibling?: RenderNode; - - /** - * On a `scoped: true` component - * with `experimentalSlotFixes` flag enabled, - * returns the actual `nextElementSibling` of the component - */ - __previousElementSibling?: RenderNode; -} - -export type LazyBundlesRuntimeData = LazyBundleRuntimeData[]; - -export type LazyBundleRuntimeData = [ - /** bundleIds */ - string, - ComponentRuntimeMetaCompact[], -]; - -export type ComponentRuntimeMetaCompact = [ - /** flags */ - number, - - /** tagname */ - string, - - /** members */ - { [memberName: string]: ComponentRuntimeMember }?, - - /** listeners */ - ComponentRuntimeHostListener[]?, - - /** watchers */ - ComponentConstructorChangeHandlers?, - - /** serializers */ - ComponentConstructorChangeHandlers?, - - /** deserializers */ - ComponentConstructorChangeHandlers?, -]; - -/** - * Runtime metadata for a Stencil component - */ -export interface ComponentRuntimeMeta { - /** - * This number is used to hold a series of bitflags for various features we - * support on components. The flags which this value is intended to store are - * documented in the `CMP_FLAGS` enum. - */ - $flags$: number; - /** - * Just what it says on the tin - the tag name for the component, as set in - * the `@Component` decorator. - */ - $tagName$: string; - /** - * A map of the component's members, which could include fields decorated - * with `@Prop`, `@State`, etc as well as methods. - */ - $members$?: ComponentRuntimeMembers; - /** - * Information about listeners on the component. - */ - $listeners$?: ComponentRuntimeHostListener[]; - /** - * Tuples containing information about `@Prop` fields on the component which - * are set to be reflected (i.e. kept in sync) as HTML attributes when - * updated. - */ - $attrsToReflect$?: ComponentRuntimeReflectingAttr[]; - /** - * Information about which class members have watchers attached on the component. - */ - $watchers$?: ComponentConstructorChangeHandlers; - /** - * A bundle ID used for lazy loading. - */ - $lazyBundleId$?: string; - /** - * Information about which class members have prop > attribute serializers attached on the component. - */ - $serializers$?: ComponentConstructorChangeHandlers; - /** - * Information about which class members have attribute > prop deserializers attached on the component. - */ - $deserializers$?: ComponentConstructorChangeHandlers; -} - -/** - * A mapping of the names of members on the component to some runtime-specific - * information about them. - */ -export interface ComponentRuntimeMembers { - [memberName: string]: ComponentRuntimeMember; -} - -/** - * A tuple with information about a class member that's relevant at runtime. - * The fields are: - * - * 1. A number used to hold bitflags for component members. The bit flags which - * this is intended to store are documented in the `MEMBER_FLAGS` enum. - * 2. The attribute name to observe. - */ -export type ComponentRuntimeMember = [number, string?]; - -/** - * A tuple holding information about a host listener which is relevant at - * runtime. The field are: - * - * 1. A number used to hold bitflags for listeners. The bit flags which this is - * intended to store are documented in the `LISTENER_FLAGS` enum. - * 2. The event name. - * 3. The method name. - */ -export type ComponentRuntimeHostListener = [number, string, string]; - -/** - * A tuple containing information about props which are "reflected" at runtime, - * meaning that HTML attributes on the component instance are kept in sync with - * the prop value. - * - * The fields are: - * - * 1. the prop name - * 2. the prop attribute. - */ -export type ComponentRuntimeReflectingAttr = [string, string | undefined]; - -/** - * A runtime component reference, consistent of either a host element _or_ an - * empty object. This is used in particular in a few different places as the - * keys in a `WeakMap` which maps {@link HostElement} instances to their - * associated {@link HostRef} instance. - */ -export type RuntimeRef = HostElement | { __stencil__getHostRef?: () => HostRef }; - -/** - * Interface used to track an Element, it's virtual Node (`VNode`), and other data - */ -export interface HostRef { - $ancestorComponent$?: HostElement; - $flags$: number; - $cmpMeta$: ComponentRuntimeMeta; - $hostElement$: HostElement; - $instanceValues$?: Map; - $serializerValues$?: Map; - $lazyInstance$?: ComponentInterface; - /** - * A list of callback functions called immediately after a lazy component module has been fetched. - */ - $fetchedCbList$?: ((elm: HostElement) => void)[]; - /** - * A promise that gets resolved if `BUILD.asyncLoading` is enabled and after the `componentDidLoad` - * and before the `componentDidUpdate` lifecycle events are triggered. - */ - $onReadyPromise$?: Promise; - /** - * A callback which resolves {@link HostRef.$onReadyPromise$} - * @param elm host element - */ - $onReadyResolve$?: (elm: HostElement) => void; - /** - * A promise which resolves with the host component once it has finished rendering - * for the first time. This is primarily used to wait for the first `update` to be - * called on a component. - */ - $onInstancePromise$?: Promise; - /** - * A callback which resolves {@link HostRef.$onInstancePromise$} - * @param elm host element - */ - $onInstanceResolve$?: (elm: HostElement) => void; - /** - * A promise which resolves when the component has finished rendering for the first time. - * It is called after {@link HostRef.$onInstancePromise$} resolves. - */ - $onRenderResolve$?: () => void; - $vnode$?: VNode; - $queuedListeners$?: [string, any][]; - $rmListeners$?: (() => void)[]; - $modeName$?: string; - $renderCount$?: number; - /** - * Defer connectedCallback until after first render for components with slot relocation. - */ - $deferredConnectedCallback$?: boolean; -} - -export interface PlatformRuntime { - /** - * This number is used to hold a series of bitflags for various features we - * support within the runtime. The flags which this value is intended to store are - * documented in the {@link PLATFORM_FLAGS} enum. - */ - $flags$: number; - /** - * Holds a map of nodes to be hydrated. - */ - $orgLocNodes$?: Map; - /** - * Holds the resource url for given platform environment. - */ - $resourcesUrl$: string; - /** - * The nonce value to be applied to all script/style tags at runtime. - * If `null`, the nonce attribute will not be applied. - */ - $nonce$?: string | null; - /** - * A utility function that executes a given function and returns the result. - * @param c The callback function to execute - */ - jmp: (c: Function) => any; - /** - * A wrapper for {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame `requestAnimationFrame`} - */ - raf: (c: FrameRequestCallback) => number; - /** - * A wrapper for {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener `addEventListener`} - */ - ael: ( - el: EventTarget, - eventName: string, - listener: EventListenerOrEventListenerObject, - options: boolean | AddEventListenerOptions, - ) => void; - /** - * A wrapper for {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener `removeEventListener`} - */ - rel: ( - el: EventTarget, - eventName: string, - listener: EventListenerOrEventListenerObject, - options: boolean | AddEventListenerOptions, - ) => void; - /** - * A wrapper for creating a {@link https://developer.mozilla.org/docs/Web/API/CustomEvent `CustomEvent`} - */ - ce: (eventName: string, opts?: any) => CustomEvent; -} - -export type StyleMap = Map; - -export type RootAppliedStyleMap = WeakMap>; - -export interface ScreenshotConnector { - initBuild(opts: ScreenshotConnectorOptions): Promise; - completeBuild(masterBuild: ScreenshotBuild): Promise; - getMasterBuild(): Promise; - pullMasterBuild(): Promise; - publishBuild(buildResults: ScreenshotBuildResults): Promise; - getScreenshotCache(): Promise; - updateScreenshotCache( - screenshotCache: ScreenshotCache, - buildResults: ScreenshotBuildResults, - ): Promise; - generateJsonpDataUris(build: ScreenshotBuild): Promise; - sortScreenshots(screenshots: Screenshot[]): Screenshot[]; - toJson(masterBuild: ScreenshotBuild, screenshotCache: ScreenshotCache): string; -} - -export interface ScreenshotBuildResults { - appNamespace: string; - masterBuild: ScreenshotBuild; - currentBuild: ScreenshotBuild; - compare: ScreenshotCompareResults; -} - -export interface ScreenshotCompareResults { - id: string; - a: { - id: string; - message: string; - author: string; - url: string; - previewUrl: string; - }; - b: { - id: string; - message: string; - author: string; - url: string; - previewUrl: string; - }; - timestamp: number; - url: string; - appNamespace: string; - diffs: ScreenshotDiff[]; -} - -export interface ScreenshotConnectorOptions { - buildId: string; - buildMessage: string; - buildAuthor?: string; - buildUrl?: string; - previewUrl?: string; - appNamespace: string; - buildTimestamp: number; - logger: Logger; - rootDir: string; - cacheDir: string; - packageDir: string; - screenshotDirName?: string; - imagesDirName?: string; - buildsDirName?: string; - currentBuildDir?: string; - updateMaster?: boolean; - allowableMismatchedPixels?: number; - allowableMismatchedRatio?: number; - pixelmatchThreshold?: number; - waitBeforeScreenshot?: number; - pixelmatchModulePath?: string; -} - -export interface ScreenshotBuildData { - buildId: string; - rootDir: string; - screenshotDir: string; - imagesDir: string; - buildsDir: string; - currentBuildDir: string; - updateMaster: boolean; - allowableMismatchedPixels: number; - allowableMismatchedRatio: number; - pixelmatchThreshold: number; - masterScreenshots: { [screenshotId: string]: string }; - cache: { [cacheKey: string]: number }; - timeoutBeforeScreenshot: number; - pixelmatchModulePath: string; -} - -export interface PixelMatchInput { - imageAPath: string; - imageBPath: string; - width: number; - height: number; - pixelmatchThreshold: number; -} - -export interface ScreenshotBuild { - id: string; - message: string; - author?: string; - url?: string; - previewUrl?: string; - appNamespace: string; - timestamp: number; - screenshots: Screenshot[]; -} - -export interface ScreenshotCache { - timestamp?: number; - lastBuildId?: string; - size?: number; - items?: { - /** - * Cache key - */ - key: string; - - /** - * Timestamp used to remove the oldest data - */ - ts: number; - - /** - * Mismatched pixels - */ - mp: number; - }[]; -} - -export interface Screenshot { - id: string; - desc?: string; - image: string; - device?: string; - userAgent?: string; - width: number; - height: number; - deviceScaleFactor?: number; - hasTouch?: boolean; - isLandscape?: boolean; - isMobile?: boolean; - testPath?: string; - diff?: ScreenshotDiff; -} - -export interface ScreenshotDiff { - mismatchedPixels: number; - id: string; - desc?: string; - imageA?: string; - imageB?: string; - device?: string; - userAgent?: string; - width: number; - height: number; - deviceScaleFactor?: number; - hasTouch?: boolean; - isLandscape?: boolean; - isMobile?: boolean; - allowableMismatchedPixels: number; - allowableMismatchedRatio: number; - testPath?: string; - cacheKey?: string; -} - -export interface ScreenshotOptions { - /** - * When true, takes a screenshot of the full scrollable page. - * Default: `false` - */ - fullPage?: boolean; - - /** - * An object which specifies clipping region of the page. - */ - clip?: ScreenshotBoundingBox; - - /** - * Hides default white background and allows capturing screenshots with transparency. - * Default: `false` - */ - omitBackground?: boolean; - - /** - * Matching threshold, ranges from `0` to 1. Smaller values make the comparison - * more sensitive. Defaults to the testing config `pixelmatchThreshold` value; - */ - pixelmatchThreshold?: number; - /** - * Capture the screenshot beyond the viewport. - * - * @defaultValue `false` if there is no `clip`. `true` otherwise. - */ - captureBeyondViewport?: boolean; -} - -export interface ScreenshotBoundingBox { - /** - * The x-coordinate of top-left corner. - */ - x: number; - - /** - * The y-coordinate of top-left corner. - */ - y: number; - - /** - * The width in pixels. - */ - width: number; - - /** - * The height in pixels. - */ - height: number; -} - -export interface StyleCompiler { - modeName: string; - styleId: string; - styleStr: string; - styleIdentifier: string; - externalStyles: ExternalStyleCompiler[]; -} - -export interface ExternalStyleCompiler { - absolutePath: string; - relativePath: string; - originalComponentPath: string; -} - -export interface CompilerModeStyles { - [modeName: string]: string[]; -} - -export interface CssImportData { - srcImport: string; - updatedImport?: string; - url: string; - filePath: string; - altFilePath?: string; - styleText?: string | null; - modifiers?: string; -} - -export interface CssToEsmImportData { - srcImportText: string; - varName: string; - url: string; - filePath: string; - /** - * True if this is a node module import (e.g. using ~ prefix like ~foo/style.css) - * These should be treated as bare module specifiers and not have ./ prepended - */ - isNodeModule?: boolean; -} - -/** - * Input CSS to be transformed into ESM - */ -export interface TransformCssToEsmInput { - input: string; - module?: 'cjs' | 'esm' | string; - file?: string; - tag?: string; - tags?: string[]; - addTagTransformers: boolean; - encapsulation?: string; - /** - * The mode under which the CSS will be applied. - * - * Corresponds to a key used when `@Component`'s `styleUrls` field is an object: - * ```ts - * @Component({ - * tag: 'todo-list', - * styleUrls: { - * ios: 'todo-list.ios.scss', - * md: 'todo-list.md.scss', - * } - * }) - * ``` - * In the example above, two `TransformCssToEsmInput`s should be created, one for 'ios' and one for 'md' (this field - * is not shared by multiple fields, nor is it a composite of multiple modes). - */ - mode?: string; - sourceMap?: boolean; - minify?: boolean; - docs?: boolean; - autoprefixer?: any; - styleImportData?: string; -} - -export interface TransformCssToEsmOutput { - styleText: string; - output: string; - map: any; - diagnostics: Diagnostic[]; - defaultVarName: string; - styleDocs: StyleDoc[]; - imports: { varName: string; importPath: string }[]; -} - -export interface PackageJsonData { - name?: string; - version?: string; - main?: string; - exports?: { [key: string]: string | { [key: string]: string } }; - description?: string; - bin?: { [key: string]: string }; - browser?: string; - module?: string; - 'jsnext:main'?: string; - 'collection:main'?: string; - unpkg?: string; - collection?: string; - types?: string; - files?: string[]; - ['dist-tags']?: { - latest: string; - }; - dependencies?: { - [moduleId: string]: string; - }; - devDependencies?: { - [moduleId: string]: string; - }; - repository?: { - type?: string; - url?: string; - }; - private?: boolean; - scripts?: { - [runName: string]: string; - }; - license?: string; - keywords?: string[]; -} - -export interface Workbox { - generateSW(swConfig: any): Promise; - generateFileManifest(): Promise; - getFileManifestEntries(): Promise; - injectManifest(swConfig: any): Promise; - copyWorkboxLibraries(wwwDir: string): Promise; -} - -declare global { - namespace jest { - /* eslint-disable-next-line @typescript-eslint/no-unused-vars -- - * these type params need to be here for compatibility with Jest, but we aren't using them for anything - */ - interface Matchers { - /** - * Compares HTML, but first normalizes the HTML so all - * whitespace, attribute order and css class order are - * the same. When given an element, it will compare - * the element's `outerHTML`. When given a Document Fragment, - * such as a Shadow Root, it'll compare its `innerHTML`. - * Otherwise it'll compare two strings representing HTML. - */ - toEqualHtml(expectHtml: string): void; - - /** - * Compares HTML light DOM only, but first normalizes the HTML so all - * whitespace, attribute order and css class order are - * the same. When given an element, it will compare - * the element's `outerHTML`. When given a Document Fragment, - * such as a Shadow Root, it'll compare its `innerHTML`. - * Otherwise it'll compare two strings representing HTML. - */ - toEqualLightHtml(expectLightHtml: string): void; - - /** - * When given an element, it'll compare the element's - * `textContent`. Otherwise it'll compare two strings. This - * matcher will also `trim()` each string before comparing. - */ - toEqualText(expectTextContent: string): void; - - /** - * Checks if an element simply has the attribute. It does - * not check any values of the attribute - */ - toHaveAttribute(expectAttrName: string): void; - - /** - * Checks if an element's attribute value equals the expect value. - */ - toEqualAttribute(expectAttrName: string, expectAttrValue: any): void; - - /** - * Checks if an element's has each of the expected attribute - * names and values. - */ - toEqualAttributes(expectAttrs: { [attrName: string]: any }): void; - - /** - * Checks if an element has the expected css class. - */ - toHaveClass(expectClassName: string): void; - - /** - * Checks if an element has each of the expected css classes - * in the array. - */ - toHaveClasses(expectClassNames: string[]): void; - - /** - * Checks if an element has the exact same css classes - * as the expected array of css classes. - */ - toMatchClasses(expectClassNames: string[]): void; - - /** - * When given an EventSpy, checks if the event has been - * received or not. - */ - toHaveReceivedEvent(): void; - - /** - * When given an EventSpy, checks how many times the - * event has been received. - */ - toHaveReceivedEventTimes(count: number): void; - - /** - * When given an EventSpy, checks the event has - * received the correct custom event `detail` data. - */ - toHaveReceivedEventDetail(eventDetail: any): void; - - /** - * When given an EventSpy, checks the first event has - * received the correct custom event `detail` data. - */ - toHaveFirstReceivedEventDetail(eventDetail: any): void; - - /** - * When given an EventSpy, checks the last event has - * received the correct custom event `detail` data. - */ - toHaveLastReceivedEventDetail(eventDetail: any): void; - - /** - * When given an EventSpy, checks the event at an index - * has received the correct custom event `detail` data. - */ - toHaveNthReceivedEventDetail(index: number, eventDetail: any): void; - - /** - * Used to evaluate the results of `compareScreenshot()`, such as - * `expect(compare).toMatchScreenshot()`. The `allowableMismatchedRatio` - * value from the testing config is used by default if - * `MatchScreenshotOptions` were not provided. - */ - toMatchScreenshot(opts?: MatchScreenshotOptions): void; - } - } -} - -export interface MatchScreenshotOptions { - /** - * The `allowableMismatchedPixels` value is the total number of pixels - * that can be mismatched until the test fails. For example, if the value - * is `100`, and if there were `101` pixels that were mismatched then the - * test would fail. If the `allowableMismatchedRatio` is provided it will - * take precedence, otherwise `allowableMismatchedPixels` will be used. - */ - allowableMismatchedPixels?: number; - - /** - * The `allowableMismatchedRatio` ranges from `0` to `1` and is used to - * determine an acceptable ratio of pixels that can be mismatched before - * the image is considered to have changes. Realistically, two screenshots - * representing the same content may have a small number of pixels that - * are not identical due to anti-aliasing, which is perfectly normal. The - * `allowableMismatchedRatio` is the number of pixels that were mismatched, - * divided by the total number of pixels in the screenshot. For example, - * a ratio value of `0.06` means 6% of the pixels can be mismatched before - * the image is considered to have changes. If the `allowableMismatchedRatio` - * is provided it will take precedence, otherwise `allowableMismatchedPixels` - * will be used. - */ - allowableMismatchedRatio?: number; -} - -export interface EventSpy { - events: SerializedEvent[]; - eventName: string; - firstEvent: SerializedEvent; - lastEvent: SerializedEvent; - length: number; - next(): Promise<{ - done: boolean; - value: SerializedEvent; - }>; -} - -export interface SerializedEvent { - bubbles: boolean; - cancelBubble: boolean; - cancelable: boolean; - composed: boolean; - currentTarget: any; - defaultPrevented: boolean; - detail: any; - eventPhase: any; - isTrusted: boolean; - returnValue: any; - srcElement: any; - target: any; - timeStamp: number; - type: string; - isSerializedEvent: boolean; -} - -export interface EventInitDict { - bubbles?: boolean; - cancelable?: boolean; - composed?: boolean; - detail?: any; -} - -export interface JestEnvironmentGlobal { - __NEW_TEST_PAGE__: () => Promise; - __CLOSE_OPEN_PAGES__: () => Promise; - loadTestWindow: (testWindow: any) => Promise; - h: any; - resourcesUrl: string; - currentSpec?: { - id?: string; - description: string; - fullName: string; - testPath: string | null; - }; - env: { [prop: string]: string }; - screenshotDescriptions: Set; -} - -export interface E2EProcessEnv { - STENCIL_COMMIT_ID?: string; - STENCIL_COMMIT_MESSAGE?: string; - STENCIL_REPO_URL?: string; - STENCIL_SCREENSHOT_CONNECTOR?: string; - STENCIL_SCREENSHOT_SERVER?: string; - - __STENCIL_EMULATE_CONFIGS__?: string; - __STENCIL_ENV__?: string; - __STENCIL_EMULATE__?: string; - __STENCIL_BROWSER_URL__?: string; - __STENCIL_APP_SCRIPT_URL__?: string; - __STENCIL_APP_STYLE_URL__?: string; - __STENCIL_BROWSER_WS_ENDPOINT__?: string; - __STENCIL_BROWSER_WAIT_UNTIL?: string; - - __STENCIL_SCREENSHOT__?: 'true'; - __STENCIL_SCREENSHOT_BUILD__?: string; - __STENCIL_SCREENSHOT_TIMEOUT_MS__?: string; - - __STENCIL_E2E_TESTS__?: 'true'; - __STENCIL_E2E_DEVTOOLS__?: 'true'; - __STENCIL_SPEC_TESTS__?: 'true'; - - __STENCIL_PUPPETEER_MODULE__?: string; - __STENCIL_PUPPETEER_VERSION__?: number; - __STENCIL_DEFAULT_TIMEOUT__?: string; - - /** - * Property for injecting transformAliasedImportPaths into the Jest context - */ - __STENCIL_TRANSPILE_PATHS__?: 'true' | 'false'; -} - -export interface AnyHTMLElement extends HTMLElement { - [key: string]: any; -} - -export interface SpecPage { - /** - * Mocked testing `document.body`. - */ - body: HTMLBodyElement; - /** - * Mocked testing `document`. - */ - doc: HTMLDocument; - /** - * The first component found within the mocked `document.body`. If a component isn't found, then it'll return `document.body.firstElementChild`. - */ - root?: AnyHTMLElement; - /** - * Similar to `root`, except returns the component instance. If a root component was not found it'll return `null`. - */ - rootInstance?: any; - /** - * Convenience function to set `document.body.innerHTML` and `waitForChanges()`. Function argument should be a HTML string. - */ - setContent: (html: string) => Promise; - /** - * After changes have been made to a component, such as a update to a property or attribute, the test page does not automatically apply the changes. In order to wait for, and apply the update, call `await page.waitForChanges()`. - */ - waitForChanges: () => Promise; - /** - * Mocked testing `window`. - */ - win: Window; - - build: BuildConditionals; - flushLoadModule: (bundleId?: string) => Promise; - flushQueue: () => Promise; - styles: Map; -} - -/** - * Options pertaining to the creation and functionality of a {@link SpecPage} - */ -export interface NewSpecPageOptions { - /** - * An array of components to test. Component classes can be imported into the spec file, then their reference should be added to the `component` array in order to be used throughout the test. - */ - components: any[]; - /** - * Sets the mocked `document.cookie`. - */ - cookie?: string; - /** - * Sets the mocked `dir` attribute on ``. - */ - direction?: string; - /** - * If `false`, do not flush the render queue on initial test setup. - */ - flushQueue?: boolean; - /** - * The initial HTML used to generate the test. This can be useful to construct a collection of components working together, and assign HTML attributes. This value sets the mocked `document.body.innerHTML`. - */ - html?: string; - /** - * The initial JSX used to generate the test. - * Use `template` when you want to initialize a component using their properties, instead of their HTML attributes. - * It will render the specified template (JSX) into `document.body`. - */ - template?: () => any; - /** - * Sets the mocked `lang` attribute on ``. - */ - language?: string; - /** - * Useful for debugging hydrating components client-side. Sets that the `html` option already includes annotated prerender attributes and comments. - */ - hydrateClientSide?: boolean; - /** - * Useful for debugging hydrating components server-side. The output HTML will also include prerender annotations. - */ - hydrateServerSide?: boolean; - /** - * Sets the mocked `document.referrer`. - */ - referrer?: string; - /** - * Manually set if the mocked document supports Shadow DOM or not. Default is `true`. - */ - supportsShadowDom?: boolean; - /** - * When a component is pre-rendered it includes HTML annotations, such as `s-id` attributes and `` comments. This information is used by client-side hydrating. Default is `false`. - */ - includeAnnotations?: boolean; - /** - * Sets the mocked browser's `location.href`. - */ - url?: string; - /** - * Sets the mocked browser's `navigator.userAgent`. - */ - userAgent?: string; - /** - * By default, any changes to component properties and attributes must `page.waitForChanges()` in order to test the updates. As an option, `autoApplyChanges` continuously flushes the queue on the background. Default is `false`. - */ - autoApplyChanges?: boolean; - /** - * By default, styles are not attached to the DOM and they are not reflected in the serialized HTML. - * Setting this option to `true` will include the component's styles in the serializable output. - */ - attachStyles?: boolean; - /** - * Set {@link BuildConditionals} for testing based off the metadata of the component under test. - * When `true` all `BuildConditionals` will be assigned to the global testing `BUILD` object, regardless of their - * value. When `false`, only `BuildConditionals` with a value of `true` will be assigned to the `BUILD` object. - */ - strictBuild?: boolean; - /** - * Default values to be set on the platform runtime object {@see PlatformRuntime} when creating - * the spec page. - */ - platform?: Partial; -} - -/** - * A record of `TypesMemberNameData` entities. - * - * Each key in this record is intended to be the path to a file that declares one or more types used by a component. - * However, this is not enforced by the type system - users of this interface should not make any assumptions regarding - * the format of the path used as a key (relative vs. absolute) - */ -export interface TypesImportData { - [key: string]: TypesMemberNameData[]; -} - -/** - * A type describing how Stencil may alias an imported type to avoid naming collisions when performing operations such - * as generating `components.d.ts` files. - */ -export interface TypesMemberNameData { - /** - * The original name of the import before any aliasing was applied. - * - * i.e. if a component imports a type as follows: - * `import { MyType as MyCoolType } from './my-type';` - * - * the `originalName` would be 'MyType'. If the import is not aliased, then `originalName` and `localName` will be the same. - */ - originalName: string; - /** - * The name of the type as it's used within a file. - */ - localName: string; - /** - * An alias that Stencil may apply to the `localName` to avoid naming collisions. This name does not appear in the - * file that is using `localName`. - */ - importName?: string; - /** - * Whether this is a default import/export (e.g., `import MyEnum from './my-enum'`) - * vs a named import/export (e.g., `import { MyType } from './my-type'`) - */ - isDefault?: boolean; -} - -export interface TypesModule { - isDep: boolean; - tagName: string; - tagNameAsPascal: string; - htmlElementName: string; - component: string; - jsx: string; - element: string; - explicitAttributes: string | null; - explicitProperties: string | null; - requiredProps: Array<{ - name: string; - type: string; - complexType?: ComponentCompilerProperty['complexType']; - }> | null; -} - -export type TypeInfo = { - name: string; - type: string; - optional: boolean; - required: boolean; - internal: boolean; - jsdoc?: string; -}[]; - -export type ChildType = VNode | number | string; - -export type PropsType = VNodeProdData | number | string | null; - -export interface VNodeProdData { - key?: string | number; - class?: { [className: string]: boolean } | string; - className?: { [className: string]: boolean } | string; - style?: any; - [key: string]: any; -} - -/** - * An abstraction to bundle up four methods which _may_ be handled by - * dispatching work to workers running in other OS threads or may be called - * synchronously. Environment and `CompilerSystem` related setup code will - * determine which one, but in either case the call sites for these methods can - * dispatch to this shared interface. - */ -export interface CompilerWorkerContext { - optimizeCss(inputOpts: OptimizeCssInput): Promise; - prepareModule( - input: string, - minifyOpts: any, - transpile: boolean, - inlineHelpers: boolean, - ): Promise<{ output: string; diagnostics: Diagnostic[]; sourceMap?: SourceMap }>; - prerenderWorker(prerenderRequest: PrerenderUrlRequest): Promise; - transformCssToEsm(input: TransformCssToEsmInput): Promise; -} - -/** - * The methods that are supported on a {@link CompilerWorkerContext} - */ -export type WorkerContextMethod = keyof CompilerWorkerContext; - -/** - * A little type guard which will cause a type error if the parameter `T` does - * not satisfy {@link CPSerializable} (i.e. if it's not possible to cleanly - * serialize it for message passing via an IPC channel). - */ -type IPCSerializable = T; - -/** - * A manifest for a job that a worker thread should carry out, as determined by - * and dispatched from the main thread. This includes the name of the task to do - * and any arguments necessary to carry it out properly. - * - * This message must satisfy {@link CPSerializable} so it can be sent from the - * main thread to a worker thread via an IPC channel - */ -export type MsgToWorker = IPCSerializable<{ - stencilId: number; - method: T; - args: Parameters; -}>; - -/** - * A manifest for a job that a worker thread should carry out, as determined by - * and dispatched from the main thread. This includes the name of the task to do - * and any arguments necessary to carry it out properly. - * - * This message must satisfy {@link CPSerializable} so it can be sent from the - * main thread to a worker thread via an IPC channel - */ -export type MsgFromWorker = IPCSerializable<{ - stencilId?: number; - stencilRtnValue: ReturnType; - stencilRtnError: string | null; -}>; - -/** - * A description of a task which should be passed to a worker in another - * thread. This interface differs from {@link MsgToWorker} in that it doesn't - * have to be serializable for transmission through an IPC channel, so we can - * hold things like a `resolve` and `reject` callback to use when the task - * completes. - */ -export interface CompilerWorkerTask { - stencilId: number; - inputArgs: any[]; - resolve: (val: any) => any; - reject: (msg: string) => any; - retries: number; -} - -/** - * A handler for IPC messages from the main thread to a worker thread. This - * involves dispatching an action specified by a {@link MsgToWorker} object to a - * {@link CompilerWorkerContext}. - * - * @param msgToWorker the message to handle - * @returns the return value of the specified function - */ -export type WorkerMsgHandler = ( - msgToWorker: MsgToWorker, -) => ReturnType; - -export interface TranspileModuleResults { - sourceFilePath: string; - code: string; - map: any; - diagnostics: Diagnostic[]; - moduleFile: Module; -} - -export interface ValidateTypesResults { - diagnostics: Diagnostic[]; - dirPaths: string[]; - filePaths: string[]; -} - -export interface TerminalInfo { - /** - * Whether this is in CI or not. - */ - readonly ci: boolean; - /** - * Whether the terminal is an interactive TTY or not. - */ - readonly tty: boolean; -} - -/** - * The task to run in order to collect the duration data point. - */ -export type TelemetryCallback = (...args: any[]) => void | Promise; - -/** - * The model for the data that's tracked. - */ -export interface TrackableData { - arguments: string[]; - build: string; - component_count?: number; - config: Config; - cpu_model: string | undefined; - duration_ms: number | undefined; - has_app_pwa_config: boolean; - os_name: string | undefined; - os_version: string | undefined; - packages: string[]; - packages_no_versions?: string[]; - rollup: string; - stencil: string; - system: string; - system_major?: string; - targets: string[]; - task: TaskCommand | null; - typescript: string; - yarn: boolean; -} - -/** - * Used as the object sent to the server. Value is the data tracked. - */ -export interface Metric { - name: string; - timestamp: string; - source: 'stencil_cli'; - value: TrackableData; - session_id: string; -} -export interface TelemetryConfig { - 'telemetry.stencil'?: boolean; - 'tokens.telemetry'?: string; -} diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts deleted file mode 100644 index 67180a0baae..00000000000 --- a/src/declarations/stencil-public-compiler.ts +++ /dev/null @@ -1,3249 +0,0 @@ -import type { ConfigFlags } from '../cli/config-flags'; -import type { PrerenderUrlResults, PrintLine } from '../internal'; -import type { BuildCtx, CompilerCtx } from './stencil-private'; -import type { JsonDocs } from './stencil-public-docs'; -import type { ResolutionHandler } from './stencil-public-runtime'; - -export * from './stencil-public-docs'; - -/** - * https://stenciljs.com/docs/config/ - */ -export interface StencilConfig { - /** - * By default, Stencil will attempt to optimize small scripts by inlining them in HTML. Setting - * this flag to `false` will prevent this optimization and keep all scripts separate from HTML. - */ - allowInlineScripts?: boolean; - /** - * By setting `autoprefixCss` to `true`, Stencil will use the appropriate config to automatically - * prefix css. For example, developers can write modern and standard css properties, such as - * "transform", and Stencil will automatically add in the prefixed version, such as "-webkit-transform". - * As of Stencil v2, autoprefixing CSS is no longer the default. - * Defaults to `false` - */ - autoprefixCss?: boolean | any; - - /** - * By default, Stencil will statically analyze the application and generate a component graph of - * how all the components are interconnected. - * - * From the component graph it is able to best decide how components should be grouped - * depending on their usage with one another within the app. - * By doing so it's able to bundle components together in order to reduce network requests. - * However, bundles can be manually generated using the bundles config. - * - * The bundles config is an array of objects that represent how components are grouped together - * in lazy-loaded bundles. - * This config is rarely needed as Stencil handles this automatically behind the scenes. - */ - bundles?: ConfigBundle[]; - - /** - * Stencil will cache build results in order to speed up rebuilds. - * To disable this feature, set enableCache to false. - */ - enableCache?: boolean; - /** - * The directory where sub-directories will be created for caching when `enableCache` is set - * `true` or if using Stencil's Screenshot Connector. - * - * @default '.stencil' - * - * @example - * - * A Stencil config like the following: - * ```ts - * export const config = { - * ..., - * enableCache: true, - * cacheDir: '.cache', - * testing: { - * screenshotConnector: 'connector.js' - * } - * } - * ``` - * - * Will result in the following file structure: - * ```tree - * stencil-project-root - * └── .cache - * ├── .build <-- Where build related file caching is written - * | - * └── screenshot-cache.json <-- Where screenshot caching is written - * ``` - */ - cacheDir?: string; - - /** - * Stencil is traditionally used to compile many components into an app, - * and each component comes with its own compartmentalized styles. - * However, it's still common to have styles which should be "global" across all components and the website. - * A global CSS file is often useful to set CSS Variables. - * - * Additionally, the globalStyle config can be used to precompile styles with Sass, PostCSS, etc. - * Below is an example folder structure containing a webapp's global sass file, named app.css. - */ - globalStyle?: string; - - /** - * Will generate {@link https://nodejs.org/api/packages.html#packages_exports export map} entry points - * for each component in the build when `true`. - * - * @default false - */ - generateExportMaps?: boolean; - - /** - * When the hashFileNames config is set to true, and it is a production build, - * the hashedFileNameLength config is used to determine how many characters the file name's hash should be. - */ - hashedFileNameLength?: number; - - /** - * During production builds, the content of each generated file is hashed to represent the content, - * and the hashed value is used as the filename. If the content isn't updated between builds, - * then it receives the same filename. When the content is updated, then the filename is different. - * - * By doing this, deployed apps can "forever-cache" the build directory and take full advantage of - * content delivery networks (CDNs) and heavily caching files for faster apps. - */ - hashFileNames?: boolean; - - /** - * The namespace config is a string representing a namespace for the app. - * For apps that are not meant to be a library of reusable components, - * the default of App is just fine. However, if the app is meant to be consumed - * as a third-party library, such as Ionic, a unique namespace is required. - */ - namespace?: string; - - /** - * Stencil is able to take an app's source and compile it to numerous targets, - * such as an app to be deployed on an http server, or as a third-party library - * to be distributed on npm. By default, Stencil apps have an output target type of www. - * - * The outputTargets config is an array of objects, with types of www and dist. - */ - outputTargets?: OutputTarget[]; - - /** - * The plugins config can be used to add your own rollup plugins. - * By default, Stencil does not come with Sass or PostCSS support. - * However, either can be added using the plugin array. - */ - plugins?: any[]; - - /** - * Generate js source map files for all bundles. - * Set to `true` to always generate source maps, `false` to never generate source maps. - * Set to `'dev'` to only generate source maps when the `--dev` flag is passed. - * Defaults to `'dev'`. - */ - sourceMap?: boolean | 'dev'; - - /** - * The srcDir config specifies the directory which should contain the source typescript files - * for each component. The standard for Stencil apps is to use src, which is the default. - */ - srcDir?: string; - - /** - * Sets whether or not Stencil should transform path aliases set in a project's - * `tsconfig.json` from the assigned module aliases to resolved relative paths. - * - * This behavior defaults to `true`, but may be opted-out of by setting this flag to `false`. - */ - transformAliasedImportPaths?: boolean; - /** - * When `true`, Stencil will suppress diagnostics which warn about public members using reserved names - * (for example, decorating a method named `focus` with `@Method()`). Defaults to `false`. - */ - suppressReservedPublicNameWarnings?: boolean; - /** - * When `true`, we will validate a project's `package.json` based on the output target the user has designated - * as `isPrimaryPackageOutputTarget: true` in their Stencil config. - */ - validatePrimaryPackageOutputTarget?: boolean; - - /** - * Passes custom configuration down to the "@rollup/plugin-commonjs" that Stencil uses under the hood. - * For further information: https://stenciljs.com/docs/module-bundling - */ - commonjs?: BundlingConfig; - - /** - * Passes custom configuration down to the "@rollup/plugin-node-resolve" that Stencil uses under the hood. - * For further information: https://stenciljs.com/docs/module-bundling - */ - nodeResolve?: NodeResolveConfig; - - /** - * Passes custom configuration down to rollup itself, not all rollup options can be overridden. - */ - rollupConfig?: RollupConfig; - - /** - * Sets if the ES5 build should be generated or not. Stencil generates a modern build without ES5, - * whereas this setting to `true` will also create es5 builds for both dev and prod modes. Setting - * `buildEs5` to `prod` will only build ES5 in prod mode. Basically if the app does not need to run - * on legacy browsers (IE11 and Edge 18 and below), it's safe to not build ES5, which will also speed - * up build times. Defaults to `false`. - */ - buildEs5?: boolean | 'prod'; - - /** - * Sets if the JS browser files are minified or not. Stencil uses `terser` under the hood. - * Defaults to `false` in dev mode and `true` in production mode. - */ - minifyJs?: boolean; - - /** - * Sets if the CSS is minified or not. - * Defaults to `false` in dev mode and `true` in production mode. - */ - minifyCss?: boolean; - - /** - * Forces Stencil to run in `dev` mode if the value is `true` and `production` mode - * if it's `false`. - * - * Defaults to `false` (ie. production) unless the `--dev` flag is used in the CLI. - */ - devMode?: boolean; - - /** - * Object to provide a custom logger. By default a `logger` is already provided for the - * platform the compiler is running on, such as NodeJS or a browser. - */ - logger?: Logger; - - /** - * Config to add extra runtime for DOM features that require more polyfills. Note - * that not all DOM APIs are fully polyfilled when using the slot polyfill. These - * are opt-in since not all users will require the additional runtime. - */ - extras?: ConfigExtras; - - /** - * The hydrated flag identifies if a component and all of its child components - * have finished hydrating. This helps prevent any flash of unstyled content (FOUC) - * as various components are asynchronously downloaded and rendered. By default it - * will add the `hydrated` CSS class to the element. The `hydratedFlag` config can be used - * to change the name of the CSS class, change it to an attribute, or change which - * type of CSS properties and values are assigned before and after hydrating. This config - * can also be used to not include the hydrated flag at all by setting it to `null`. - */ - hydratedFlag?: HydratedFlag | null; - - /** - * Ionic prefers to hide all components prior to hydration with a style tag appended - * to the head of the document containing some `visibility: hidden;` css rules. - * - * Disabling this will remove the style tag that sets `visibility: hidden;` on all - * un-hydrated web components. This more closely follows the HTML spec, and allows - * you to set your own fallback content. - * - */ - invisiblePrehydration?: boolean; - - /** - * Sets the task queue used by stencil's runtime. The task queue schedules DOM read and writes - * across the frames to efficiently render and reduce layout thrashing. By default, - * `async` is used. It's recommended to also try each setting to decide which works - * best for your use-case. In all cases, if your app has many CPU intensive tasks causing the - * main thread to periodically lock-up, it's always recommended to try - * [Web Workers](https://stenciljs.com/docs/web-workers) for those tasks. - * - * - `async`: DOM read and writes are scheduled in the next frame to prevent layout thrashing. - * During intensive CPU tasks it will not reschedule rendering to happen in the next frame. - * `async` is ideal for most apps, and if the app has many intensive tasks causing the main - * thread to lock-up, it's recommended to try [Web Workers](https://stenciljs.com/docs/web-workers) - * rather than the congestion async queue. - * - * - `congestionAsync`: DOM reads and writes are scheduled in the next frame to prevent layout - * thrashing. When the app is heavily tasked and the queue becomes congested it will then - * split the work across multiple frames to prevent blocking the main thread. However, it can - * also introduce unnecessary reflows in some cases, especially during startup. `congestionAsync` - * is ideal for apps running animations while also simultaneously executing intensive tasks - * which may lock-up the main thread. - * - * - `immediate`: Makes writeTask() and readTask() callbacks to be executed synchronously. Tasks - * are not scheduled to run in the next frame, but do note there is at least one microtask. - * The `immediate` setting is ideal for apps that do not provide long running and smooth - * animations. Like the async setting, if the app has intensive tasks causing the main thread - * to lock-up, it's recommended to try [Web Workers](https://stenciljs.com/docs/web-workers). - */ - taskQueue?: 'async' | 'immediate' | 'congestionAsync'; - - /** - * Provide a object of key/values accessible within the app, using the `Env` object. - */ - env?: { [prop: string]: string | undefined }; - - docs?: StencilDocsConfig; - - globalScript?: string; - srcIndexHtml?: string; - /** - * Configuration for Stencil's integrated testing (Jest + Puppeteer). - * - * @deprecated Integrated testing support will be removed in Stencil v5. Migrate spec tests to - * [`@stencil/vitest`](https://github.com/stenciljs/vitest) and e2e / browser tests to either - * [`@stencil/vitest`](https://github.com/stenciljs/vitest) or - * [`@stencil/playwright`](https://github.com/stenciljs/playwright). - * See https://github.com/stenciljs/core/issues/6584 for full discussion and migration guidance. - */ - testing?: TestingConfig; - maxConcurrentWorkers?: number; - preamble?: string; - rollupPlugins?: { before?: any[]; after?: any[] }; - - entryComponentsHint?: string[]; - /** - * Sets whether Stencil will write files to `dist/` during the build or not. - * - * By default this value is set to the opposite value of {@link devMode}, - * i.e. it will be `true` when building for production and `false` when - * building for development. - */ - buildDist?: boolean; - buildLogFilePath?: string; - devInspector?: boolean; - devServer?: StencilDevServerConfig; - sys?: CompilerSystem; - tsconfig?: string; - validateTypes?: boolean; - - /** - * Sets whether Stencil will watch for changes in the source files and rebuild the project automatically. - * @default true - */ - watch?: boolean; - /** - * External directories to watch for changes. By default, Stencil will watch the root and {@link StencilConfig.srcDir} - * directory for changes. If you want to watch additional directories, including e.g. `node_modules`, you can add them here. - * @default [] - */ - watchExternalDirs?: string[]; - /** - * An array of RegExp patterns that are matched against all source files before adding - * to the watch list in watch mode. If the file path matches any of the patterns, when it - * is updated, it will not trigger a re-run of tests. - */ - watchIgnoredRegex?: RegExp | RegExp[]; - - /** - * An array of component tag names to exclude from production builds. - * Useful to remove test, demo or experimental components from final output. - * - * **Note:** Exclusion only applies to production builds (default, or when `--prod` is used). - * Development builds (with `--dev` flag) will include all components to support local testing. - * - * Supports glob patterns for matching multiple components: - * - `['demo-*']` - Excludes all components starting with "demo-" - * - `['*-test', '*-demo']` - Excludes components ending with "-test" or "-demo" - * - `['my-component']` - Excludes a specific component - * - * Components matching these patterns will be completely excluded from all output targets. - * - * @example - * ```ts - * export const config: Config = { - * excludeComponents: ['demo-*', 'test-component', '*-internal'], - * }; - * ``` - * - * @default [] - */ - excludeComponents?: string[]; - - /** - * Set whether unused dependencies should be excluded from the built output. - */ - excludeUnusedDependencies?: boolean; - stencilCoreResolvedId?: string; -} - -interface ConfigExtrasBase { - /** - * Experimental flag. Projects that use a Stencil library built using the `dist` output target may have trouble lazily - * loading components when using a bundler such as Vite or Parcel. Setting this flag to `true` will change how Stencil - * lazily loads components in a way that works with additional bundlers. Setting this flag to `true` will increase - * the size of the compiled output. Defaults to `false`. - * @deprecated This flag has been deprecated in favor of `enableImportInjection`, which provides the same - * functionality. `experimentalImportInjection` will be removed in a future major version of Stencil. - */ - experimentalImportInjection?: boolean; - - /** - * Projects that use a Stencil library built using the `dist` output target may have trouble lazily - * loading components when using a bundler such as Vite or Parcel. Setting this flag to `true` will change how Stencil - * lazily loads components in a way that works with additional bundlers. Setting this flag to `true` will increase - * the size of the compiled output. Defaults to `false`. - */ - enableImportInjection?: boolean; - - /** - * Dispatches component lifecycle events. Mainly used for testing. Defaults to `false`. - */ - lifecycleDOMEvents?: boolean; - - /** - * It is possible to assign data to the actual ` - - - Initializing First Build... - - - - - -
    -
    -
    Initializing First Build...
    -
    - -
    -
    -
    - -
    -
    
    -  
    - - - - - \ No newline at end of file diff --git a/src/dev-server/test/dev-server-utils.spec.ts b/src/dev-server/test/dev-server-utils.spec.ts deleted file mode 100644 index 457aa898d6c..00000000000 --- a/src/dev-server/test/dev-server-utils.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { isCssFile, isHtmlFile } from '../dev-server-utils'; - -describe('dev-server-utils', () => { - describe('isHtmlFile', () => { - it.each(['.html', 'foo.html', 'foo/bar.html'])('returns true for .html files (%s)', (filename) => { - expect(isHtmlFile(filename)).toEqual(true); - }); - - it.each(['.htm', 'foo.htm', 'foo/bar.htm'])('returns true for .htm files (%s)', (filename) => { - expect(isHtmlFile(filename)).toEqual(true); - }); - - it.each(['.ht', 'foo.htmx', 'foo/bar.xaml'])('returns false for other types of files (%s)', (filename) => { - expect(isHtmlFile(filename)).toEqual(false); - }); - - it.each(['.hTMl', 'foo.HTML', 'foo/bar.htmL'])('is case insensitive for filename (%s)', (filename) => { - expect(isHtmlFile(filename)).toEqual(true); - }); - }); - - describe('isCssFile', () => { - it.each(['.css', 'foo.css', 'foo/bar.css'])('returns true for .css files (%s)', (filename) => { - expect(isCssFile(filename)).toEqual(true); - }); - - it.each(['.txt', 'foo.sass', 'foo/bar.htm'])('returns false for other types of files (%s)', (filename) => { - expect(isCssFile(filename)).toEqual(false); - }); - - it.each(['.cSs', 'foo.cSS', 'foo/bar.CSS'])('is case insensitive for filename (%s)', (filename) => { - expect(isCssFile(filename)).toEqual(true); - }); - }); -}); diff --git a/src/dev-server/test/req-handler.spec.ts b/src/dev-server/test/req-handler.spec.ts deleted file mode 100644 index 152dfe55e5c..00000000000 --- a/src/dev-server/test/req-handler.spec.ts +++ /dev/null @@ -1,476 +0,0 @@ -import type * as d from '@stencil/core/declarations'; -import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; -import { normalizePath } from '@utils'; -import nodeFs from 'fs'; -import type { IncomingMessage, ServerResponse } from 'http'; -import path from 'path'; - -import { validateConfig } from '../../compiler/config/validate-config'; -import { validateDevServer } from '../../compiler/config/validate-dev-server'; -import { createSystem } from '../../compiler/sys/stencil-sys'; -import { createRequestHandler } from '../request-handler'; -import { appendDevServerClientIframe } from '../serve-file'; -import { createServerContext } from '../server-context'; - -describe('request-handler', () => { - let devServerConfig: d.DevServerConfig; - let serverCtx: d.DevServerContext; - let sys: d.CompilerSystem; - let req: IncomingMessage; - let res: TestServerResponse; - let sendMsg: d.DevServerSendMessage; - const root = path.resolve('/'); - const tmplDirPath = normalizePath(path.join(__dirname, '..', 'templates', 'directory-index.html')); - const tmplDir = nodeFs.readFileSync(tmplDirPath, 'utf8'); - - beforeEach(async () => { - sys = createSystem(); - - const validated = validateConfig(mockConfig(), mockLoadConfigInit()); - const stencilConfig = validated.config; - stencilConfig.flags.serve = true; - - stencilConfig.devServer = { - devServerDir: normalizePath(path.join(__dirname, '..')), - root: normalizePath(path.join(root, 'www')), - basePath: '/', - }; - - await sys.createDir(stencilConfig.devServer.root); - await sys.writeFile(path.join(stencilConfig.devServer.devServerDir, 'templates', 'directory-index.html'), tmplDir); - - devServerConfig = validateDevServer(stencilConfig, []); - req = {} as any; - res = {} as any; - - res.writeHead = (statusCode: number, headers: any): any => { - res.$statusCode = statusCode; - res.$headers = headers; - res.$contentType = headers && headers['content-type']; - }; - - res.write = (content: any) => { - res.$contentWrite = content; - return true; - }; - - res.end = () => { - res.$content = res.$contentWrite; - return this; - }; - - sendMsg = () => {}; - - serverCtx = createServerContext(sys, sendMsg, devServerConfig, [], []); - }); - - describe('historyApiFallback', () => { - it('should load historyApiFallback index.html when dot in the url disableDotRule true', async () => { - await sys.writeFile(path.join(root, 'www', 'index.html'), `root-index`); - devServerConfig.historyApiFallback = { - index: 'index.html', - disableDotRule: true, - }; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.headers = { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', - }; - req.url = '/about.us'; - req.method = 'GET'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - }); - - it('should not load historyApiFallback index.html when dot in the url', async () => { - await sys.writeFile(path.join(root, 'www', 'index.html'), `root-index`); - devServerConfig.historyApiFallback = { - index: 'index.html', - }; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.headers = { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', - }; - req.url = '/about.us'; - req.method = 'GET'; - - await handler(req, res); - expect(res.$statusCode).toBe(404); - }); - - it('should not load historyApiFallback index.html when no text/html accept header', async () => { - await sys.writeFile(path.join(root, 'www', 'index.html'), `root-index`); - devServerConfig.historyApiFallback = { - index: 'index.html', - }; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.headers = { - accept: '*/*', - }; - req.url = '/about-us'; - req.method = 'GET'; - - await handler(req, res); - expect(res.$statusCode).toBe(404); - }); - - it('should not load historyApiFallback index.html when not GET request', async () => { - await sys.writeFile(path.join(root, 'www', 'index.html'), `root-index`); - devServerConfig.historyApiFallback = { - index: 'index.html', - }; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.headers = { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', - }; - req.url = '/about-us'; - req.method = 'POST'; - - await handler(req, res); - expect(res.$statusCode).toBe(404); - }); - - it('should load historyApiFallback index.html when no trailing slash', async () => { - await sys.writeFile(path.join(root, 'www', 'index.html'), `root-index`); - devServerConfig.historyApiFallback = { - index: 'index.html', - }; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.headers = { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', - }; - req.url = '/about-us'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('root-index'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('should load historyApiFallback index.html when trailing slash', async () => { - await sys.writeFile(path.join(root, 'www', 'index.html'), `root-index`); - devServerConfig.historyApiFallback = { - index: 'index.html', - }; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.headers = { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', - }; - req.url = '/about-us/'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('root-index'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('should list directory when ended in slash and not using historyApiFallback', async () => { - await sys.createDir(path.join(root, 'www', 'about-us')); - await sys.writeFile(path.join(root, 'www', 'about-us', 'somefile1.html'), `somefile1`); - await sys.writeFile(path.join(root, 'www', 'about-us', 'somefile2.html'), `somefile2`); - devServerConfig.historyApiFallback = null; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.headers = { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', - }; - req.url = '/about-us/'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('tmpl-dir'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - }); - - describe('serve directory index', () => { - it('should load index.html in directory', async () => { - await sys.createDir(path.join(root, 'www', 'about-us')); - await sys.writeFile(path.join(root, 'www', 'about-us.html'), `about-us.html page`); - await sys.writeFile(path.join(root, 'www', 'about-us', 'index.html'), `about-us-index-directory`); - devServerConfig.historyApiFallback = null; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.headers = { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', - }; - req.url = '/about-us/'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('about-us-index-directory'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('should redirect directory w/ slash', async () => { - await sys.createDir(path.join(root, 'www', 'about-us')); - await sys.writeFile(path.join(root, 'www', 'about-us', 'somefile1.html'), `somefile1`); - await sys.writeFile(path.join(root, 'www', 'about-us', 'somefile2.html'), `somefile2`); - devServerConfig.historyApiFallback = {}; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.headers = { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', - }; - req.url = '/about-us'; - - await handler(req, res); - expect(res.$statusCode).toBe(302); - expect(res.$headers.location).toBe('/about-us/'); - }); - - it('get directory index.html with no trailing slash', async () => { - await sys.createDir(path.join(root, 'www', 'about-us')); - await sys.writeFile(path.join(root, 'www', 'about-us', 'index.html'), `aboutus`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/about-us'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('aboutus'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('get directory index.html with trailing slash and base url', async () => { - devServerConfig.basePath = '/my-base-url/'; - await sys.createDir(path.join(root, 'www', 'about-us')); - await sys.writeFile(path.join(root, 'www', 'about-us', 'index.html'), `aboutus`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/my-base-url/about-us/'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('aboutus'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('get directory index.html without trailing slash and base url', async () => { - devServerConfig.basePath = '/my-base-url/'; - await sys.createDir(path.join(root, 'www', 'about-us')); - await sys.writeFile(path.join(root, 'www', 'about-us', 'index.html'), `aboutus`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/my-base-url/about-us'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('aboutus'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('get directory index.html with trailing slash', async () => { - await sys.createDir(path.join(root, 'www', 'about-us')); - await sys.writeFile(path.join(root, 'www', 'about-us', 'index.html'), `aboutus`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/about-us/'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('aboutus'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - }); - - describe('error not found static files', () => { - it('not find file', async () => { - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/www/index.html'; - - await handler(req, res); - expect(res.$statusCode).toBe(404); - expect(res.$content).toContain('/index.html'); - expect(res.$contentType).toBe('text/plain; charset=utf-8'); - }); - }); - - describe('root index', () => { - it('serve directory listing when no index.html', async () => { - await sys.writeFile(path.join(root, 'www', 'styles.css'), `/* hi */`); - await sys.writeFile(path.join(root, 'www', 'scripts.js'), `// hi`); - await sys.writeFile(path.join(root, 'www', '.gitignore'), `# gitignore`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('tmpl-dir'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('serve root index.html w/ querystring', async () => { - await sys.writeFile(path.join(root, 'www', 'index.html'), `hello`); - devServerConfig.gzip = false; - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/?qs=123'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('hello'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('serve root index.html w/ base url without url trailing slash', async () => { - devServerConfig.basePath = '/my-base-url/'; - await sys.writeFile(path.join(root, 'www', 'index.html'), `hello`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/my-base-url'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('hello'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('serve root index.html w/ base url without trailing slash, with trailing slash url', async () => { - devServerConfig.basePath = '/my-base-url'; - await sys.writeFile(path.join(root, 'www', 'index.html'), `hello`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/my-base-url/'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('hello'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('serve root index.html w/ base url w/ index.html', async () => { - devServerConfig.basePath = '/my-base-url/'; - await sys.writeFile(path.join(root, 'www', 'index.html'), `hello`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/my-base-url/index.html'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('hello'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('serve root index.html w/ base url', async () => { - devServerConfig.basePath = '/my-base-url/'; - await sys.writeFile(path.join(root, 'www', 'index.html'), `hello`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/my-base-url/'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('hello'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('serve root index.html', async () => { - await sys.writeFile(path.join(root, 'www', 'index.html'), `hello`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content).toContain('hello'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('302 redirect to / when no path at all', async () => { - await sys.writeFile(path.join(root, 'www', 'index.html'), `hello`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = ''; - - await handler(req, res); - expect(res.$statusCode).toBe(302); - expect(res.$headers.location).toBe('/'); - }); - }); - - describe('serve static text files', () => { - it('should load file w/ querystring', async () => { - await sys.writeFile(path.join(root, 'www', 'scripts', 'file1.html'), `html`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/scripts/file1.html?qs=1234'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content.split('\n')[0]).toContain('html'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - - it('should load html file', async () => { - await sys.writeFile(path.join(root, 'www', 'scripts', 'file1.html'), `html`); - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/scripts/file1.html'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - expect(res.$content.split('\n')[0]).toContain('html'); - expect(res.$contentType).toBe('text/html; charset=utf-8'); - }); - }); - - describe('iframe connector', () => { - it('appends to ', () => { - const h = appendDevServerClientIframe(`88mph`, ``); - expect(h).toBe(`88mph`); - }); - - it('appends to ', () => { - const h = appendDevServerClientIframe(`88mph`, ``); - expect(h).toBe(`88mph`); - }); - - it('appends to end', () => { - const h = appendDevServerClientIframe(`88mph`, ``); - expect(h).toBe(`88mph`); - }); - }); - - describe('pingRoute', () => { - it('should return a 200 for successful build', async () => { - serverCtx.getBuildResults = () => - Promise.resolve({ hasSuccessfulBuild: true }) as Promise; - - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/ping'; - - await handler(req, res); - expect(res.$statusCode).toBe(200); - }); - - it('should return a 500 for unsuccessful build', async () => { - serverCtx.getBuildResults = () => - Promise.resolve({ hasSuccessfulBuild: false }) as Promise; - - const handler = createRequestHandler(devServerConfig, serverCtx); - - req.url = '/ping'; - - await handler(req, res); - expect(res.$statusCode).toBe(500); - }); - }); -}); - -interface TestServerResponse extends ServerResponse { - $statusCode?: number; - $headers?: any; - $contentWrite?: string; - $content?: string; - $contentType?: string; -} diff --git a/src/dev-server/test/server-http.spec.ts b/src/dev-server/test/server-http.spec.ts deleted file mode 100644 index a9d87a706c1..00000000000 --- a/src/dev-server/test/server-http.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as net from 'net'; - -import { findClosestOpenPort } from '../server-http'; - -describe('server-http', () => { - describe('findClosestOpenPort', () => { - let testServer: net.Server; - const TEST_HOST = '127.0.0.1'; - const TEST_PORT = 9876; - - afterEach(async () => { - if (testServer) { - await new Promise((resolve) => { - testServer.close(() => resolve()); - }); - } - }); - - it('should return the same port if it is available', async () => { - const port = await findClosestOpenPort(TEST_HOST, TEST_PORT); - expect(port).toBe(TEST_PORT); - }); - - it('should find the next available port when strictPort is false', async () => { - // Occupy the test port - testServer = net.createServer(); - await new Promise((resolve) => { - testServer.listen(TEST_PORT, TEST_HOST, () => resolve()); - }); - - const port = await findClosestOpenPort(TEST_HOST, TEST_PORT, false); - expect(port).toBe(TEST_PORT + 1); - }); - - it('should find the next available port when strictPort is not provided (defaults to false)', async () => { - // Occupy the test port - testServer = net.createServer(); - await new Promise((resolve) => { - testServer.listen(TEST_PORT, TEST_HOST, () => resolve()); - }); - - const port = await findClosestOpenPort(TEST_HOST, TEST_PORT); - expect(port).toBe(TEST_PORT + 1); - }); - - it('should throw an error when port is taken and strictPort is true', async () => { - // Occupy the test port - testServer = net.createServer(); - await new Promise((resolve) => { - testServer.listen(TEST_PORT, TEST_HOST, () => resolve()); - }); - - await expect(findClosestOpenPort(TEST_HOST, TEST_PORT, true)).rejects.toThrow( - `Port ${TEST_PORT} is already in use. Please specify a different port or set strictPort to false.`, - ); - }); - - it('should return the port when available and strictPort is true', async () => { - const port = await findClosestOpenPort(TEST_HOST, TEST_PORT, true); - expect(port).toBe(TEST_PORT); - }); - }); -}); diff --git a/src/dev-server/test/tsconfig.json b/src/dev-server/test/tsconfig.json deleted file mode 100644 index 3593ee977ed..00000000000 --- a/src/dev-server/test/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../testing/tsconfig.internal.json" -} diff --git a/src/dev-server/test/util.spec.ts b/src/dev-server/test/util.spec.ts deleted file mode 100644 index be1f87ef924..00000000000 --- a/src/dev-server/test/util.spec.ts +++ /dev/null @@ -1,212 +0,0 @@ -import type * as d from '@stencil/core/declarations'; - -import { DEV_SERVER_URL } from '../dev-server-constants'; -import { - getBrowserUrl, - getDevServerClientUrl, - getSsrStaticDataPath, - isExtensionLessPath, - isSsrStaticDataPath, -} from '../dev-server-utils'; - -describe('dev-server, util', () => { - it('should get url with custom base url and pathname', () => { - const protocol = 'http:'; - const address = '0.0.0.0'; - const port = 44; - const baseUrl = '/my-base-url/'; - const pathname = '/my-custom-path-name'; - const url = getBrowserUrl(protocol, address, port, baseUrl, pathname); - expect(url).toBe('http://localhost:44/my-base-url/my-custom-path-name'); - }); - - it('should get url with custom pathname', () => { - const protocol = 'http'; - const address = '0.0.0.0'; - const port = 44; - const baseUrl = '/'; - const pathname = '/my-custom-path-name'; - const url = getBrowserUrl(protocol, address, port, baseUrl, pathname); - expect(url).toBe('http://localhost:44/my-custom-path-name'); - }); - - it('should get path with 80 port', () => { - const protocol = 'http'; - const address = '0.0.0.0'; - const port = 80; - const baseUrl = '/'; - const pathname = '/'; - const url = getBrowserUrl(protocol, address, port, baseUrl, pathname); - expect(url).toBe('http://localhost/'); - }); - - it('should get path with no port', () => { - const protocol = 'http'; - const address = '0.0.0.0'; - const port: any = undefined; - const baseUrl = '/'; - const pathname = '/'; - const url = getBrowserUrl(protocol, address, port, baseUrl, pathname); - expect(url).toBe('http://localhost/'); - }); - - it('should get path with https', () => { - const protocol = 'https'; - const address = '0.0.0.0'; - const port = 3333; - const baseUrl = '/'; - const pathname = '/'; - const url = getBrowserUrl(protocol, address, port, baseUrl, pathname); - expect(url).toBe('https://localhost:3333/'); - }); - - it('should get path with custom address', () => { - const protocol = 'http'; - const address = 'staging.stenciljs.com'; - const port = 3333; - const baseUrl = '/'; - const pathname = '/'; - const url = getBrowserUrl(protocol, address, port, baseUrl, pathname); - expect(url).toBe('http://staging.stenciljs.com:3333/'); - }); -}); - -describe('getDevServerClientUrl', () => { - it('should get path for dev server w/ host w/ port w/ protocol', () => { - const devServerConfig: d.DevServerConfig = { - protocol: 'http', - address: '0.0.0.0', - port: 3333, - basePath: '/my-base-url/', - }; - const proto = 'https'; - const host = 'staging.stenciljs:5555.com'; - const url = getDevServerClientUrl(devServerConfig, host, proto); - expect(url).toBe(`https://staging.stenciljs:5555.com/my-base-url${DEV_SERVER_URL}`); - }); - - it('should get path for dev server w/ host w/ port no protocol', () => { - const devServerConfig: d.DevServerConfig = { - protocol: 'http', - address: '0.0.0.0', - port: 3333, - basePath: '/my-base-url/', - }; - const proto: string = null; - const host = 'staging.stenciljs:5555.com'; - const url = getDevServerClientUrl(devServerConfig, host, proto); - expect(url).toBe(`http://staging.stenciljs:5555.com/my-base-url${DEV_SERVER_URL}`); - }); - - it('should get path for dev server w/ host no port', () => { - const devServerConfig: d.DevServerConfig = { - protocol: 'http', - address: '0.0.0.0', - port: 3333, - basePath: '/my-base-url/', - }; - const proto: string = null; - const host = 'staging.stenciljs.com'; - const url = getDevServerClientUrl(devServerConfig, host, proto); - expect(url).toBe(`http://staging.stenciljs.com/my-base-url${DEV_SERVER_URL}`); - }); - - it('should get path for dev server w/ base url and port, no host', () => { - const devServerConfig: d.DevServerConfig = { - protocol: 'http', - address: '0.0.0.0', - port: 3333, - basePath: '/my-base-url/', - }; - const proto: string = null; - const host: string = null; - const url = getDevServerClientUrl(devServerConfig, host, proto); - expect(url).toBe(`http://localhost:3333/my-base-url${DEV_SERVER_URL}`); - }); - - it('should get path for dev server w/ base url and w/out port', () => { - const devServerConfig: d.DevServerConfig = { - protocol: 'http', - address: '0.0.0.0', - basePath: '/my-base-url/', - }; - const proto: string = null; - const host: string = null; - const url = getDevServerClientUrl(devServerConfig, host, proto); - expect(url).toBe(`${devServerConfig.protocol}://localhost/my-base-url${DEV_SERVER_URL}`); - }); - - it('should get path for dev server w/ custom address, base url and port', () => { - const devServerConfig: d.DevServerConfig = { - protocol: 'http', - address: '1.2.3.4', - port: 3333, - basePath: '/my-base-url/', - }; - const proto: string = null; - const host: string = null; - const url = getDevServerClientUrl(devServerConfig, host, proto); - expect(url).toBe(`${devServerConfig.protocol}://${devServerConfig.address}:3333/my-base-url${DEV_SERVER_URL}`); - }); - - it('isExtensionLessPath', () => { - expect(isExtensionLessPath('http://stenciljs.com/')).toBe(true); - expect(isExtensionLessPath('http://stenciljs.com/blog')).toBe(true); - expect(isExtensionLessPath('http://stenciljs.com/blog/')).toBe(true); - expect(isExtensionLessPath('http://stenciljs.com/.')).toBe(false); - expect(isExtensionLessPath('http://stenciljs.com/data.json')).toBe(false); - expect(isExtensionLessPath('http://stenciljs.com/index.html')).toBe(false); - expect(isExtensionLessPath('http://stenciljs.com/blog.html')).toBe(false); - }); - - it('isSsrStaticDataPath', () => { - expect(isSsrStaticDataPath('http://stenciljs.com/')).toBe(false); - expect(isSsrStaticDataPath('http://stenciljs.com/index.html')).toBe(false); - expect(isSsrStaticDataPath('http://stenciljs.com/page.state.json')).toBe(true); - }); - - it('getSsrStaticDataPath, root', () => { - const req: d.HttpRequest = { - url: new URL('http://stenciljs.com/page.static.json'), - method: 'GET', - acceptHeader: '', - searchParams: null, - }; - const r = getSsrStaticDataPath(req); - expect(r.fileName).toBe('page.static.json'); - expect(r.hasQueryString).toBe(false); - expect(r.ssrPath).toBe('http://stenciljs.com/'); - }); - - it('getSsrStaticDataPath, no trailing slash refer', () => { - const req: d.HttpRequest = { - url: new URL('http://stenciljs.com/blog/page.static.json?v=1234'), - method: 'GET', - acceptHeader: '', - searchParams: null, - headers: { - Referer: 'http://stenciljs.com/page', - }, - }; - const r = getSsrStaticDataPath(req); - expect(r.fileName).toBe('page.static.json'); - expect(r.hasQueryString).toBe(true); - expect(r.ssrPath).toBe('http://stenciljs.com/blog'); - }); - - it('getSsrStaticDataPath, with trailing slash refer', () => { - const req: d.HttpRequest = { - url: new URL('http://stenciljs.com/blog/page.static.json?v=1234'), - method: 'GET', - acceptHeader: '', - searchParams: null, - headers: { - Referer: 'http://stenciljs.com/page/', - }, - }; - const r = getSsrStaticDataPath(req); - expect(r.fileName).toBe('page.static.json'); - expect(r.hasQueryString).toBe(true); - expect(r.ssrPath).toBe('http://stenciljs.com/blog/'); - }); -}); diff --git a/src/hydrate/platform/hydrate-app.ts b/src/hydrate/platform/hydrate-app.ts deleted file mode 100644 index 4213a003b34..00000000000 --- a/src/hydrate/platform/hydrate-app.ts +++ /dev/null @@ -1,396 +0,0 @@ -import { globalScripts } from '@app-globals'; -import { addHostEventListeners, getHostRef, loadModule, plt, registerHost, setScopedSSR } from '@platform'; -import { connectedCallback, insertVdomAnnotations } from '@runtime'; -import { CMP_FLAGS } from '@utils'; - -import type * as d from '../../declarations'; -import { proxyHostElement } from './proxy-host-element'; - -export function hydrateApp( - win: Window & typeof globalThis, - opts: d.HydrateFactoryOptions, - results: d.HydrateResults, - afterHydrate: ( - win: Window, - opts: d.HydrateFactoryOptions, - results: d.HydrateResults, - resolve: (results: d.HydrateResults) => void, - ) => void, - resolve: (results: d.HydrateResults) => void, -) { - const connectedElements = new Set(); - const createdElements = new Set(); - const waitingElements = new Set(); - const orgDocumentCreateElement = win.document.createElement; - const orgDocumentCreateElementNS = win.document.createElementNS; - const resolved = Promise.resolve(); - setScopedSSR(opts); - - let tmrId: any; - let ranCompleted = false; - - function hydratedComplete() { - globalThis.clearTimeout(tmrId); - createdElements.clear(); - connectedElements.clear(); - - if (!ranCompleted) { - ranCompleted = true; - try { - if (opts.clientHydrateAnnotations) { - insertVdomAnnotations(win.document, opts.staticComponents); - } - - win.dispatchEvent(new win.Event('DOMContentLoaded')); - - win.document.createElement = orgDocumentCreateElement; - win.document.createElementNS = orgDocumentCreateElementNS; - } catch (e) { - renderCatchError(opts, results, e); - } - } - - afterHydrate(win, opts, results, resolve); - } - - function hydratedError(err: any) { - renderCatchError(opts, results, err); - hydratedComplete(); - } - - function timeoutExceeded() { - hydratedError(`Hydrate exceeded timeout${waitingOnElementsMsg(waitingElements)}`); - } - - try { - function patchedConnectedCallback(this: d.HostElement) { - return connectElement(this); - } - - function patchElement(elm: d.HostElement) { - if (isValidComponent(elm, opts)) { - // this element is a valid component - - const hostRef = getHostRef(elm); - if (!hostRef) { - // we haven't registered this component's host element yet - - // get the component's constructor - const Cstr = loadModule( - { - $tagName$: elm.nodeName.toLowerCase(), - $flags$: null, - }, - null, - ) as d.ComponentConstructor; - - if (Cstr != null && Cstr.cmpMeta != null) { - // we found valid component metadata - - if ( - opts.serializeShadowRoot !== false && - !!(Cstr.cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && - tagRequiresScoped(elm.tagName, opts.serializeShadowRoot) - ) { - // this component requires scoped css encapsulation during SSR - const cmpMeta = Cstr.cmpMeta; - cmpMeta.$flags$ |= CMP_FLAGS.shadowNeedsScopedCss; - - // 'cmpMeta' is a getter only, so needs redefining - Object.defineProperty(Cstr as any, 'cmpMeta', { - get: function (this: any) { - return cmpMeta; - }, - }); - } - - createdElements.add(elm); - elm.connectedCallback = patchedConnectedCallback; - - // register the host element - registerHost(elm, Cstr.cmpMeta); - - // proxy the host element with the component's metadata - proxyHostElement(elm, Cstr); - } - } - } - } - - function patchChild(elm: any) { - if (elm != null && elm.nodeType === 1) { - patchElement(elm); - const children = elm.children; - for (let i = 0, ii = children.length; i < ii; i++) { - patchChild(children[i]); - } - } - } - - function connectElement(elm: HTMLElement) { - createdElements.delete(elm); - - if (isValidComponent(elm, opts) && results.hydratedCount < opts.maxHydrateCount) { - // this is a valid component to hydrate - // and we haven't hit our max hydrated count yet - - if (!connectedElements.has(elm) && shouldHydrate(elm)) { - // we haven't connected this component yet - // and all of its ancestor elements are valid too - - // add it to our Set so we know it's already being connected - connectedElements.add(elm); - return hydrateComponent.call(elm, win, results, elm.nodeName, elm, waitingElements); - } - } - - return resolved; - } - - function waitLoop(): Promise { - const toConnect = Array.from(createdElements).filter((elm) => elm.parentElement); - if (toConnect.length > 0) { - return Promise.all(toConnect.map(connectElement)).then(waitLoop); - } - return resolved; - } - - win.document.createElement = function patchedCreateElement(tagName: string) { - const elm = orgDocumentCreateElement.call(win.document, tagName); - patchElement(elm); - return elm; - }; - - win.document.createElementNS = function patchedCreateElement(namespaceURI: string, tagName: string) { - const elm = orgDocumentCreateElementNS.call(win.document, namespaceURI, tagName); - patchElement(elm as d.HostElement); - return elm; - } as (typeof window)['document']['createElementNS']; - - // ensure we use NodeJS's native setTimeout, not the mocked hydrate app scoped one - tmrId = globalThis.setTimeout(timeoutExceeded, opts.timeout); - - plt.$resourcesUrl$ = new URL(opts.resourcesUrl || './', win.document.baseURI).href; - - globalScripts(); - - patchChild(win.document.body); - - waitLoop().then(hydratedComplete).catch(hydratedError); - } catch (e) { - hydratedError(e); - } -} - -async function hydrateComponent( - this: HTMLElement, - win: Window & typeof globalThis, - results: d.HydrateResults, - tagName: string, - elm: d.HostElement, - waitingElements: Set, -) { - tagName = tagName.toLowerCase(); - const Cstr = loadModule( - { - $tagName$: tagName, - $flags$: null, - }, - null, - ) as d.ComponentConstructor; - - if (Cstr != null) { - const cmpMeta = Cstr.cmpMeta; - - if (cmpMeta != null) { - waitingElements.add(elm); - const hostRef = getHostRef(this); - if (!hostRef) { - return; - } - addHostEventListeners(this, hostRef, cmpMeta.$listeners$, false); - - try { - connectedCallback(elm); - await elm.componentOnReady(); - - results.hydratedCount++; - - const ref = getHostRef(elm); - const modeName = !ref?.$modeName$ ? '$' : ref?.$modeName$; - if (!results.components.some((c) => c.tag === tagName && c.mode === modeName)) { - results.components.push({ - tag: tagName, - mode: modeName, - count: 0, - depth: -1, - }); - } - } catch (e) { - win.console.error(e); - } - waitingElements.delete(elm); - } - } -} - -function isValidComponent(elm: Element, opts: d.HydrateFactoryOptions) { - if (elm != null && elm.nodeType === 1) { - // playing it safe and not using elm.tagName or elm.localName on purpose - const tagName = elm.nodeName; - if (typeof tagName === 'string' && tagName.includes('-')) { - if (opts.excludeComponents.includes(tagName.toLowerCase())) { - // this tagName we DO NOT want to hydrate - return false; - } - // all good, this is a valid component - return true; - } - } - return false; -} - -function shouldHydrate(elm: Element): boolean { - if (elm.nodeType === 9) { - return true; - } - if (NO_HYDRATE_TAGS.has(elm.nodeName)) { - return false; - } - if (elm.hasAttribute('no-prerender')) { - return false; - } - const parentNode = elm.parentNode; - if (parentNode == null) { - return true; - } - - return shouldHydrate(parentNode as Element); -} - -const NO_HYDRATE_TAGS = new Set([ - 'CODE', - 'HEAD', - 'IFRAME', - 'INPUT', - 'OBJECT', - 'OUTPUT', - 'NOSCRIPT', - 'PRE', - 'SCRIPT', - 'SELECT', - 'STYLE', - 'TEMPLATE', - 'TEXTAREA', -]); - -function renderCatchError(opts: d.HydrateFactoryOptions, results: d.HydrateResults, err: any) { - const diagnostic: d.Diagnostic = { - level: 'error', - type: 'build', - header: 'Hydrate Error', - messageText: '', - relFilePath: undefined, - absFilePath: undefined, - lines: [], - }; - - if (opts.url) { - try { - const u = new URL(opts.url); - if (u.pathname !== '/') { - diagnostic.header += ': ' + u.pathname; - } - } catch (e) {} - } - - if (err != null) { - if (err.stack != null) { - diagnostic.messageText = err.stack.toString(); - } else if (err.message != null) { - diagnostic.messageText = err.message.toString(); - } else { - diagnostic.messageText = err.toString(); - } - } - - results.diagnostics.push(diagnostic); -} - -function printTag(elm: HTMLElement) { - let tag = `<${elm.nodeName.toLowerCase()}`; - if (Array.isArray(elm.attributes)) { - for (let i = 0; i < elm.attributes.length; i++) { - const attr = elm.attributes[i]; - tag += ` ${attr.name}`; - if (attr.value !== '') { - tag += `="${attr.value}"`; - } - } - } - tag += `>`; - return tag; -} - -function waitingOnElementMsg(waitingElement: HTMLElement) { - let msg = ''; - if (waitingElement) { - const lines = []; - - msg = ' - waiting on:'; - let elm = waitingElement; - while (elm && elm.nodeType !== 9 && elm.nodeName !== 'BODY') { - lines.unshift(printTag(elm)); - elm = elm.parentElement; - } - - let indent = ''; - for (const ln of lines) { - indent += ' '; - msg += `\n${indent}${ln}`; - } - } - return msg; -} - -function waitingOnElementsMsg(waitingElements: Set) { - return Array.from(waitingElements).map(waitingOnElementMsg); -} - -/** - * Determines if the tag requires a declarative shadow dom - * or a scoped / light dom during SSR. - * - * @param tagName - component tag name - * @param opts - serializeShadowRoot options - * @returns `true` when the tag requires a scoped / light dom during SSR - */ -export function tagRequiresScoped(tagName: string, opts: d.HydrateFactoryOptions['serializeShadowRoot']) { - if (typeof opts === 'string') { - return opts === 'scoped'; - } - - if (typeof opts === 'boolean') { - return opts === true ? false : true; - } - - if (typeof opts === 'object') { - tagName = tagName.toLowerCase(); - - if (Array.isArray(opts['declarative-shadow-dom']) && opts['declarative-shadow-dom'].includes(tagName)) { - // if the tag is in the dsd array, return dsd - return false; - } else if ( - (!Array.isArray(opts.scoped) || !opts.scoped.includes(tagName)) && - opts.default === 'declarative-shadow-dom' - ) { - // if the tag is not in the scoped array and the default is dsd, return dsd - return false; - } else { - // otherwise, return scoped - return true; - } - } - - return false; -} diff --git a/src/hydrate/platform/index.ts b/src/hydrate/platform/index.ts deleted file mode 100644 index af8f85a93c0..00000000000 --- a/src/hydrate/platform/index.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { BUILD } from '@app-data'; -import { reWireGetterSetter } from '@utils/es2022-rewire-class-members'; - -import type * as d from '../../declarations'; -import { CMP_FLAGS } from '@utils/constants'; - -/** - * Access transformTag via the closure-scoped $stencilTagTransform object. - * This object is defined in the factory closure (HYDRATE_FACTORY_INTRO). - * We declare it here to satisfy TypeScript, but at runtime it will be - * provided by the factory closure scope. - */ -declare const $stencilTagTransform: { transformTag: (tag: string) => string }; - -let customError: d.ErrorHandler; - -export const cmpModules = new Map(); - -const getModule = (tagName: string): d.ComponentConstructor | null => { - if (typeof tagName === 'string') { - tagName = tagName.toLowerCase(); - const cmpModule = cmpModules.get(tagName); - if (cmpModule != null) { - return cmpModule[tagName]; - } - } - return null; -}; - -export const loadModule = ( - cmpMeta: d.ComponentRuntimeMeta, - _hostRef: d.HostRef, - _hmrVersionId?: string, -): d.ComponentConstructor | null => { - return getModule(cmpMeta.$tagName$); -}; - -export const isMemberInElement = (elm: any, memberName: string) => { - if (elm != null) { - if (memberName in elm) { - return true; - } - const cstr = getModule(elm.nodeName); - if (cstr != null) { - const hostRef: d.ComponentNativeConstructor = cstr as any; - if (hostRef != null && hostRef.cmpMeta != null && hostRef.cmpMeta.$members$ != null) { - return memberName in hostRef.cmpMeta.$members$; - } - } - } - return false; -}; - -export const registerComponents = (Cstrs: d.ComponentNativeConstructor[]) => { - for (const Cstr of Cstrs) { - // using this format so it follows exactly how client-side modules work - const exportName = Cstr.cmpMeta.$tagName$; - // Access transformTag from the closure-scoped $stencilTagTransform object - // This ensures we use the same instance as the runner (prevents duplication) - const transformedTagName = $stencilTagTransform.transformTag(exportName); - - cmpModules.set(exportName, { - [exportName]: Cstr, - }); - if (transformedTagName !== exportName) { - cmpModules.set(transformedTagName, { - [transformedTagName]: Cstr, - }); - } - } -}; - -export const win = window; - -export const readTask = (cb: Function) => { - nextTick(() => { - try { - cb(); - } catch (e) { - consoleError(e); - } - }); -}; - -export const writeTask = (cb: Function) => { - nextTick(() => { - try { - cb(); - } catch (e) { - consoleError(e); - } - }); -}; - -const resolved = /*@__PURE__*/ Promise.resolve(); -export const nextTick = (cb: () => void) => resolved.then(cb); - -const defaultConsoleError = (e: any) => { - if (e != null) { - console.error(e.stack || e.message || e); - } -}; - -export const consoleError: d.ErrorHandler = (e: any, el?: any) => (customError || defaultConsoleError)(e, el); - -export const consoleDevError = (..._: any[]) => { - /* noop for hydrate */ -}; - -export const consoleDevWarn = (..._: any[]) => { - /* noop for hydrate */ -}; - -export const consoleDevInfo = (..._: any[]) => { - /* noop for hydrate */ -}; - -export const setErrorHandler = (handler: d.ErrorHandler) => (customError = handler); - -export const plt: d.PlatformRuntime = { - $flags$: 0, - $resourcesUrl$: '', - jmp: (h) => h(), - raf: (h) => requestAnimationFrame(h), - ael: (el, eventName, listener, opts) => el.addEventListener(eventName, listener, opts), - rel: (el, eventName, listener, opts) => el.removeEventListener(eventName, listener, opts), - ce: (eventName, opts) => new win.CustomEvent(eventName, opts), -}; - -export const setPlatformHelpers = (helpers: { - jmp?: (c: any) => any; - raf?: (c: any) => number; - ael?: (el: any, eventName: string, listener: any, options: any) => void; - rel?: (el: any, eventName: string, listener: any, options: any) => void; - ce?: (eventName: string, opts?: any) => any; -}) => { - Object.assign(plt, helpers); -}; - -export const supportsShadow = BUILD.shadowDom; - -export const supportsListenerOptions = false; - -export const supportsConstructableStylesheets = false; -export const supportsMutableAdoptedStyleSheets = false; - -export const getHostRef = (ref: d.RuntimeRef) => { - if (ref.__stencil__getHostRef) { - return ref.__stencil__getHostRef(); - } - - return undefined; -}; - -export const registerInstance = (lazyInstance: any, hostRef: d.HostRef) => { - if (!hostRef) return undefined; - lazyInstance.__stencil__getHostRef = () => hostRef; - hostRef.$lazyInstance$ = lazyInstance; - - if (hostRef.$cmpMeta$.$flags$ & CMP_FLAGS.hasModernPropertyDecls && (BUILD.state || BUILD.prop)) { - reWireGetterSetter(lazyInstance, hostRef); - } - return hostRef; -}; - -export const registerHost = (elm: d.HostElement, cmpMeta: d.ComponentRuntimeMeta) => { - const hostRef: d.HostRef = { - $flags$: 0, - $cmpMeta$: cmpMeta, - $hostElement$: elm, - $instanceValues$: new Map(), - $serializerValues$: new Map(), - $renderCount$: 0, - }; - hostRef.$fetchedCbList$ = []; - hostRef.$onInstancePromise$ = new Promise((r) => (hostRef.$onInstanceResolve$ = r)); - hostRef.$onReadyPromise$ = new Promise((r) => (hostRef.$onReadyResolve$ = r)); - elm['s-p'] = []; - elm['s-rc'] = []; - elm.__stencil__getHostRef = () => hostRef; - - return hostRef; -}; - -export const Build: d.UserBuildConditionals = { - isDev: false, - isBrowser: false, - isServer: true, - isTesting: false, -}; - -export const styles: d.StyleMap = new Map(); -export const modeResolutionChain: d.ResolutionHandler[] = []; - -/** - * Checks to see any components are rendered with `scoped` - * @param opts - SSR options - */ -export const setScopedSSR = (opts: d.HydrateFactoryOptions) => { - scopedSSR = - BUILD.shadowDom && opts.serializeShadowRoot !== false && opts.serializeShadowRoot !== 'declarative-shadow-dom'; -}; -export const needsScopedSSR = () => scopedSSR; - -let scopedSSR = false; - -export { hAsync as h } from './h-async'; -export { hydrateApp } from './hydrate-app'; -export { BUILD, Env, NAMESPACE } from '@app-data'; -export { - addHostEventListeners, - bootstrapLazy, - connectedCallback, - createEvent, - defineCustomElement, - disconnectedCallback, - forceModeUpdate, - forceUpdate, - Fragment, - getAssetPath, - getElement, - getMode, - getRenderingRef, - getValue, - Host, - insertVdomAnnotations, - jsx, - jsxDEV, - jsxs, - Mixin, - parsePropertyValue, - postUpdateComponent, - proxyComponent, - proxyCustomElement, - renderVdom, - setAssetPath, - setMode, - setNonce, - setTagTransformer, - setValue, - transformTag, -} from '@runtime'; diff --git a/src/hydrate/platform/test/serialize-shadow-root-opts.spec.ts b/src/hydrate/platform/test/serialize-shadow-root-opts.spec.ts deleted file mode 100644 index 85f1b1c25e6..00000000000 --- a/src/hydrate/platform/test/serialize-shadow-root-opts.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { tagRequiresScoped as TypeTagRequiresScoped } from '../hydrate-app'; - -describe('tagRequiresScoped', () => { - let tagRequiresScoped: typeof TypeTagRequiresScoped; - - beforeEach(async () => { - tagRequiresScoped = require('../hydrate-app').tagRequiresScoped; - }); - - afterEach(async () => { - jest.resetModules(); - }); - - it('should return true for a component with serializeShadowRoot: true', () => { - expect(tagRequiresScoped('cmp-a', true)).toBe(false); - }); - - it('should return false for a component serializeShadowRoot: false', () => { - expect(tagRequiresScoped('cmp-b', false)).toBe(true); - }); - - it('should return false for a component with serializeShadowRoot: undefined', () => { - expect(tagRequiresScoped('cmp-c', undefined)).toBe(false); - }); - - it('should return true for a component with serializeShadowRoot: "scoped"', () => { - expect(tagRequiresScoped('cmp-d', 'scoped')).toBe(true); - }); - - it('should return false for a component with serializeShadowRoot: "declarative-shadow-dom"', () => { - expect(tagRequiresScoped('cmp-e', 'declarative-shadow-dom')).toBe(false); - }); - - it('should return true for a component when tag is in scoped list', () => { - expect(tagRequiresScoped('cmp-f', { scoped: ['cmp-f'], default: 'scoped' })).toBe(true); - }); - - it('should return false for a component when tag is not scoped list', () => { - expect(tagRequiresScoped('cmp-g', { scoped: ['cmp-f'], default: 'declarative-shadow-dom' })).toBe(false); - }); - - it('should return true for a component when default is scoped', () => { - expect(tagRequiresScoped('cmp-g', { 'declarative-shadow-dom': ['cmp-f'], default: 'scoped' })).toBe(true); - }); - - it('should return false for a component when default is declarative-shadow-dom', () => { - expect(tagRequiresScoped('cmp-g', { 'declarative-shadow-dom': ['cmp-f'], default: 'declarative-shadow-dom' })).toBe( - false, - ); - }); -}); diff --git a/src/hydrate/runner/hydrate-factory.ts b/src/hydrate/runner/hydrate-factory.ts deleted file mode 100644 index 0bf0a66c31a..00000000000 --- a/src/hydrate/runner/hydrate-factory.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MockWindow } from '@stencil/core/mock-doc'; - -import type * as d from '../../declarations'; - -export function hydrateFactory( - win: MockWindow, - opts: d.HydrateDocumentOptions, - results: d.HydrateResults, - afterHydrate: ( - win: MockWindow, - opts: DocOptions, - results: d.HydrateResults, - resolve: (results: d.HydrateResults) => void, - ) => void, - resolve: (results: d.HydrateResults) => void, -) { - win; - opts; - results; - afterHydrate; - resolve; -} - -/** - * These are stub exports that will be replaced during compilation with the actual - * tag transform functions from the factory bundle. - */ -export const setTagTransformer: d.TagTransformer = null as any; -export const transformTag: (tag: T) => T = null as any; diff --git a/src/hydrate/runner/index.ts b/src/hydrate/runner/index.ts deleted file mode 100644 index bd171dd1027..00000000000 --- a/src/hydrate/runner/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { createWindowFromHtml } from './create-window'; -export { hydrateDocument, renderToString, serializeDocumentToString, streamToString } from './render'; -export { deserializeProperty, serializeProperty } from '@utils'; - -import { setTagTransformer, transformTag } from '@runtime'; -export { setTagTransformer, transformTag }; diff --git a/src/hydrate/runner/render.ts b/src/hydrate/runner/render.ts deleted file mode 100644 index 959b8b580ef..00000000000 --- a/src/hydrate/runner/render.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { Readable } from 'node:stream'; - -import { hydrateFactory } from '@hydrate-factory'; -import { modeResolutionChain, setMode } from '@platform'; -import { HYDRATED_STYLE_ID } from '@runtime'; -import { MockWindow, serializeNodeToHtml } from '@stencil/core/mock-doc'; -import { hasError } from '@utils'; - -import { updateCanonicalLink } from '../../compiler/html/canonical-link'; -import { relocateMetaCharset } from '../../compiler/html/relocate-meta-charset'; -import { removeUnusedStyles } from '../../compiler/html/remove-unused-styles'; -import type { - HydrateDocumentOptions, - HydrateFactoryOptions, - HydrateResults, - SerializeDocumentOptions, -} from '../../declarations'; -import { inspectElement } from './inspect-element'; -import { patchDomImplementation } from './patch-dom-implementation'; -import { generateHydrateResults, normalizeHydrateOptions, renderBuildError, renderCatchError } from './render-utils'; -import { initializeWindow } from './window-initialize'; - -const NOOP = () => {}; - -export function streamToString(html: string | any, option?: SerializeDocumentOptions) { - return renderToString(html, option, true); -} - -export function renderToString(html: string | any, options?: SerializeDocumentOptions): Promise; -export function renderToString( - html: string | any, - options: SerializeDocumentOptions | undefined, - asStream: true, -): Readable; -export function renderToString( - html: string | any, - options?: SerializeDocumentOptions, - asStream?: boolean, -): Promise | Readable { - const opts = normalizeHydrateOptions(options); - /** - * Makes the rendered DOM not being rendered to a string. - */ - opts.serializeToHtml = true; - /** - * Set the flag whether or not we like to render into a declarative shadow root. - */ - opts.fullDocument = typeof opts.fullDocument === 'boolean' ? opts.fullDocument : true; - /** - * Defines whether we render the shadow root as a declarative shadow root or as scoped shadow root. - */ - opts.serializeShadowRoot = - typeof opts.serializeShadowRoot === 'undefined' ? 'declarative-shadow-dom' : opts.serializeShadowRoot; - /** - * Make sure we wait for components to be hydrated. - */ - opts.constrainTimeouts = false; - - return hydrateDocument(html, opts, asStream); -} - -export function hydrateDocument(doc: any | string, options?: HydrateDocumentOptions): Promise; -export function hydrateDocument( - doc: any | string, - options: HydrateDocumentOptions | undefined, - asStream?: boolean, -): Readable; -export function hydrateDocument( - doc: any | string, - options?: HydrateDocumentOptions, - asStream?: boolean, -): Promise | Readable { - const opts = normalizeHydrateOptions(options); - /** - * Defines whether we render the shadow root as a declarative shadow root or as scoped shadow root. - */ - opts.serializeShadowRoot = - typeof opts.serializeShadowRoot === 'undefined' ? 'declarative-shadow-dom' : opts.serializeShadowRoot; - - let win: MockWindow | null = null; - const results = generateHydrateResults(opts); - - if (hasError(results.diagnostics)) { - return Promise.resolve(results); - } - - if (typeof doc === 'string') { - try { - opts.destroyWindow = true; - opts.destroyDocument = true; - win = new MockWindow(doc); - - if (!asStream) { - return render(win, opts, results).then(() => results); - } - - return renderStream(win, opts, results); - } catch (e) { - if (win && win.close) { - win.close(); - } - win = null; - renderCatchError(results, e); - return Promise.resolve(results); - } - } - - if (isValidDocument(doc)) { - try { - opts.destroyDocument = false; - win = patchDomImplementation(doc, opts); - - if (!asStream) { - return render(win, opts, results).then(() => results); - } - - return renderStream(win, opts, results); - } catch (e) { - if (win && win.close) { - win.close(); - } - win = null; - renderCatchError(results, e); - return Promise.resolve(results); - } - } - - renderBuildError(results, `Invalid html or document. Must be either a valid "html" string, or DOM "document".`); - return Promise.resolve(results); -} - -async function render(win: MockWindow, opts: HydrateFactoryOptions, results: HydrateResults) { - if ('process' in globalThis && typeof process.on === 'function' && !(process as any).__stencilErrors) { - (process as any).__stencilErrors = true; - process.on('unhandledRejection', (e) => { - console.log('unhandledRejection', e); - }); - } - - initializeWindow(win, win.document, opts, results); - const beforeHydrateFn = typeof opts.beforeHydrate === 'function' ? opts.beforeHydrate : NOOP; - try { - await Promise.resolve(beforeHydrateFn(win.document)); - return new Promise((resolve) => { - if (Array.isArray(opts.modes)) { - /** - * Reset the mode resolution chain as we expect every `renderToString` call to render - * the components in new environment/document. - */ - modeResolutionChain.length = 0; - opts.modes.forEach((mode) => setMode(mode)); - } - return hydrateFactory(win, opts, results, afterHydrate, resolve); - }); - } catch (e) { - renderCatchError(results, e); - return finalizeHydrate(win, win.document, opts, results); - } -} - -/** - * Wrapper around `render` method to enable streaming by returning a Readable instead of a promise. - * @param win MockDoc window object - * @param opts serialization options - * @param results render result object - * @returns a Readable that can be passed into a response - */ -function renderStream(win: MockWindow, opts: HydrateFactoryOptions, results: HydrateResults) { - async function* processRender() { - const renderResult = await render(win, opts, results); - yield renderResult.html; - } - - return Readable.from(processRender()); -} - -async function afterHydrate( - win: MockWindow, - opts: HydrateFactoryOptions, - results: HydrateResults, - resolve: (results: HydrateResults) => void, -) { - const afterHydrateFn = typeof opts.afterHydrate === 'function' ? opts.afterHydrate : NOOP; - try { - await Promise.resolve(afterHydrateFn(win.document)); - return resolve(finalizeHydrate(win, win.document, opts, results)); - } catch (e) { - renderCatchError(results, e); - return resolve(finalizeHydrate(win, win.document, opts, results)); - } -} - -function finalizeHydrate(win: MockWindow, doc: Document, opts: HydrateFactoryOptions, results: HydrateResults) { - try { - inspectElement(results, doc.documentElement, 0); - - if (opts.removeUnusedStyles !== false) { - try { - removeUnusedStyles(doc, results.diagnostics); - } catch (e) { - renderCatchError(results, e); - } - } - - if (typeof opts.title === 'string') { - try { - doc.title = opts.title; - } catch (e) { - renderCatchError(results, e); - } - } - - results.title = doc.title; - - if (opts.removeScripts) { - removeScripts(doc.documentElement); - } - - const styles = doc.querySelectorAll('head style'); - if (styles.length > 0) { - results.styles.push( - ...Array.from(styles).map((style) => ({ - href: style.getAttribute('href'), - id: style.getAttribute(HYDRATED_STYLE_ID), - content: style.textContent, - })), - ); - } - - try { - updateCanonicalLink(doc, opts.canonicalUrl); - } catch (e) { - renderCatchError(results, e); - } - - try { - relocateMetaCharset(doc); - } catch (e) {} - - if (!hasError(results.diagnostics)) { - results.httpStatus = 200; - } - - try { - const metaStatus = doc.head.querySelector('meta[http-equiv="status"]'); - if (metaStatus != null) { - const metaStatusContent = metaStatus.getAttribute('content'); - if (metaStatusContent && metaStatusContent.length > 0) { - results.httpStatus = parseInt(metaStatusContent, 10); - } - } - } catch (e) {} - - if (opts.clientHydrateAnnotations) { - doc.documentElement.classList.add('hydrated'); - } - - if (opts.serializeToHtml) { - results.html = serializeDocumentToString(doc, opts); - } - } catch (e) { - renderCatchError(results, e); - } - - destroyWindow(win, doc, opts, results); - return results; -} - -function destroyWindow(win: MockWindow, doc: Document, opts: HydrateFactoryOptions, results: HydrateResults) { - if (!opts.destroyWindow) { - return; - } - - try { - if (!opts.destroyDocument) { - (win as any).document = null; - (doc as any).defaultView = null; - } - - if (win.close) { - win.close(); - } - } catch (e) { - renderCatchError(results, e); - } -} - -export function serializeDocumentToString(doc: Document, opts: HydrateFactoryOptions) { - return serializeNodeToHtml(doc, { - approximateLineWidth: opts.approximateLineWidth, - outerHtml: false, - prettyHtml: opts.prettyHtml, - removeAttributeQuotes: opts.removeAttributeQuotes, - removeBooleanAttributeQuotes: opts.removeBooleanAttributeQuotes, - removeEmptyAttributes: opts.removeEmptyAttributes, - removeHtmlComments: opts.removeHtmlComments, - serializeShadowRoot: opts.serializeShadowRoot, - fullDocument: opts.fullDocument, - }); -} - -function isValidDocument(doc: Document) { - return ( - doc != null && - doc.nodeType === 9 && - doc.documentElement != null && - doc.documentElement.nodeType === 1 && - doc.body != null && - doc.body.nodeType === 1 - ); -} - -function removeScripts(elm: HTMLElement) { - const children = elm.children; - for (let i = children.length - 1; i >= 0; i--) { - const child = children[i]; - removeScripts(child as any); - - if (child.nodeName === 'SCRIPT' || (child.nodeName === 'LINK' && child.getAttribute('rel') === 'modulepreload')) { - child.remove(); - } - } -} diff --git a/src/hydrate/runner/window-initialize.ts b/src/hydrate/runner/window-initialize.ts deleted file mode 100644 index ac2bc421430..00000000000 --- a/src/hydrate/runner/window-initialize.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { constrainTimeouts, type MockWindow } from '@stencil/core/mock-doc'; -import { STENCIL_DOC_DATA } from 'src/runtime/runtime-constants'; - -import type * as d from '../../declarations'; -import { runtimeLogging } from './runtime-log'; - -/** - * Maintain a unique `docData` object across multiple hydration runs - * to ensure that host ids remain unique. - */ -const docData: d.DocData = { - hostIds: 0, - rootLevelIds: 0, - staticComponents: new Set(), -} as d.DocData; - -export function initializeWindow( - win: MockWindow, - doc: Document, - opts: d.HydrateDocumentOptions, - results: d.HydrateResults, -) { - if (typeof opts.url === 'string') { - try { - win.location.href = opts.url; - } catch (e) {} - } - - if (typeof opts.userAgent === 'string') { - try { - win.navigator.userAgent = opts.userAgent; - } catch (e) {} - } - if (typeof opts.cookie === 'string') { - try { - doc.cookie = opts.cookie; - } catch (e) {} - } - if (typeof opts.referrer === 'string') { - try { - (doc as any).referrer = opts.referrer; - } catch (e) {} - } - if (typeof opts.direction === 'string') { - try { - doc.documentElement.setAttribute('dir', opts.direction); - } catch (e) {} - } - if (typeof opts.language === 'string') { - try { - doc.documentElement.setAttribute('lang', opts.language); - } catch (e) {} - } - if (typeof opts.buildId === 'string') { - try { - doc.documentElement.setAttribute('data-stencil-build', opts.buildId); - } catch (e) {} - } - - try { - // TODO(STENCIL-345) - Evaluate reconciling MockWindow, Window differences - // @ts-ignore - win.customElements = null; - } catch (e) {} - - if (opts.constrainTimeouts) { - constrainTimeouts(win); - } - - runtimeLogging(win, opts, results); - - (doc as d.StencilDocument)[STENCIL_DOC_DATA] = docData; - - return win; -} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 0c5fc8a124a..00000000000 --- a/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './internal/stencil-core'; diff --git a/src/internal/default.ts b/src/internal/default.ts deleted file mode 100644 index c1ea565c2cc..00000000000 --- a/src/internal/default.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@stencil/core/internal/client'; diff --git a/src/internal/index.ts b/src/internal/index.ts deleted file mode 100644 index b53904afce4..00000000000 --- a/src/internal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../declarations'; diff --git a/src/internal/readme.md b/src/internal/readme.md deleted file mode 100644 index 2172b9e1a83..00000000000 --- a/src/internal/readme.md +++ /dev/null @@ -1,13 +0,0 @@ -# @stencil/core/internal - -NOTE!! The `@stencil/core/internal` package is not meant to be consumed directly by anything other than Stencil internals. It is its own package so that it can be resolved by Stencil, but breaking changes can/will happen at any time. This isn't a "use at your own discretion" moment, but it's more of a "never use this because your code will break" fact. - - -## `index.ts` - -This is the main entry file for all of Stencil's internals, such as Stencil's runtime and compiler types. However, any public references to the internals are found in `declarations/stencil-core.ts` and `internal/default.ts`. This file is largely used to generate all of Stencil's internal types. But the transpiled JavaScript from this is not exposed. - - -## `default.ts` - -By default, when Stencil resolves `@stencil/core/internal`, it's going to assume it wants the `client` internals (rather than `hydrate`). So by default, `@stencil/core/internal` actually points to `@stencil/core/internal/client`. diff --git a/src/internal/stencil-core/index.cjs b/src/internal/stencil-core/index.cjs deleted file mode 100644 index a10e1472f5f..00000000000 --- a/src/internal/stencil-core/index.cjs +++ /dev/null @@ -1 +0,0 @@ -exports.h = function () {}; diff --git a/src/internal/stencil-core/index.d.ts b/src/internal/stencil-core/index.d.ts deleted file mode 100644 index 393dfea09a3..00000000000 --- a/src/internal/stencil-core/index.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -export type { StencilConfig as Config, PrerenderConfig } from '../stencil-public-compiler'; -export type { - ChildNode, - ComponentDidLoad, - ComponentDidUpdate, - ComponentInterface, - ComponentOptions, - ComponentWillLoad, - ComponentWillUpdate, - EventEmitter, - EventOptions, - FunctionalComponent, - FunctionalUtilities, - JSX, - ListenOptions, - ListenTargetOptions, - MethodOptions, - ModeStyles, - PropOptions, - QueueApi, - RafCallback, - VNode, - VNodeData, -} from '../stencil-public-runtime'; -export { - AttrDeserialize, - PropSerialize, - AttachInternals, - Build, - Component, - Element, - Env, - Event, - forceUpdate, - Fragment, - getAssetPath, - getElement, - getMode, - getRenderingRef, - h, - Host, - Listen, - Method, - MixedInCtor, - Mixin, - MixinFactory, - Prop, - readTask, - render, - resolveVar, - setAssetPath, - setErrorHandler, - setMode, - setNonce, - setPlatformHelpers, - setTagTransformer, - State, - transformTag, - Watch, - writeTask, -} from '../stencil-public-runtime'; diff --git a/src/internal/stencil-core/index.js b/src/internal/stencil-core/index.js deleted file mode 100644 index b5329806026..00000000000 --- a/src/internal/stencil-core/index.js +++ /dev/null @@ -1,18 +0,0 @@ -export { - Build, - forceUpdate, - getAssetPath, - getElement, - getMode, - getRenderingRef, - h, - Host, - Mixin, - readTask, - render, - setAssetPath, - setErrorHandler, - setMode, - setPlatformHelpers, - writeTask, -} from '../client/index.js'; diff --git a/src/internal/stencil-core/jsx-dev-runtime.cjs b/src/internal/stencil-core/jsx-dev-runtime.cjs deleted file mode 100644 index 399bc3af353..00000000000 --- a/src/internal/stencil-core/jsx-dev-runtime.cjs +++ /dev/null @@ -1,7 +0,0 @@ -// Export automatic JSX development runtime (CommonJS) -// Note: This requires the client platform to be built -const client = require('../client/index.js'); -module.exports = { - jsxDEV: client.jsxDEV, - Fragment: client.Fragment, -}; diff --git a/src/internal/stencil-core/jsx-dev-runtime.d.ts b/src/internal/stencil-core/jsx-dev-runtime.d.ts deleted file mode 100644 index bf187c75422..00000000000 --- a/src/internal/stencil-core/jsx-dev-runtime.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Automatic JSX Development Runtime type definitions for Stencil - * - * This module provides TypeScript type definitions for the automatic JSX development runtime. - * When using "jsx": "react-jsxdev" in tsconfig.json with "jsxImportSource": "@stencil/core", - * TypeScript will automatically import from this module in development mode. - */ - -import type { VNode, JSXBase } from '../stencil-public-runtime'; -import type { JSX as LocalJSX } from '../stencil-public-runtime'; - -export { Fragment } from '../stencil-public-runtime'; - -/** - * JSX development runtime function for creating elements with debug info. - */ -export function jsxDEV( - type: any, - props: any, - key?: string | number, - isStaticChildren?: boolean, - source?: any, - self?: any, -): VNode; - -/** - * JSX namespace for TypeScript's automatic JSX runtime. - * This is required for TypeScript to resolve JSX element types when using - * "jsx": "react-jsxdev" with "jsxImportSource": "@stencil/core". - */ -export namespace JSX { - type BaseElements = LocalJSX.IntrinsicElements & JSXBase.IntrinsicElements; - - export type IntrinsicElements = { - [K in keyof BaseElements]: BaseElements[K] & { children?: any }; - } & { - [tagName: string]: any; - }; - - export type Element = VNode | VNode[] | null; -} diff --git a/src/internal/stencil-core/jsx-dev-runtime.js b/src/internal/stencil-core/jsx-dev-runtime.js deleted file mode 100644 index 8dc3f38047b..00000000000 --- a/src/internal/stencil-core/jsx-dev-runtime.js +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from the client platform (same pattern as index.js) -export { jsxDEV, Fragment } from '../client/index.js'; diff --git a/src/internal/stencil-core/jsx-runtime.cjs b/src/internal/stencil-core/jsx-runtime.cjs deleted file mode 100644 index e5f26a60ff8..00000000000 --- a/src/internal/stencil-core/jsx-runtime.cjs +++ /dev/null @@ -1,8 +0,0 @@ -// Export automatic JSX runtime (CommonJS) -// Note: This requires the client platform to be built -const client = require('../client/index.js'); -module.exports = { - jsx: client.jsx, - jsxs: client.jsxs, - Fragment: client.Fragment, -}; diff --git a/src/internal/stencil-core/jsx-runtime.d.ts b/src/internal/stencil-core/jsx-runtime.d.ts deleted file mode 100644 index d36bb05fb7b..00000000000 --- a/src/internal/stencil-core/jsx-runtime.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Automatic JSX Runtime type definitions for Stencil - * - * This module provides TypeScript type definitions for the automatic JSX runtime. - * When using "jsx": "react-jsx" or "jsx": "react-jsxdev" in tsconfig.json with - * "jsxImportSource": "@stencil/core", TypeScript will automatically import from - * these modules instead of requiring manual `h` imports. - */ - -import type { VNode, JSXBase } from '../stencil-public-runtime'; -import type { JSX as LocalJSX } from '../stencil-public-runtime'; - -export { Fragment } from '../stencil-public-runtime'; - -/** - * JSX runtime function for creating elements in production mode. - */ -export function jsx(type: any, props: any, key?: string): VNode; - -/** - * JSX runtime function for creating elements with static children. - */ -export function jsxs(type: any, props: any, key?: string): VNode; - -/** -+ * JSX namespace for TypeScript's automatic JSX runtime. -+ * This is required for TypeScript to resolve JSX element types when using -+ * "jsx": "react-jsx" with "jsxImportSource": "@stencil/core". -+ */ -export namespace JSX { - type BaseElements = LocalJSX.IntrinsicElements & JSXBase.IntrinsicElements; - - export type IntrinsicElements = { - [K in keyof BaseElements]: BaseElements[K] & { children?: any }; - } & { - [tagName: string]: any; - }; - - export type Element = VNode | VNode[] | null; -} diff --git a/src/internal/stencil-core/jsx-runtime.js b/src/internal/stencil-core/jsx-runtime.js deleted file mode 100644 index 1aaf2dfa5c2..00000000000 --- a/src/internal/stencil-core/jsx-runtime.js +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from the client platform (same pattern as index.js) -export { jsx, jsxs, Fragment } from '../client/index.js'; diff --git a/src/internal/testing/jsx-dev-runtime.d.ts b/src/internal/testing/jsx-dev-runtime.d.ts deleted file mode 100644 index 036669f9d05..00000000000 --- a/src/internal/testing/jsx-dev-runtime.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Type definitions for automatic JSX development runtime in testing -export { jsxDEV, Fragment, JSX } from '../stencil-core/jsx-dev-runtime'; diff --git a/src/internal/testing/jsx-dev-runtime.js b/src/internal/testing/jsx-dev-runtime.js deleted file mode 100644 index 0ed93466313..00000000000 --- a/src/internal/testing/jsx-dev-runtime.js +++ /dev/null @@ -1,8 +0,0 @@ -// Export automatic JSX development runtime for testing -// This file allows TypeScript's automatic JSX transform to work in tests -// when using jsxImportSource: "@stencil/core/internal/testing" with jsx: "react-jsxdev" -const testing = require('./index.js'); -module.exports = { - jsxDEV: testing.jsxDEV, - Fragment: testing.Fragment, -}; diff --git a/src/internal/testing/jsx-runtime.d.ts b/src/internal/testing/jsx-runtime.d.ts deleted file mode 100644 index 6ef93dc8bb4..00000000000 --- a/src/internal/testing/jsx-runtime.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Type definitions for automatic JSX runtime in testing -export { jsx, jsxs, Fragment, JSX } from '../stencil-core/jsx-runtime'; diff --git a/src/internal/testing/jsx-runtime.js b/src/internal/testing/jsx-runtime.js deleted file mode 100644 index 77720c3ccd9..00000000000 --- a/src/internal/testing/jsx-runtime.js +++ /dev/null @@ -1,9 +0,0 @@ -// Export automatic JSX runtime for testing -// This file allows TypeScript's automatic JSX transform to work in tests -// when using jsxImportSource: "@stencil/core/internal/testing" -const testing = require('./index.js'); -module.exports = { - jsx: testing.jsx, - jsxs: testing.jsxs, - Fragment: testing.Fragment, -}; diff --git a/src/mock-doc/attribute.ts b/src/mock-doc/attribute.ts deleted file mode 100644 index a048b9fa131..00000000000 --- a/src/mock-doc/attribute.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { XLINK_NS } from '../runtime/runtime-constants'; - -const attrHandler = { - get(obj: any, prop: string) { - if (prop in obj) { - return obj[prop]; - } - if (typeof prop !== 'symbol' && !isNaN(prop as any)) { - return (obj as MockAttributeMap).__items[prop as any]; - } - return undefined; - }, -}; - -export const createAttributeProxy = (caseInsensitive: boolean) => - new Proxy(new MockAttributeMap(caseInsensitive), attrHandler); - -export class MockAttributeMap { - __items: MockAttr[] = []; - - constructor(public caseInsensitive = false) {} - - get length() { - return this.__items.length; - } - - item(index: number) { - return this.__items[index] || null; - } - - setNamedItem(attr: MockAttr) { - attr.namespaceURI = null; - this.setNamedItemNS(attr); - } - - setNamedItemNS(attr: MockAttr) { - if (attr != null && attr.value != null) { - attr.value = String(attr.value); - } - - const existingAttr = this.__items.find((a) => a.name === attr.name && a.namespaceURI === attr.namespaceURI); - if (existingAttr != null) { - existingAttr.value = attr.value; - } else { - this.__items.push(attr); - } - } - - getNamedItem(attrName: string) { - if (this.caseInsensitive) { - attrName = attrName.toLowerCase(); - } - return this.getNamedItemNS(null, attrName); - } - - getNamedItemNS(namespaceURI: string | null, attrName: string) { - namespaceURI = getNamespaceURI(namespaceURI); - return ( - this.__items.find((attr) => attr.name === attrName && getNamespaceURI(attr.namespaceURI) === namespaceURI) || null - ); - } - - removeNamedItem(attr: MockAttr) { - this.removeNamedItemNS(attr); - } - - removeNamedItemNS(attr: MockAttr) { - for (let i = 0, ii = this.__items.length; i < ii; i++) { - if (this.__items[i].name === attr.name && this.__items[i].namespaceURI === attr.namespaceURI) { - this.__items.splice(i, 1); - break; - } - } - } - - [Symbol.iterator]() { - let i = 0; - - return { - next: () => ({ - done: i === this.length, - value: this.item(i++), - }), - }; - } - - get [Symbol.toStringTag]() { - return 'MockAttributeMap'; - } -} - -function getNamespaceURI(namespaceURI: string | null) { - return namespaceURI === XLINK_NS ? null : namespaceURI; -} - -export function cloneAttributes(srcAttrs: MockAttributeMap, sortByName = false) { - const dstAttrs = new MockAttributeMap(srcAttrs.caseInsensitive); - if (srcAttrs != null) { - const attrLen = srcAttrs.length; - - if (sortByName && attrLen > 1) { - const sortedAttrs: MockAttr[] = []; - for (let i = 0; i < attrLen; i++) { - const srcAttr = srcAttrs.item(i); - const dstAttr = new MockAttr(srcAttr.name, srcAttr.value, srcAttr.namespaceURI); - sortedAttrs.push(dstAttr); - } - - sortedAttrs.sort(sortAttributes).forEach((attr) => { - dstAttrs.setNamedItemNS(attr); - }); - } else { - for (let i = 0; i < attrLen; i++) { - const srcAttr = srcAttrs.item(i); - const dstAttr = new MockAttr(srcAttr.name, srcAttr.value, srcAttr.namespaceURI); - dstAttrs.setNamedItemNS(dstAttr); - } - } - } - return dstAttrs; -} - -function sortAttributes(a: MockAttr, b: MockAttr) { - if (a.name < b.name) return -1; - if (a.name > b.name) return 1; - return 0; -} - -export class MockAttr { - private _name: string; - private _value: string; - private _namespaceURI: string | null; - - constructor(attrName: string, attrValue: string, namespaceURI: string | null = null) { - this._name = attrName; - this._value = String(attrValue); - this._namespaceURI = namespaceURI; - } - - get name() { - return this._name; - } - set name(value) { - this._name = value; - } - - get value() { - return this._value; - } - set value(value) { - this._value = String(value); - } - - get nodeName() { - return this._name; - } - set nodeName(value) { - this._name = value; - } - - get nodeValue() { - return this._value; - } - set nodeValue(value) { - this._value = String(value); - } - - get namespaceURI() { - return this._namespaceURI; - } - set namespaceURI(namespaceURI) { - this._namespaceURI = namespaceURI; - } -} diff --git a/src/mock-doc/constants.ts b/src/mock-doc/constants.ts deleted file mode 100644 index fd62a40c24f..00000000000 --- a/src/mock-doc/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const enum NODE_TYPES { - ELEMENT_NODE = 1, - ATTRIBUTE_NODE = 2, - TEXT_NODE = 3, - CDATA_SECTION_NODE = 4, - ENTITY_REFERENCE_NODE = 5, - ENTITY_NODE = 6, - PROCESSING_INSTRUCTION_NODE = 7, - COMMENT_NODE = 8, - DOCUMENT_NODE = 9, - DOCUMENT_TYPE_NODE = 10, - DOCUMENT_FRAGMENT_NODE = 11, - NOTATION_NODE = 12, -} - -export const enum NODE_NAMES { - COMMENT_NODE = '#comment', - DOCUMENT_NODE = '#document', - DOCUMENT_FRAGMENT_NODE = '#document-fragment', - TEXT_NODE = '#text', -} diff --git a/src/mock-doc/element.ts b/src/mock-doc/element.ts deleted file mode 100644 index d99ef0d242b..00000000000 --- a/src/mock-doc/element.ts +++ /dev/null @@ -1,805 +0,0 @@ -import { cloneAttributes } from './attribute'; -import { NODE_TYPES } from './constants'; -import { getStyleElementText, MockCSSStyleSheet, setStyleElementText } from './css-style-sheet'; -import { createCustomElement } from './custom-element-registry'; -import { MockDocumentFragment } from './document-fragment'; -import { MockElement, MockHTMLElement, MockNode } from './node'; - -export function createElement(ownerDocument: any, tagName: string): any { - if (typeof tagName !== 'string' || tagName === '' || !/^[a-z0-9-_:]+$/i.test(tagName)) { - throw new Error(`The tag name provided (${tagName}) is not a valid name.`); - } - tagName = tagName.toLowerCase(); - - switch (tagName) { - case 'a': - return new MockAnchorElement(ownerDocument); - - case 'base': - return new MockBaseElement(ownerDocument); - - case 'button': - return new MockButtonElement(ownerDocument); - - case 'canvas': - return new MockCanvasElement(ownerDocument); - - case 'form': - return new MockFormElement(ownerDocument); - - case 'img': - return new MockImageElement(ownerDocument); - - case 'input': - return new MockInputElement(ownerDocument); - - case 'label': - return new MockLabelElement(ownerDocument); - - case 'link': - return new MockLinkElement(ownerDocument); - - case 'meta': - return new MockMetaElement(ownerDocument); - - case 'script': - return new MockScriptElement(ownerDocument); - - case 'slot': - return new MockSlotElement(ownerDocument); - - case 'slot-fb': - return new MockHTMLElement(ownerDocument, tagName); - - case 'style': - return new MockStyleElement(ownerDocument); - - case 'template': - return new MockTemplateElement(ownerDocument); - - case 'title': - return new MockTitleElement(ownerDocument); - - case 'ul': - return new MockUListElement(ownerDocument); - } - - if (ownerDocument != null && tagName.includes('-')) { - const win = ownerDocument.defaultView; - if (win != null && win.customElements != null) { - return createCustomElement(win.customElements, ownerDocument, tagName); - } - } - - return new MockHTMLElement(ownerDocument, tagName); -} - -export function createElementNS(ownerDocument: any, namespaceURI: string, tagName: string) { - if (namespaceURI === 'http://www.w3.org/1999/xhtml') { - return createElement(ownerDocument, tagName); - } else if (namespaceURI === 'http://www.w3.org/2000/svg') { - switch (tagName.toLowerCase()) { - case 'text': - case 'tspan': - case 'tref': - case 'altglyph': - case 'textpath': - return new MockSVGTextContentElement(ownerDocument, tagName); - case 'circle': - case 'ellipse': - case 'image': - case 'line': - case 'path': - case 'polygon': - case 'polyline': - case 'rect': - case 'use': - return new MockSVGGraphicsElement(ownerDocument, tagName); - case 'svg': - return new MockSVGSVGElement(ownerDocument, tagName); - default: - return new MockSVGElement(ownerDocument, tagName); - } - } else { - return new MockElement(ownerDocument, tagName, namespaceURI); - } -} - -export class MockAnchorElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'a'); - } - - get href() { - return fullUrl(this, 'href'); - } - set href(value: string) { - this.setAttribute('href', value); - } - get pathname() { - if (!this.href) { - return ''; - } - return new URL(this.href).pathname; - } -} - -export class MockButtonElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'button'); - } - - get labels() { - return getLabelsForElement(this); - } -} -patchPropAttributes( - MockButtonElement.prototype, - { - type: String, - }, - { - type: 'submit', - }, -); - -Object.defineProperty(MockButtonElement.prototype, 'form', { - get(this: MockElement) { - return this.hasAttribute('form') ? this.getAttribute('form') : null; - }, -}); - -export class MockImageElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'img'); - } - - override get draggable() { - return this.getAttributeNS(null, 'draggable') !== 'false'; - } - override set draggable(value: boolean) { - this.setAttributeNS(null, 'draggable', value); - } - - get src() { - return fullUrl(this, 'src'); - } - set src(value: string) { - this.setAttribute('src', value); - } -} -patchPropAttributes(MockImageElement.prototype, { - height: Number, - width: Number, -}); - -export class MockInputElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'input'); - } - - get list() { - const listId = this.getAttribute('list'); - if (listId) { - return (this.ownerDocument as Document).getElementById(listId); - } - return null; - } - - get labels() { - return getLabelsForElement(this); - } -} - -patchPropAttributes( - MockInputElement.prototype, - { - accept: String, - autocomplete: String, - autofocus: Boolean, - capture: String, - checked: Boolean, - disabled: Boolean, - form: String, - formaction: String, - formenctype: String, - formmethod: String, - formnovalidate: String, - formtarget: String, - height: Number, - inputmode: String, - max: String, - maxLength: Number, - min: String, - minLength: Number, - multiple: Boolean, - name: String, - pattern: String, - placeholder: String, - required: Boolean, - readOnly: Boolean, - size: Number, - spellCheck: Boolean, - src: String, - step: String, - type: String, - value: String, - width: Number, - }, - { - type: 'text', - }, -); - -export class MockFormElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'form'); - } -} -patchPropAttributes(MockFormElement.prototype, { - name: String, -}); - -export class MockLabelElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'label'); - } - - get htmlFor() { - return this.getAttributeNS(null, 'for') || ''; - } - set htmlFor(value: string) { - this.setAttributeNS(null, 'for', value); - } - - get control(): MockHTMLElement | null { - const forAttr = this.htmlFor; - if (forAttr) { - // Label references an element by ID via the "for" attribute - return this.ownerDocument?.getElementById(forAttr) ?? null; - } - // If no "for" attribute, look for the first labelable descendant - const labelableSelector = 'button, input:not([type="hidden"]), meter, output, progress, select, textarea'; - return this.querySelector(labelableSelector); - } -} - -export class MockLinkElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'link'); - } - - get href() { - return fullUrl(this, 'href'); - } - set href(value: string) { - this.setAttribute('href', value); - } -} -patchPropAttributes(MockLinkElement.prototype, { - crossorigin: String, - media: String, - rel: String, - type: String, -}); - -export class MockMetaElement extends MockHTMLElement { - content: string; - - constructor(ownerDocument: any) { - super(ownerDocument, 'meta'); - } -} -patchPropAttributes(MockMetaElement.prototype, { - charset: String, - content: String, - name: String, -}); - -export class MockScriptElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'script'); - } - - get src() { - return fullUrl(this, 'src'); - } - set src(value: string) { - this.setAttribute('src', value); - } -} -patchPropAttributes(MockScriptElement.prototype, { - type: String, -}); - -export class MockDOMMatrix { - static fromMatrix() { - return new MockDOMMatrix(); - } - a: number = 1; - b: number = 0; - c: number = 0; - d: number = 1; - e: number = 0; - f: number = 0; - m11: number = 1; - m12: number = 0; - m13: number = 0; - m14: number = 0; - m21: number = 0; - m22: number = 1; - m23: number = 0; - m24: number = 0; - m31: number = 0; - m32: number = 0; - m33: number = 1; - m34: number = 0; - m41: number = 0; - m42: number = 0; - m43: number = 0; - m44: number = 1; - is2D: boolean = true; - isIdentity: boolean = true; - inverse() { - return new MockDOMMatrix(); - } - flipX() { - return new MockDOMMatrix(); - } - flipY() { - return new MockDOMMatrix(); - } - multiply() { - return new MockDOMMatrix(); - } - rotate() { - return new MockDOMMatrix(); - } - rotateAxisAngle() { - return new MockDOMMatrix(); - } - rotateFromVector() { - return new MockDOMMatrix(); - } - scale() { - return new MockDOMMatrix(); - } - scaleNonUniform() { - return new MockDOMMatrix(); - } - skewX() { - return new MockDOMMatrix(); - } - skewY() { - return new MockDOMMatrix(); - } - toJSON() {} - toString() {} - transformPoint() { - return new MockDOMPoint(); - } - translate() { - return new MockDOMMatrix(); - } -} - -export class MockDOMPoint { - w: number = 1; - x: number = 0; - y: number = 0; - z: number = 0; - toJSON() {} - matrixTransform() { - return new MockDOMMatrix(); - } -} - -export class MockSVGRect { - height: number = 10; - width: number = 10; - x: number = 0; - y: number = 0; -} - -export class MockStyleElement extends MockHTMLElement { - sheet: MockCSSStyleSheet; - - constructor(ownerDocument: any) { - super(ownerDocument, 'style'); - this.sheet = new MockCSSStyleSheet(this); - } - - override get innerHTML() { - return getStyleElementText(this); - } - override set innerHTML(value: string) { - setStyleElementText(this, value); - } - - override get innerText() { - return getStyleElementText(this); - } - override set innerText(value: string) { - setStyleElementText(this, value); - } - - override get textContent() { - return getStyleElementText(this); - } - override set textContent(value: string) { - setStyleElementText(this, value); - } -} -export class MockSVGElement extends MockElement { - override __namespaceURI = 'http://www.w3.org/2000/svg'; - - // SVGElement properties and methods - get ownerSVGElement(): SVGSVGElement { - return null; - } - get viewportElement(): SVGElement { - return null; - } - onunload() { - /**/ - } - - // SVGGeometryElement properties and methods - get pathLength(): number { - return 0; - } - - isPointInFill(_pt: DOMPoint): boolean { - return false; - } - isPointInStroke(_pt: DOMPoint): boolean { - return false; - } - getTotalLength(): number { - return 0; - } -} - -export class MockSVGGraphicsElement extends MockSVGElement { - getBBox(_options?: { clipped: boolean; fill: boolean; markers: boolean; stroke: boolean }): MockSVGRect { - return new MockSVGRect(); - } - getCTM(): MockDOMMatrix { - return new MockDOMMatrix(); - } - getScreenCTM(): MockDOMMatrix { - return new MockDOMMatrix(); - } -} - -export class MockSVGSVGElement extends MockSVGGraphicsElement { - createSVGPoint(): MockDOMPoint { - return new MockDOMPoint(); - } -} - -export class MockSVGTextContentElement extends MockSVGGraphicsElement { - getComputedTextLength(): number { - return 0; - } -} - -export class MockBaseElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'base'); - } - - get href() { - return fullUrl(this, 'href'); - } - set href(value: string) { - this.setAttribute('href', value); - } -} - -export class MockTemplateElement extends MockHTMLElement { - content: MockDocumentFragment; - - constructor(ownerDocument: any) { - super(ownerDocument, 'template'); - this.content = new MockDocumentFragment(ownerDocument); - } - - override get innerHTML() { - return this.content.innerHTML; - } - override set innerHTML(html: string) { - this.content.innerHTML = html; - } - - override cloneNode(deep?: boolean) { - const cloned = new MockTemplateElement(null); - cloned.attributes = cloneAttributes(this.attributes); - - const styleCssText = this.getAttribute('style'); - if (styleCssText != null && styleCssText.length > 0) { - cloned.setAttribute('style', styleCssText); - } - - cloned.content = this.content.cloneNode(deep); - - if (deep) { - for (let i = 0, ii = this.childNodes.length; i < ii; i++) { - const clonedChildNode = this.childNodes[i].cloneNode(true); - cloned.appendChild(clonedChildNode); - } - } - - return cloned; - } -} - -export class MockTitleElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'title'); - } - - get text() { - return this.textContent; - } - set text(value: string) { - this.textContent = value; - } -} - -export class MockUListElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'ul'); - } -} - -export class MockSlotElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'slot'); - } - - assignedNodes(opts?: { flatten: boolean }): (MockNode | Node)[] { - let nodesToReturn: (MockNode | Node)[] = []; - - const ownerHost = (this.getRootNode() as any).host as MockElement; - if (!ownerHost) return nodesToReturn; - - if (ownerHost.childNodes.length) { - // try to find lightDOM nodes matching this slot's name (or lack of) - if ((this as any).name) { - nodesToReturn = ownerHost.childNodes.filter( - (n) => - n.nodeType === NODE_TYPES.ELEMENT_NODE && (n as MockElement).getAttribute('slot') === (this as any).name, - ); - } else { - // find elements that do not have a slot attribute or - // any other type of node - nodesToReturn = ownerHost.childNodes.filter( - (n) => - (n.nodeType === NODE_TYPES.ELEMENT_NODE && !(n as MockElement).getAttribute('slot')) || - n.nodeType !== NODE_TYPES.ELEMENT_NODE, - ); - } - if (nodesToReturn.length) return nodesToReturn; - } - - // no flatten option? Return whatever's in this slot (without nested slots) - if (!opts?.flatten) return this.childNodes.filter((n) => !(n instanceof MockSlotElement)); - - // flatten option? Return all nodes in this slot (including anything within nested slots) - return this.childNodes.reduce( - (acc, node) => { - if (node instanceof MockSlotElement) { - acc.push(...node.assignedNodes(opts)); - } else { - acc.push(node); - } - return acc; - }, - [] as (MockNode | Node)[], - ); - } - - assignedElements(opts?: { flatten: boolean }): (Element | MockHTMLElement)[] { - let elesToReturn: (Element | MockHTMLElement)[] = []; - - const ownerHost = (this.getRootNode() as any).host as MockElement; - if (!ownerHost) return elesToReturn; - - if (ownerHost.children.length) { - // try to find lightDOM elements matching this slot's name (or lack of) - if ((this as any).name) { - elesToReturn = ownerHost.children.filter((n) => (n as MockElement).getAttribute('slot') == (this as any).name); - } else { - elesToReturn = ownerHost.children.filter((n) => !(n as MockElement).getAttribute('slot')); - } - if (elesToReturn.length) return elesToReturn; - } - - // no flatten option? Return whatever elements are in this slot (without nested slots) - if (!opts?.flatten) return this.children.filter((n) => !(n instanceof MockSlotElement)); - - // flatten option? Return all elements in this slot (including anything within nested slots) - return this.children.reduce( - (acc, node) => { - if (node instanceof MockSlotElement) { - acc.push(...node.assignedElements(opts)); - } else { - acc.push(node); - } - return acc; - }, - [] as (MockElement | Element)[], - ); - } -} - -patchPropAttributes(MockSlotElement.prototype, { - name: String, -}); - -type CanvasContext = '2d' | 'webgl' | 'webgl2' | 'bitmaprenderer'; -export class CanvasRenderingContext { - context: CanvasContext; - contextAttributes: WebGLContextAttributes; - constructor(context: CanvasContext, contextAttributes?: WebGLContextAttributes) { - this.context = context; - this.contextAttributes = contextAttributes; - } - fillRect() { - return; - } - clearRect() {} - getImageData(_: number, __: number, w: number, h: number) { - return { - data: new Array(w * h * 4), - }; - } - toDataURL() { - return 'data:,'; // blank image - } - putImageData() {} - createImageData(): ImageData { - return {} as ImageData; - } - setTransform() {} - drawImage() {} - save() {} - fillText() {} - restore() {} - beginPath() {} - moveTo() {} - lineTo() {} - closePath() {} - stroke() {} - translate() {} - scale() {} - rotate() {} - arc() {} - fill() {} - measureText() { - return { width: 0 }; - } - transform() {} - rect() {} - clip() {} -} - -export class MockCanvasElement extends MockHTMLElement { - constructor(ownerDocument: any) { - super(ownerDocument, 'canvas'); - } - getContext(context: CanvasContext, contextAttributes?: WebGLContextAttributes): CanvasRenderingContext { - return new CanvasRenderingContext(context, contextAttributes); - } -} - -function fullUrl(elm: MockElement, attrName: string) { - const val = elm.getAttribute(attrName) || ''; - if (elm.ownerDocument != null) { - const win = elm.ownerDocument.defaultView as Window; - if (win != null) { - const loc = win.location; - if (loc != null) { - try { - const url = new URL(val, loc.href); - return url.href; - } catch (e) {} - } - } - } - return val.replace(/\'|\"/g, '').trim(); -} - -function getLabelsForElement(elm: MockHTMLElement): MockHTMLElement[] { - const labels: MockHTMLElement[] = []; - const id = elm.id; - const doc = elm.ownerDocument; - - if (doc) { - // Find labels with "for" attribute matching this element's ID - if (id) { - const allLabels = doc.getElementsByTagName('label'); - for (let i = 0; i < allLabels.length; i++) { - const label = allLabels[i] as MockLabelElement; - if (label.htmlFor === id) { - labels.push(label); - } - } - } - - // Find labels that contain this element as a descendant - let parent = elm.parentNode as MockHTMLElement | null; - while (parent) { - if (parent.nodeName === 'LABEL' && !labels.includes(parent)) { - labels.push(parent); - } - parent = parent.parentNode as MockHTMLElement | null; - } - } - - return labels; -} - -function patchPropAttributes(prototype: any, attrs: any, defaults: any = {}) { - Object.keys(attrs).forEach((propName) => { - const attr = attrs[propName]; - const defaultValue = defaults[propName]; - - if (attr === Boolean) { - Object.defineProperty(prototype, propName, { - get(this: MockElement) { - return this.hasAttribute(propName); - }, - set(this: MockElement, value: boolean) { - if (value) { - this.setAttribute(propName, ''); - } else { - this.removeAttribute(propName); - } - }, - }); - } else if (attr === Number) { - Object.defineProperty(prototype, propName, { - get(this: MockElement) { - const value = this.getAttribute(propName); - return value ? parseInt(value, 10) : defaultValue === undefined ? 0 : defaultValue; - }, - set(this: MockElement, value: boolean) { - this.setAttribute(propName, value); - }, - }); - } else { - Object.defineProperty(prototype, propName, { - get(this: MockElement) { - return this.hasAttribute(propName) ? this.getAttribute(propName) : defaultValue || ''; - }, - set(this: MockElement, value: boolean) { - this.setAttribute(propName, value); - }, - }); - } - }); -} - -MockElement.prototype.cloneNode = function (this: MockElement, deep?: boolean) { - // because we're creating elements, which extending specific HTML base classes there - // is a MockElement circular reference that bundling has trouble dealing with so - // the fix is to add cloneNode() to MockElement's prototype after the HTML classes - const cloned = createElement(this.ownerDocument, this.nodeName); - cloned.attributes = cloneAttributes(this.attributes); - - const styleCssText = this.getAttribute('style'); - if (styleCssText != null && styleCssText.length > 0) { - cloned.setAttribute('style', styleCssText); - } - - if (deep) { - for (let i = 0, ii = this.childNodes.length; i < ii; i++) { - const clonedChildNode = this.childNodes[i].cloneNode(true); - cloned.appendChild(clonedChildNode); - } - } - - return cloned; -}; diff --git a/src/mock-doc/global.ts b/src/mock-doc/global.ts deleted file mode 100644 index 605348cf2c0..00000000000 --- a/src/mock-doc/global.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { MockCSSStyleSheet } from './css-style-sheet'; -import { MockDocumentFragment } from './document-fragment'; -import { - MockAnchorElement, - MockBaseElement, - MockButtonElement, - MockCanvasElement, - MockFormElement, - MockImageElement, - MockInputElement, - MockLinkElement, - MockMetaElement, - MockScriptElement, - MockStyleElement, - MockTemplateElement, - MockTitleElement, - MockUListElement, -} from './element'; -import { MockCustomEvent, MockEvent, MockFocusEvent, MockKeyboardEvent, MockMouseEvent } from './event'; -import { MockHeaders } from './headers'; -import { MockDOMParser } from './parser'; -import { MockRequest, MockResponse } from './request-response'; -import { MockWindow } from './window'; - -export function setupGlobal(gbl: any) { - if (gbl.window == null) { - const win: any = (gbl.window = new MockWindow()); - - WINDOW_FUNCTIONS.forEach((fnName) => { - if (!(fnName in gbl)) { - gbl[fnName] = win[fnName].bind(win); - } - }); - - WINDOW_PROPS.forEach((propName) => { - if (!(propName in gbl)) { - Object.defineProperty(gbl, propName, { - get() { - return win[propName]; - }, - set(val: any) { - win[propName] = val; - }, - configurable: true, - enumerable: true, - }); - } - }); - - GLOBAL_CONSTRUCTORS.forEach(([cstrName]) => { - gbl[cstrName] = win[cstrName]; - }); - } - - return gbl.window; -} - -export function teardownGlobal(gbl: any) { - const win = gbl.window as Window; - if (win && typeof win.close === 'function') { - win.close(); - } -} - -export function patchWindow(winToBePatched: any) { - const mockWin: any = new MockWindow(false); - - WINDOW_FUNCTIONS.forEach((fnName) => { - if (typeof winToBePatched[fnName] !== 'function') { - winToBePatched[fnName] = mockWin[fnName].bind(mockWin); - } - }); - - WINDOW_PROPS.forEach((propName) => { - if (winToBePatched === undefined) { - Object.defineProperty(winToBePatched, propName, { - get() { - return mockWin[propName]; - }, - set(val: any) { - mockWin[propName] = val; - }, - configurable: true, - enumerable: true, - }); - } - }); -} - -export function addGlobalsToWindowPrototype(mockWinPrototype: any) { - GLOBAL_CONSTRUCTORS.forEach(([cstrName, Cstr]) => { - Object.defineProperty(mockWinPrototype, cstrName, { - get() { - return this['__' + cstrName] || Cstr; - }, - set(cstr: any) { - this['__' + cstrName] = cstr; - }, - configurable: true, - enumerable: true, - }); - }); -} - -const WINDOW_FUNCTIONS = [ - 'addEventListener', - 'alert', - 'blur', - 'cancelAnimationFrame', - 'cancelIdleCallback', - 'clearInterval', - 'clearTimeout', - 'close', - 'confirm', - 'dispatchEvent', - 'focus', - 'getComputedStyle', - 'matchMedia', - 'open', - 'prompt', - 'removeEventListener', - 'requestAnimationFrame', - 'requestIdleCallback', - 'URL', -]; - -const WINDOW_PROPS = [ - 'customElements', - 'devicePixelRatio', - 'document', - 'history', - 'innerHeight', - 'innerWidth', - 'localStorage', - 'location', - 'navigator', - 'pageXOffset', - 'pageYOffset', - 'performance', - 'screenLeft', - 'screenTop', - 'screenX', - 'screenY', - 'scrollX', - 'scrollY', - 'sessionStorage', - 'CSS', - 'CustomEvent', - 'Event', - 'Element', - 'HTMLElement', - 'Node', - 'NodeList', - 'FocusEvent', - 'KeyboardEvent', - 'MouseEvent', - 'CSSStyleSheet', -]; - -const GLOBAL_CONSTRUCTORS: [string, any][] = [ - ['CSSStyleSheet', MockCSSStyleSheet], - ['CustomEvent', MockCustomEvent], - ['DocumentFragment', MockDocumentFragment], - ['DOMParser', MockDOMParser], - ['Event', MockEvent], - ['FocusEvent', MockFocusEvent], - ['Headers', MockHeaders], - ['KeyboardEvent', MockKeyboardEvent], - ['MouseEvent', MockMouseEvent], - ['Request', MockRequest], - ['Response', MockResponse], - ['ShadowRoot', MockDocumentFragment], - - ['HTMLAnchorElement', MockAnchorElement], - ['HTMLBaseElement', MockBaseElement], - ['HTMLButtonElement', MockButtonElement], - ['HTMLCanvasElement', MockCanvasElement], - ['HTMLFormElement', MockFormElement], - ['HTMLImageElement', MockImageElement], - ['HTMLInputElement', MockInputElement], - ['HTMLLinkElement', MockLinkElement], - ['HTMLMetaElement', MockMetaElement], - ['HTMLScriptElement', MockScriptElement], - ['HTMLStyleElement', MockStyleElement], - ['HTMLTemplateElement', MockTemplateElement], - ['HTMLTitleElement', MockTitleElement], - ['HTMLUListElement', MockUListElement], -]; diff --git a/src/mock-doc/index.ts b/src/mock-doc/index.ts deleted file mode 100644 index f911dafc7ef..00000000000 --- a/src/mock-doc/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { cloneAttributes, MockAttr, MockAttributeMap } from './attribute'; -export { MockComment } from './comment-node'; -export { NODE_TYPES } from './constants'; -export { createDocument, createFragment, MockDocument, resetDocument } from './document'; -export { MockCustomEvent, MockKeyboardEvent, MockMouseEvent } from './event'; -export { patchWindow, setupGlobal, teardownGlobal } from './global'; -export { MockHeaders } from './headers'; -export { MockElement, MockHTMLElement, MockNode, MockTextNode } from './node'; -export { parseHtmlToDocument, parseHtmlToFragment } from './parse-html'; -export { MockRequest, MockRequestInfo, MockRequestInit, MockResponse, MockResponseInit } from './request-response'; -export { serializeNodeToHtml, SerializeNodeToHtmlOptions } from './serialize-node'; -export { cloneDocument, cloneWindow, constrainTimeouts, MockWindow } from './window'; diff --git a/src/mock-doc/selector.ts b/src/mock-doc/selector.ts deleted file mode 100644 index 9bde4810bba..00000000000 --- a/src/mock-doc/selector.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { MockElement } from './node'; -import jQuery from './third-party/jquery'; - -/** - * Check whether an element of interest matches a given selector. - * - * @param selector the selector of interest - * @param elm an element within which to find matching elements - * @returns whether the element matches the selector - */ -export function matches(selector: string, elm: MockElement): boolean { - try { - const r = jQuery.find(selector, undefined, undefined, [elm]); - return r.length > 0; - } catch (e) { - updateSelectorError(selector, e); - throw e; - } -} - -/** - * Select the first element that matches a given selector - * - * @param selector the selector of interest - * @param elm the element within which to find a matching element - * @returns the first matching element, or null if none is found - */ -export function selectOne(selector: string, elm: MockElement) { - try { - const r = jQuery.find(selector, elm, undefined, undefined); - return r[0] || null; - } catch (e) { - updateSelectorError(selector, e); - throw e; - } -} - -/** - * Select all elements that match a given selector - * - * @param selector the selector of interest - * @param elm an element within which to find matching elements - * @returns all matching elements - */ -export function selectAll(selector: string, elm: MockElement): any { - try { - return jQuery.find(selector, elm, undefined, undefined); - } catch (e) { - updateSelectorError(selector, e); - throw e; - } -} - -/** - * A manifest of selectors which are known to be problematic in jQuery. See - * here to track implementation and support: - * https://github.com/jquery/jquery/issues/5111 - */ -export const PROBLEMATIC_SELECTORS = [':scope', ':where', ':is'] as const; - -/** - * Given a selector and an error object thrown by jQuery, annotate the - * error's message to add some context as to the probable reason for the error. - * In particular, if the selector includes a selector which is known to be - * unsupported in jQuery, then we know that was likely the cause of the - * error. - * - * @param selector our selector of interest - * @param e an error object that was thrown in the course of using jQuery - */ -function updateSelectorError(selector: string, e: unknown) { - const selectorsPresent = PROBLEMATIC_SELECTORS.filter((s) => selector.includes(s)); - - if (selectorsPresent.length > 0 && (e as Error).message) { - (e as Error).message = - `At present jQuery does not support the ${humanReadableList(selectorsPresent)} ${selectorsPresent.length === 1 ? 'selector' : 'selectors'}. -If you need this in your test, consider writing an end-to-end test instead.\n` + (e as Error).message; - } -} - -/** - * Format a list of strings in a 'human readable' way. - * - * - If one string (['string']), return 'string' - * - If two strings (['a', 'b']), return 'a and b' - * - If three or more (['a', 'b', 'c']), return 'a, b and c' - * - * @param items a list of strings to format - * @returns a formatted string - */ -function humanReadableList(items: string[]): string { - if (items.length <= 1) { - return items.join(''); - } - return `${items.slice(0, items.length - 1).join(', ')} and ${items[items.length - 1]}`; -} diff --git a/src/mock-doc/shadow-root.ts b/src/mock-doc/shadow-root.ts deleted file mode 100644 index 9b9cb80572d..00000000000 --- a/src/mock-doc/shadow-root.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { MockDocumentFragment } from './document-fragment'; - -export class MockShadowRoot extends MockDocumentFragment { - get activeElement(): HTMLElement | null { - return null; - } - - get cloneable(): boolean { - return false; - } - - get delegatesFocus(): boolean { - return false; - } - - get fullscreenElement(): HTMLElement | null { - return null; - } - - get host(): HTMLElement | null { - let parent = this.parentElement(); - while (parent) { - if (parent.nodeType === 11) { - return parent; - } - parent = parent.parentElement(); - } - return null; - } - - get mode(): 'open' | 'closed' { - return 'open'; - } - - get pictureInPictureElement(): HTMLElement | null { - return null; - } - - get pointerLockElement(): HTMLElement | null { - return null; - } - - get serializable(): boolean { - return false; - } - - get slotAssignment(): 'named' | 'manual' { - return 'named'; - } - - get styleSheets(): StyleSheet[] { - return []; - } -} diff --git a/src/mock-doc/test/clone.spec.ts b/src/mock-doc/test/clone.spec.ts deleted file mode 100644 index 233413ccae4..00000000000 --- a/src/mock-doc/test/clone.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createDocument, MockDocument } from '../document'; -import { cloneDocument } from '../window'; - -describe('cloneNode', () => { - let doc: MockDocument; - beforeEach(() => { - doc = new MockDocument(); - }); - - it('style', () => { - const elm = doc.createElement('div'); - elm.setAttribute('style', 'color: red;'); - - const cloned = elm.cloneNode(true); - expect(cloned.getAttribute('style')).toEqual(`color: red;`); - }); - - it('id', () => { - const elm = doc.createElement('div'); - elm.setAttribute('id', 'value'); - - const cloned = elm.cloneNode(true); - expect(cloned.getAttribute('id')).toEqual(`value`); - }); - - it('div', () => { - const doc = createDocument(` -
    - content -
    - `); - - const cloned = cloneDocument(doc); - const clonedDiv = cloned.querySelector('div'); - - expect(clonedDiv.innerHTML.trim()).toEqual(`content`); - }); - - it('template', () => { - const doc = createDocument(` - - `); - - const cloned = cloneDocument(doc); - const clonedTemplate = cloned.querySelector('template'); - - expect(clonedTemplate.innerHTML.trim()).toEqual(`content`); - expect(clonedTemplate.content.firstChild.textContent.trim()).toEqual(`content`); - }); -}); diff --git a/src/mock-doc/test/selector.spec.ts b/src/mock-doc/test/selector.spec.ts deleted file mode 100644 index 4fde447d0f0..00000000000 --- a/src/mock-doc/test/selector.spec.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { MockDocument } from '../document'; -import { MockElement } from '../node'; -import { PROBLEMATIC_SELECTORS } from '../selector'; - -describe('selector', () => { - it('closest', () => { - const doc = new MockDocument(` -
    - -

    -
    -
    - `); - - const p = doc.querySelector('p'); - const div = doc.querySelector('div'); - expect(p.closest('div')).toBe(div); - }); - - it('no closest', () => { - const doc = new MockDocument(` -
    - -

    -
    -
    - `); - - const p = doc.querySelector('p'); - expect(p.closest('div#my-id')).toBe(null); - }); - - it('matches, tag/class/id', () => { - const elm = new MockElement(null, 'h1'); - elm.classList.add('my-class'); - elm.id = 'my-id'; - expect(elm.matches('h1.my-class#my-id')).toBe(true); - }); - - it('no matches, tag/class/id', () => { - const elm = new MockElement(null, 'h1'); - expect(elm.matches('h1.my-class#my-id')).toBe(false); - }); - - it('matches, tag', () => { - const elm = new MockElement(null, 'h1'); - expect(elm.matches('h1')).toBe(true); - }); - - it('no matches, tag', () => { - const elm = new MockElement(null, 'h1'); - expect(elm.matches('div')).toBe(false); - }); - - it('not find input.checked.a.b', () => { - const doc = new MockDocument(` - - `); - - const checkbox = doc.querySelector('input.checked.a.b'); - expect(checkbox).toBe(null); - }); - - it('find input.checked', () => { - const doc = new MockDocument(` - - `); - - const checkbox = doc.querySelector('input.checked'); - expect(checkbox.id).toBe('checkbox'); - }); - - it('find input[checked=true][disabled]', () => { - const doc = new MockDocument(` - - `); - - const checkbox = doc.querySelector('input[checked=true][disabled]'); - expect(checkbox.id).toBe('checkbox'); - }); - - it('find input[checked=true]', () => { - const doc = new MockDocument(` - - `); - - const checkbox = doc.querySelector('input[checked=true]'); - expect(checkbox.id).toBe('checkbox'); - }); - - it('find input[checked]', () => { - const doc = new MockDocument(` - - `); - - const checkbox = doc.querySelector('input[checked]'); - expect(checkbox.id).toBe('checkbox'); - }); - - it('find all tag names', () => { - const doc = new MockDocument(` -
    1
    - - `); - - const elms = doc.querySelectorAll('a,div,nav'); - expect(elms.length).toBe(2); - }); - - it('find first tag name', () => { - const doc = new MockDocument(` -
    1
    - - `); - - const div = doc.querySelector('a,div,nav'); - expect(div.outerHTML).toBe('
    1
    '); - }); - - it('find one tag name', () => { - const doc = new MockDocument(` -
    1
    - - `); - - const div = doc.querySelector('div'); - expect(div.outerHTML).toBe('
    1
    '); - - const nav = doc.querySelector('nav'); - expect(nav.outerHTML).toBe(''); - }); - - it('finds child', () => { - const doc = new MockDocument(` -
    - -
    - `); - - const span = doc.querySelector('div > span'); - expect(span.outerHTML).toBe(''); - }); - - it('finds child if multiple children', () => { - const doc = new MockDocument(` -
    - - -
    - `); - - const span = doc.querySelector('div > span'); - expect(span.outerHTML).toBe(''); - }); - - it('finds child if multiple selectors', () => { - const doc = new MockDocument(` -
    - - -
    -
    -
    -
    - `); - - const span = doc.querySelector('div > span > .inner'); - expect(span.outerHTML).toBe('
    '); - }); - - it('not find child if does not exist', () => { - const doc = new MockDocument(` -
    - - -
    -
    -
    -
    - `); - - const span = doc.querySelector('div > span > .none'); - expect(span).toBeFalsy(); - }); - - it(':not()', () => { - const doc = new MockDocument(` - - - - `); - const q1 = doc.querySelector('a:not([nope]) b'); - expect(q1).toBe(null); - }); - - it('descendent, two deep', () => { - const doc = new MockDocument(); - const div = doc.createElement('div'); - const span = doc.createElement('span'); - span.classList.add('c'); - const a = doc.createElement('a'); - const b = doc.createElement('b'); - div.appendChild(span); - span.appendChild(a); - a.appendChild(b); - - const q1 = div.querySelector('span b'); - expect(q1.tagName).toBe('B'); - - const q2 = div.querySelector('span.c b'); - expect(q2.tagName).toBe('B'); - }); - - it('descendent, one deep', () => { - const doc = new MockDocument(); - const div = doc.createElement('div'); - const span = doc.createElement('span'); - span.classList.add('c'); - const a = doc.createElement('a'); - div.appendChild(span); - span.appendChild(a); - - const q1 = div.querySelector('span a'); - expect(q1.tagName).toBe('A'); - - const q2 = div.querySelector('span.c a'); - expect(q2.tagName).toBe('A'); - }); - - it.each(PROBLEMATIC_SELECTORS)("should error for '%p' selector", (selector) => { - const doc = new MockDocument(); - - const expectedMessage = [ - `At present jQuery does not support the ${selector} selector.`, - 'If you need this in your test, consider writing an end-to-end test instead.', - `Syntax error, unrecognized expression: unsupported pseudo: ${selector.replace(':', '')}`, - ].join('\n'); - - expect(() => doc.querySelector(selector)).toThrow(expectedMessage); - expect(() => doc.querySelectorAll(selector)).toThrow(expectedMessage); - expect(() => doc.matches(selector)).toThrow(expectedMessage); - }); - - it('should error for combinations of problematic selectors', () => { - const doc = new MockDocument(); - expect(() => { - doc.querySelector(':scope :is'); - }).toThrow( - [ - `At present jQuery does not support the :scope and :is selectors.`, - 'If you need this in your test, consider writing an end-to-end test instead.', - `Syntax error, unrecognized expression: unsupported pseudo: scope`, - ].join('\n'), - ); - - expect(() => { - doc.querySelector(':is :where'); - }).toThrow( - [ - `At present jQuery does not support the :where and :is selectors.`, - 'If you need this in your test, consider writing an end-to-end test instead.', - `Syntax error, unrecognized expression: unsupported pseudo: is`, - ].join('\n'), - ); - - expect(() => { - doc.querySelector(':scope :is :where'); - }).toThrow( - [ - `At present jQuery does not support the :scope, :where and :is selectors.`, - 'If you need this in your test, consider writing an end-to-end test instead.', - `Syntax error, unrecognized expression: unsupported pseudo: scope`, - ].join('\n'), - ); - }); -}); diff --git a/src/mock-doc/third-party/jquery.ts b/src/mock-doc/third-party/jquery.ts deleted file mode 100644 index e57c79cfce0..00000000000 --- a/src/mock-doc/third-party/jquery.ts +++ /dev/null @@ -1,2363 +0,0 @@ -/* eslint-disable */ -// @ts-nocheck - -/** - * ATTENTION: DO NOT MODIFY THIS FILE - * - * This file is generated by "scripts/updateSelectorEngine.ts" and can be overwritten - * at any time. Don't make changes in here as they will get lost! - */ -export default /*! - * jQuery JavaScript Library v4.0.0-pre+9352011a7.dirty +selector - * https://jquery.com/ - * - * Copyright OpenJS Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2023-12-11T17:55Z - */ -( function( global, factory ) { - - "use strict"; - - if (true) { - - // For CommonJS and CommonJS-like environments where a proper `window` - // is present, execute the factory and get jQuery. - return factory( global, true ); - } else { - factory( global ); - } - -// Pass this if window is not defined yet -} )( { - document: { - createElement() { - return {}; - }, - nodeType: 9, - documentElement: { - nodeType: 1, - nodeName: 'HTML' - } - } -}, function( window, noGlobal ) { - -"use strict"; - -if ( !window.document ) { - throw new Error( "jQuery requires a window with a document" ); -} - -var arr = []; - -var getProto = Object.getPrototypeOf; - -var slice = arr.slice; - -// Support: IE 11+ -// IE doesn't have Array#flat; provide a fallback. -var flat = arr.flat ? function( array ) { - return arr.flat.call( array ); -} : function( array ) { - return arr.concat.apply( [], array ); -}; - -var push = arr.push; - -var indexOf = arr.indexOf; - -// [[Class]] -> type pairs -var class2type = {}; - -var toString = class2type.toString; - -var hasOwn = class2type.hasOwnProperty; - -var fnToString = hasOwn.toString; - -var ObjectFunctionString = fnToString.call( Object ); - -// All support tests are defined in their respective modules. -var support = {}; - -function toType( obj ) { - if ( obj == null ) { - return obj + ""; - } - - return typeof obj === "object" ? - class2type[ toString.call( obj ) ] || "object" : - typeof obj; -} - -function isWindow( obj ) { - return obj != null && obj === obj.window; -} - -function isArrayLike( obj ) { - - var length = !!obj && obj.length, - type = toType( obj ); - - if ( typeof obj === "function" || isWindow( obj ) ) { - return false; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} - -var document = window.document; - -var preservedScriptAttributes = { - type: true, - src: true, - nonce: true, - noModule: true -}; - -function DOMEval( code, node, doc ) { - doc = doc || document; - - var i, - script = doc.createElement( "script" ); - - script.text = code; - if ( node ) { - for ( i in preservedScriptAttributes ) { - if ( node[ i ] ) { - script[ i ] = node[ i ]; - } - } - } - doc.head.appendChild( script ).parentNode.removeChild( script ); -} - -const jQuery = {} as { find: Function }; -var version = "4.0.0-pre+9352011a7.dirty +selector", - - rhtmlSuffix = /HTML$/i, - - // Define a local copy of jQuery - jQueryOrig = function( selector, context ) { - - // The jQuery object is actually just the init constructor 'enhanced' - // Need init if jQuery is called (just allow error to be thrown if not included) - return new jQuery.fn.init( selector, context ); - }; - -jQuery.fn = jQuery.prototype = { - - // The current version of jQuery being used - jquery: version, - - constructor: jQuery, - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - - // Return all the elements in a clean array - if ( num == null ) { - return slice.call( this ); - } - - // Return just the one element from the set - return num < 0 ? this[ num + this.length ] : this[ num ]; - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - each: function( callback ) { - return jQuery.each( this, callback ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map( this, function( elem, i ) { - return callback.call( elem, i, elem ); - } ) ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - even: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return ( i + 1 ) % 2; - } ) ); - }, - - odd: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return i % 2; - } ) ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); - }, - - end: function() { - return this.prevObject || this.constructor(); - } -}; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[ 0 ] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - - // Skip the boolean and the target - target = arguments[ i ] || {}; - i++; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && typeof target !== "function" ) { - target = {}; - } - - // Extend jQuery itself if only one argument is passed - if ( i === length ) { - target = this; - i--; - } - - for ( ; i < length; i++ ) { - - // Only deal with non-null/undefined values - if ( ( options = arguments[ i ] ) != null ) { - - // Extend the base object - for ( name in options ) { - copy = options[ name ]; - - // Prevent Object.prototype pollution - // Prevent never-ending loop - if ( name === "__proto__" || target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject( copy ) || - ( copyIsArray = Array.isArray( copy ) ) ) ) { - src = target[ name ]; - - // Ensure proper type for the source value - if ( copyIsArray && !Array.isArray( src ) ) { - clone = []; - } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { - clone = {}; - } else { - clone = src; - } - copyIsArray = false; - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend( { - - // Unique for each copy of jQuery on the page - expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - - // Assume jQuery is ready without the ready module - isReady: true, - - error: function( msg ) { - throw new Error( msg ); - }, - - noop: function() {}, - - isPlainObject: function( obj ) { - var proto, Ctor; - - // Detect obvious negatives - // Use toString instead of jQuery.type to catch host objects - if ( !obj || toString.call( obj ) !== "[object Object]" ) { - return false; - } - - proto = getProto( obj ); - - // Objects with no prototype (e.g., `Object.create( null )`) are plain - if ( !proto ) { - return true; - } - - // Objects with prototype are plain iff they were constructed by a global Object function - Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; - return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; - }, - - isEmptyObject: function( obj ) { - var name; - - for ( name in obj ) { - return false; - } - return true; - }, - - // Evaluates a script in a provided context; falls back to the global one - // if not specified. - globalEval: function( code, options, doc ) { - DOMEval( code, { nonce: options && options.nonce }, doc ); - }, - - each: function( obj, callback ) { - var length, i = 0; - - if ( isArrayLike( obj ) ) { - length = obj.length; - for ( ; i < length; i++ ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } else { - for ( i in obj ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } - - return obj; - }, - - - // Retrieve the text value of an array of DOM nodes - text: function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - - // If no nodeType, this is expected to be an array - while ( ( node = elem[ i++ ] ) ) { - - // Do not traverse comment nodes - ret += jQuery.text( node ); - } - } - if ( nodeType === 1 || nodeType === 11 ) { - return elem.textContent; - } - if ( nodeType === 9 ) { - return elem.documentElement.textContent; - } - if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - - // Do not include comment or processing instruction nodes - - return ret; - }, - - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArrayLike( Object( arr ) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - return arr == null ? -1 : indexOf.call( arr, elem, i ); - }, - - isXMLDoc: function( elem ) { - var namespace = elem && elem.namespaceURI, - docElem = elem && ( elem.ownerDocument || elem ).documentElement; - - // Assume HTML when documentElement doesn't yet exist, such as inside - // document fragments. - return !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || "HTML" ); - }, - - // Note: an element does not contain itself - contains: function( a, b ) { - var bup = b && b.parentNode; - - return a === bup || !!( bup && bup.nodeType === 1 && ( - - // Support: IE 9 - 11+ - // IE doesn't have `contains` on SVG. - a.contains ? - a.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - ) ); - }, - - merge: function( first, second ) { - var len = +second.length, - j = 0, - i = first.length; - - for ( ; j < len; j++ ) { - first[ i++ ] = second[ j ]; - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, invert ) { - var callbackInverse, - matches = [], - i = 0, - length = elems.length, - callbackExpect = !invert; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - callbackInverse = !callback( elems[ i ], i ); - if ( callbackInverse !== callbackExpect ) { - matches.push( elems[ i ] ); - } - } - - return matches; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var length, value, - i = 0, - ret = []; - - // Go through the array, translating each of the items to their new values - if ( isArrayLike( elems ) ) { - length = elems.length; - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - } - - // Flatten any nested arrays - return flat( ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // jQuery.support is not used in Core but other projects attach their - // properties to it so it needs to exist. - support: support -} ); - -if ( typeof Symbol === "function" ) { - jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; -} - -// Populate the class2type map -jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), - function( _i, name ) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); - } ); - -function nodeName( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); -} - -var pop = arr.pop; - -// https://www.w3.org/TR/css3-selectors/#whitespace -var whitespace = "[\\x20\\t\\r\\n\\f]"; - -var isIE = document.documentMode; - -// Support: Chrome 105 - 111 only, Safari 15.4 - 16.3 only -// Make sure the `:has()` argument is parsed unforgivingly. -// We include `*` in the test to detect buggy implementations that are -// _selectively_ forgiving (specifically when the list includes at least -// one valid selector). -// Note that we treat complete lack of support for `:has()` as if it were -// spec-compliant support, which is fine because use of `:has()` in such -// environments will fail in the qSA path and fall back to jQuery traversal -// anyway. -try { - document.querySelector( ":has(*,:jqfake)" ); - support.cssHas = false; -} catch ( e ) { - support.cssHas = true; -} - -// Build QSA regex. -// Regex strategy adopted from Diego Perini. -var rbuggyQSA = []; - -if ( isIE ) { - rbuggyQSA.push( - - // Support: IE 9 - 11+ - // IE's :disabled selector does not pick up the children of disabled fieldsets - ":enabled", - ":disabled", - - // Support: IE 11+ - // IE 11 doesn't find elements on a `[name='']` query in some cases. - // Adding a temporary attribute to the document before the selection works - // around the issue. - "\\[" + whitespace + "*name" + whitespace + "*=" + - whitespace + "*(?:''|\"\")" - ); -} - -if ( !support.cssHas ) { - - // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+ - // Our regular `try-catch` mechanism fails to detect natively-unsupported - // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`) - // in browsers that parse the `:has()` argument as a forgiving selector list. - // https://drafts.csswg.org/selectors/#relational now requires the argument - // to be parsed unforgivingly, but browsers have not yet fully adjusted. - rbuggyQSA.push( ":has" ); -} - -rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); - -var rtrimCSS = new RegExp( - "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", - "g" -); - -// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram -var identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+"; - -var booleans = "checked|selected|async|autofocus|autoplay|controls|" + - "defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped"; - -var rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + - whitespace + ")" + whitespace + "*" ); - -var rdescend = new RegExp( whitespace + "|>" ); - -var rsibling = /[+~]/; - -var documentElement = document.documentElement; - -// Support: IE 9 - 11+ -// IE requires a prefix. -var matches = documentElement.matches || documentElement.msMatchesSelector; - -/** - * Create key-value caches of limited size - * @returns {function(string, object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - - // Use (key + " ") to avoid collision with native prototype properties - // (see https://github.com/jquery/sizzle/issues/157) - if ( keys.push( key + " " ) > jQuery.expr.cacheLength ) { - - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return ( cache[ key + " " ] = value ); - } - return cache; -} - -/** - * Checks a node for validity as a jQuery selector context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - -// Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors -var attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + - - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + - whitespace + "*\\]"; - -var pseudos = ":(" + identifier + ")(?:\\((" + - - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - - // 3. anything else (capture 2) - ".*" + - ")\\)|)"; - -var filterMatchExpr = { - ID: new RegExp( "^#(" + identifier + ")" ), - CLASS: new RegExp( "^\\.(" + identifier + ")" ), - TAG: new RegExp( "^(" + identifier + "|[*])" ), - ATTR: new RegExp( "^" + attributes ), - PSEUDO: new RegExp( "^" + pseudos ), - CHILD: new RegExp( - "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + - whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + - whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ) -}; - -var rpseudo = new RegExp( pseudos ); - -// CSS escapes - -var runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\([^\\r\\n\\f])", "g" ), - funescape = function( escape, nonHex ) { - var high = "0x" + escape.slice( 1 ) - 0x10000; - - if ( nonHex ) { - - // Strip the backslash prefix from a non-hex escape sequence - return nonHex; - } - - // Replace a hexadecimal escape sequence with the encoded Unicode code point - // Support: IE <=11+ - // For values outside the Basic Multilingual Plane (BMP), manually construct a - // surrogate pair - return high < 0 ? - String.fromCharCode( high + 0x10000 ) : - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }; - -function unescapeSelector( sel ) { - return sel.replace( runescape, funescape ); -} - -function selectorError( msg ) { - jQuery.error( "Syntax error, unrecognized expression: " + msg ); -} - -var rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ); - -var tokenCache = createCache(); - -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = jQuery.expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || ( match = rcomma.exec( soFar ) ) ) { - if ( match ) { - - // Don't consume trailing commas as valid - soFar = soFar.slice( match[ 0 ].length ) || soFar; - } - groups.push( ( tokens = [] ) ); - } - - matched = false; - - // Combinators - if ( ( match = rleadingCombinator.exec( soFar ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - - // Cast descendant combinators to space - type: match[ 0 ].replace( rtrimCSS, " " ) - } ); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in filterMatchExpr ) { - if ( ( match = jQuery.expr.match[ type ].exec( soFar ) ) && ( !preFilters[ type ] || - ( match = preFilters[ type ]( match ) ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - if ( parseOnly ) { - return soFar.length; - } - - return soFar ? - selectorError( selector ) : - - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -} - -var preFilter = { - ATTR: function( match ) { - match[ 1 ] = unescapeSelector( match[ 1 ] ); - - // Move the given value to match[3] whether quoted or unquoted - match[ 3 ] = unescapeSelector( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ); - - if ( match[ 2 ] === "~=" ) { - match[ 3 ] = " " + match[ 3 ] + " "; - } - - return match.slice( 0, 4 ); - }, - - CHILD: function( match ) { - - /* matches from filterMatchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[ 1 ] = match[ 1 ].toLowerCase(); - - if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { - - // nth-* requires argument - if ( !match[ 3 ] ) { - selectorError( match[ 0 ] ); - } - - // numeric x and y parameters for jQuery.expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[ 4 ] = +( match[ 4 ] ? - match[ 5 ] + ( match[ 6 ] || 1 ) : - 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) - ); - match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); - - // other types prohibit arguments - } else if ( match[ 3 ] ) { - selectorError( match[ 0 ] ); - } - - return match; - }, - - PSEUDO: function( match ) { - var excess, - unquoted = !match[ 6 ] && match[ 2 ]; - - if ( filterMatchExpr.CHILD.test( match[ 0 ] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[ 3 ] ) { - match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - - // Get excess from tokenize (recursively) - ( excess = tokenize( unquoted, true ) ) && - - // advance to the next closing parenthesis - ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - - unquoted.length ) ) { - - // excess is a negative index - match[ 0 ] = match[ 0 ].slice( 0, excess ); - match[ 2 ] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } -}; - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[ i ].value; - } - return selector; -} - -// CSS string/identifier serialization -// https://drafts.csswg.org/cssom/#common-serializing-idioms -var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; - -function fcssescape( ch, asCodePoint ) { - if ( asCodePoint ) { - - // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER - if ( ch === "\0" ) { - return "\uFFFD"; - } - - // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; - } - - // Other potentially-special ASCII characters get backslash-escaped - return "\\" + ch; -} - -jQuery.escapeSelector = function( sel ) { - return ( sel + "" ).replace( rcssescape, fcssescape ); -}; - -var sort = arr.sort; - -var splice = arr.splice; - -var hasDuplicate; - -// Document order sorting -function sortOrder( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - // Support: IE 11+ - // IE sometimes throws a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 ) { - - // Choose the first element that is related to the document - // Support: IE 11+ - // IE sometimes throws a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( a == document || a.ownerDocument == document && - jQuery.contains( document, a ) ) { - return -1; - } - - // Support: IE 11+ - // IE sometimes throws a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( b == document || b.ownerDocument == document && - jQuery.contains( document, b ) ) { - return 1; - } - - // Maintain original order - return 0; - } - - return compare & 4 ? -1 : 1; -} - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -jQuery.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - hasDuplicate = false; - - sort.call( results, sortOrder ); - - if ( hasDuplicate ) { - while ( ( elem = results[ i++ ] ) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - splice.call( results, duplicates[ j ], 1 ); - } - } - - return results; -}; - -jQuery.fn.uniqueSort = function() { - return this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) ); -}; - -var i, - outermostContext, - - // Local document vars - document$1, - documentElement$1, - documentIsHTML, - - // Instance-specific data - dirruns = 0, - done = 0, - classCache = createCache(), - compilerCache = createCache(), - nonnativeSelectorCache = createCache(), - - // Regular expressions - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rwhitespace = new RegExp( whitespace + "+", "g" ), - - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = jQuery.extend( { - bool: new RegExp( "^(?:" + booleans + ")$", "i" ), - - // For use in libraries implementing .is() - // We use this for POS matching in `select` - needsContext: new RegExp( "^" + whitespace + - "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + - "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, filterMatchExpr ), - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - // Used for iframes; see `setDocument`. - // Support: IE 9 - 11+ - // Removing the function wrapper causes a "Permission Denied" - // error in IE. - unloadHandler = function() { - setDocument(); - }, - - inDisabledFieldset = addCombinator( - function( elem ) { - return elem.disabled === true && nodeName( elem, "fieldset" ); - }, - { dir: "parentNode", next: "legend" } - ); - -function find( selector, context, results, seed ) { - var m, i, elem, nid, match, groups, newSelector, - newContext = context && context.ownerDocument, - - // nodeType defaults to 9, since context defaults to document - nodeType = context ? context.nodeType : 9; - - results = results || []; - - // Return early from calls with invalid selector or context - if ( typeof selector !== "string" || !selector || - nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { - - return results; - } - - // Try to shortcut find operations (as opposed to filters) in HTML documents - if ( false ) { - setDocument( context ); - context = context || document$1; - - if ( documentIsHTML ) { - - // If the selector is sufficiently simple, try using a "get*By*" DOM method - // (excepting DocumentFragment context, where the methods don't exist) - if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { - - // ID selector - if ( ( m = match[ 1 ] ) ) { - - // Document context - if ( nodeType === 9 ) { - if ( ( elem = context.getElementById( m ) ) ) { - push.call( results, elem ); - } - return results; - - // Element context - } else { - if ( newContext && ( elem = newContext.getElementById( m ) ) && - jQuery.contains( context, elem ) ) { - - push.call( results, elem ); - return results; - } - } - - // Type selector - } else if ( match[ 2 ] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Class selector - } else if ( ( m = match[ 3 ] ) && context.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // Take advantage of querySelectorAll - if ( !nonnativeSelectorCache[ selector + " " ] && - ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) { - - newSelector = selector; - newContext = context; - - // qSA considers elements outside a scoping root when evaluating child or - // descendant combinators, which is not what we want. - // In such cases, we work around the behavior by prefixing every selector in the - // list with an ID selector referencing the scope context. - // The technique has to be used as well when a leading combinator is used - // as such selectors are not recognized by querySelectorAll. - // Thanks to Andrew Dupont for this technique. - if ( nodeType === 1 && - ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { - - // Expand context for sibling selectors - newContext = rsibling.test( selector ) && - testContext( context.parentNode ) || - context; - - // Outside of IE, if we're not changing the context we can - // use :scope instead of an ID. - // Support: IE 11+ - // IE sometimes throws a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( newContext != context || isIE ) { - - // Capture the context ID, setting it first if necessary - if ( ( nid = context.getAttribute( "id" ) ) ) { - nid = jQuery.escapeSelector( nid ); - } else { - context.setAttribute( "id", ( nid = jQuery.expando ) ); - } - } - - // Prefix every selector in the list - groups = tokenize( selector ); - i = groups.length; - while ( i-- ) { - groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + - toSelector( groups[ i ] ); - } - newSelector = groups.join( "," ); - } - - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch ( qsaError ) { - nonnativeSelectorCache( selector, true ); - } finally { - if ( nid === jQuery.expando ) { - context.removeAttribute( "id" ); - } - } - } - } - } - - // All others - return select( selector.replace( rtrimCSS, "$1" ), context, results, seed ); -} - -/** - * Mark a function for special use by jQuery selector module - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ jQuery.expando ] = true; - return fn; -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - return nodeName( elem, "input" ) && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - return ( nodeName( elem, "input" ) || nodeName( elem, "button" ) ) && - elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for :enabled/:disabled - * @param {Boolean} disabled true for :disabled; false for :enabled - */ -function createDisabledPseudo( disabled ) { - - // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable - return function( elem ) { - - // Only certain elements can match :enabled or :disabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled - if ( "form" in elem ) { - - // Check for inherited disabledness on relevant non-disabled elements: - // * listed form-associated elements in a disabled fieldset - // https://html.spec.whatwg.org/multipage/forms.html#category-listed - // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled - // * option elements in a disabled optgroup - // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled - // All such elements have a "form" property. - if ( elem.parentNode && elem.disabled === false ) { - - // Option elements defer to a parent optgroup if present - if ( "label" in elem ) { - if ( "label" in elem.parentNode ) { - return elem.parentNode.disabled === disabled; - } else { - return elem.disabled === disabled; - } - } - - // Support: IE 6 - 11+ - // Use the isDisabled shortcut property to check for disabled fieldset ancestors - return elem.isDisabled === disabled || - - // Where there is no isDisabled, check manually - elem.isDisabled !== !disabled && - inDisabledFieldset( elem ) === disabled; - } - - return elem.disabled === disabled; - - // Try to winnow out elements that can't be disabled before trusting the disabled property. - // Some victims get caught in our net (label, legend, menu, track), but it shouldn't - // even exist on them, let alone have a boolean value. - } else if ( "label" in elem ) { - return elem.disabled === disabled; - } - - // Remaining elements are neither :enabled nor :disabled - return false; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction( function( argument ) { - argument = +argument; - return markFunction( function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ ( j = matchIndexes[ i ] ) ] ) { - seed[ j ] = !( matches[ j ] = seed[ j ] ); - } - } - } ); - } ); -} - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [node] An element or document object to use to set the document - */ -function setDocument( node ) { - var subWindow, - doc = node ? node.ownerDocument || node : document; - - // Return early if doc is invalid or already selected - // Support: IE 11+ - // IE sometimes throws a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( doc == document$1 || doc.nodeType !== 9 ) { - return; - } - - // Update global variables - document$1 = doc; - documentElement$1 = document$1.documentElement; - documentIsHTML = !jQuery.isXMLDoc( document$1 ); - - // Support: IE 9 - 11+ - // Accessing iframe documents after unload throws "permission denied" errors (see trac-13936) - // Support: IE 11+ - // IE sometimes throws a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( isIE && document != document$1 && - ( subWindow = document$1.defaultView ) && subWindow.top !== subWindow ) { - subWindow.addEventListener( "unload", unloadHandler ); - } -} - -find.matches = function( expr, elements ) { - return find( expr, null, null, elements ); -}; - -find.matchesSelector = function( elem, expr ) { - setDocument( elem ); - - if ( documentIsHTML && - !nonnativeSelectorCache[ expr + " " ] && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - return matches.call( elem, expr ); - } catch ( e ) { - nonnativeSelectorCache( expr, true ); - } - } - - return find( expr, document$1, null, [ elem ] ).length > 0; -}; - -jQuery.expr = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - find: { - ID: function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var elem = context.getElementById( id ); - return elem ? [ elem ] : []; - } - }, - - TAG: function( tag, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( tag ); - - // DocumentFragment nodes don't have gEBTN - } else { - return context.querySelectorAll( tag ); - } - }, - - CLASS: function( className, context ) { - if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - } - }, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: preFilter, - - filter: { - ID: function( id ) { - var attrId = unescapeSelector( id ); - return function( elem ) { - return elem.getAttribute( "id" ) === attrId; - }; - }, - - TAG: function( nodeNameSelector ) { - var expectedNodeName = unescapeSelector( nodeNameSelector ).toLowerCase(); - return nodeNameSelector === "*" ? - - function() { - return true; - } : - - function( elem ) { - return nodeName( elem, expectedNodeName ); - }; - }, - - CLASS: function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - ( pattern = new RegExp( "(^|" + whitespace + ")" + className + - "(" + whitespace + "|$)" ) ) && - classCache( className, function( elem ) { - return pattern.test( - typeof elem.className === "string" && elem.className || - typeof elem.getAttribute !== "undefined" && - elem.getAttribute( "class" ) || - "" - ); - } ); - }, - - ATTR: function( name, operator, check ) { - return function( elem ) { - var result = elem.getAttribute( name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - if ( operator === "=" ) { - return result === check; - } - if ( operator === "!=" ) { - return result !== check; - } - if ( operator === "^=" ) { - return check && result.indexOf( check ) === 0; - } - if ( operator === "*=" ) { - return check && result.indexOf( check ) > -1; - } - if ( operator === "$=" ) { - return check && result.slice( -check.length ) === check; - } - if ( operator === "~=" ) { - return ( " " + result.replace( rwhitespace, " " ) + " " ) - .indexOf( check ) > -1; - } - if ( operator === "|=" ) { - return result === check || result.slice( 0, check.length + 1 ) === check + "-"; - } - - return false; - }; - }, - - CHILD: function( type, what, _argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, _context, xml ) { - var cache, outerCache, node, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType, - diff = false; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( ( node = node[ dir ] ) ) { - if ( ofType ? - nodeName( node, name ) : - node.nodeType === 1 ) { - - return false; - } - } - - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - - // Seek `elem` from a previously-cached index - outerCache = parent[ jQuery.expando ] || - ( parent[ jQuery.expando ] = {} ); - cache = outerCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex && cache[ 2 ]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( ( node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - } else { - - // Use previously-cached element index if available - if ( useCache ) { - outerCache = elem[ jQuery.expando ] || - ( elem[ jQuery.expando ] = {} ); - cache = outerCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex; - } - - // xml :nth-child(...) - // or :nth-last-child(...) or :nth(-last)?-of-type(...) - if ( diff === false ) { - - // Use the same loop as above to seek `elem` from the start - while ( ( node = ++nodeIndex && node && node[ dir ] || - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - if ( ( ofType ? - nodeName( node, name ) : - node.nodeType === 1 ) && - ++diff ) { - - // Cache the index of each encountered element - if ( useCache ) { - outerCache = node[ jQuery.expando ] || - ( node[ jQuery.expando ] = {} ); - outerCache[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - PSEUDO: function( pseudo, argument ) { - - // pseudo-class names are case-insensitive - // https://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var fn = jQuery.expr.pseudos[ pseudo ] || - jQuery.expr.setFilters[ pseudo.toLowerCase() ] || - selectorError( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as jQuery does - if ( fn[ jQuery.expando ] ) { - return fn( argument ); - } - - return fn; - } - }, - - pseudos: { - - // Potentially complex pseudos - not: markFunction( function( selector ) { - - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrimCSS, "$1" ) ); - - return matcher[ jQuery.expando ] ? - markFunction( function( seed, matches, _context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( ( elem = unmatched[ i ] ) ) { - seed[ i ] = !( matches[ i ] = elem ); - } - } - } ) : - function( elem, _context, xml ) { - input[ 0 ] = elem; - matcher( input, null, xml, results ); - - // Don't keep the element - // (see https://github.com/jquery/sizzle/issues/299) - input[ 0 ] = null; - return !results.pop(); - }; - } ), - - has: markFunction( function( selector ) { - return function( elem ) { - return find( selector, elem ).length > 0; - }; - } ), - - contains: markFunction( function( text ) { - text = unescapeSelector( text ); - return function( elem ) { - return ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1; - }; - } ), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // https://www.w3.org/TR/selectors/#lang-pseudo - lang: markFunction( function( lang ) { - - // lang value must be a valid identifier - if ( !ridentifier.test( lang || "" ) ) { - selectorError( "unsupported lang: " + lang ); - } - lang = unescapeSelector( lang ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( ( elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); - return false; - }; - } ), - - // Miscellaneous - target: function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - root: function( elem ) { - return elem === documentElement$1; - }, - - focus: function( elem ) { - return elem === document$1.activeElement && - document$1.hasFocus() && - !!( elem.type || elem.href || ~elem.tabIndex ); - }, - - // Boolean properties - enabled: createDisabledPseudo( false ), - disabled: createDisabledPseudo( true ), - - checked: function( elem ) { - - // In CSS3, :checked should return both checked and selected elements - // https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - return ( nodeName( elem, "input" ) && !!elem.checked ) || - ( nodeName( elem, "option" ) && !!elem.selected ); - }, - - selected: function( elem ) { - - // Support: IE <=11+ - // Accessing the selectedIndex property - // forces the browser to treat the default option as - // selected when in an optgroup. - if ( isIE && elem.parentNode ) { - // eslint-disable-next-line no-unused-expressions - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - empty: function( elem ) { - - // https://www.w3.org/TR/selectors/#empty-pseudo - // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), - // but not by others (comment: 8; processing instruction: 7; etc.) - // nodeType < 6 works because attributes (2) do not appear as children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - parent: function( elem ) { - return !jQuery.expr.pseudos.empty( elem ); - }, - - // Element/input types - header: function( elem ) { - return rheader.test( elem.nodeName ); - }, - - input: function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - button: function( elem ) { - return nodeName( elem, "input" ) && elem.type === "button" || - nodeName( elem, "button" ); - }, - - text: function( elem ) { - return nodeName( elem, "input" ) && elem.type === "text"; - }, - - // Position-in-collection - first: createPositionalPseudo( function() { - return [ 0 ]; - } ), - - last: createPositionalPseudo( function( _matchIndexes, length ) { - return [ length - 1 ]; - } ), - - eq: createPositionalPseudo( function( _matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - } ), - - even: createPositionalPseudo( function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - odd: createPositionalPseudo( function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - lt: createPositionalPseudo( function( matchIndexes, length, argument ) { - var i; - - if ( argument < 0 ) { - i = argument + length; - } else if ( argument > length ) { - i = length; - } else { - i = argument; - } - - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - gt: createPositionalPseudo( function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ) - } -}; - -jQuery.expr.pseudos.nth = jQuery.expr.pseudos.eq; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - jQuery.expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - jQuery.expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = jQuery.expr.filters = jQuery.expr.pseudos; -jQuery.expr.setFilters = new setFilters(); - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - skip = combinator.next, - key = skip || dir, - checkNonElements = base && key === "parentNode", - doneName = done++; - - return combinator.first ? - - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - return false; - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var oldCache, outerCache, - newCache = [ dirruns, doneName ]; - - // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching - if ( xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ jQuery.expando ] || ( elem[ jQuery.expando ] = {} ); - - if ( skip && nodeName( elem, skip ) ) { - elem = elem[ dir ] || elem; - } else if ( ( oldCache = outerCache[ key ] ) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - // Assign to newCache so results back-propagate to previous elements - return ( newCache[ 2 ] = oldCache[ 2 ] ); - } else { - - // Reuse newcache so results back-propagate to previous elements - outerCache[ key ] = newCache; - - // A match means we're done; a fail means we have to keep checking - if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { - return true; - } - } - } - } - } - return false; - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[ i ]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[ 0 ]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - find( selector, contexts[ i ], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( ( elem = unmatched[ i ] ) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ jQuery.expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ jQuery.expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction( function( seed, results, context, xml ) { - var temp, i, elem, matcherOut, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || - multipleContexts( selector || "*", - context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems; - - if ( matcher ) { - - // If we have a postFinder, or filtered seed, or non-seed postFilter - // or preexisting results, - matcherOut = postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results; - - // Find primary matches - matcher( matcherIn, matcherOut, context, xml ); - } else { - matcherOut = matcherIn; - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( ( elem = temp[ i ] ) ) { - matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) ) { - - // Restore matcherIn since elem is not yet a final match - temp.push( ( matcherIn[ i ] = elem ) ); - } - } - postFinder( null, ( matcherOut = [] ), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) && - ( temp = postFinder ? indexOf.call( seed, elem ) : preMap[ i ] ) > -1 ) { - - seed[ temp ] = !( results[ temp ] = elem ); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - } ); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = jQuery.expr.relative[ tokens[ 0 ].type ], - implicitRelative = leadingRelative || jQuery.expr.relative[ " " ], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - - // Support: IE 11+ - // IE sometimes throws a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - var ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || ( - ( checkContext = context ).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - - // Avoid hanging onto element - // (see https://github.com/jquery/sizzle/issues/299) - checkContext = null; - return ret; - } ]; - - for ( ; i < len; i++ ) { - if ( ( matcher = jQuery.expr.relative[ tokens[ i ].type ] ) ) { - matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; - } else { - matcher = jQuery.expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ jQuery.expando ] ) { - - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( jQuery.expr.relative[ tokens[ j ].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice( 0, i - 1 ) - .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) - ).replace( rtrimCSS, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - - // We must always have either seed elements or outermost context - elems = seed || byElement && jQuery.expr.find.TAG( "*", outermost ), - - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ); - - if ( outermost ) { - - // Support: IE 11+ - // IE sometimes throws a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - outermostContext = context == document$1 || context || outermost; - } - - // Add elements passing elementMatchers directly to results - for ( ; ( elem = elems[ i ] ) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - - // Support: IE 11+ - // IE sometimes throws a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( !context && elem.ownerDocument != document$1 ) { - setDocument( elem ); - xml = !documentIsHTML; - } - while ( ( matcher = elementMatchers[ j++ ] ) ) { - if ( matcher( elem, context || document$1, xml ) ) { - push.call( results, elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - - // They will have gone through all possible matchers - if ( ( elem = !matcher && elem ) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // `i` is now the count of elements visited above, and adding it to `matchedCount` - // makes the latter nonnegative. - matchedCount += i; - - // Apply set filters to unmatched elements - // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` - // equals `i`), unless we didn't visit _any_ elements in the above loop because we have - // no element matchers and no seed. - // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that - // case, which will result in a "00" `matchedCount` that differs from `i` but is also - // numerically zero. - if ( bySet && i !== matchedCount ) { - j = 0; - while ( ( matcher = setMatchers[ j++ ] ) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !( unmatched[ i ] || setMatched[ i ] ) ) { - setMatched[ i ] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - jQuery.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -function compile( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - - // Generate a function of recursive functions that can be used to check each element - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[ i ] ); - if ( cached[ jQuery.expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, - matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - - // Save selector and tokenization - cached.selector = selector; - } - return cached; -} - -/** - * A low-level selection function that works with jQuery's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with jQuery selector compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -function select( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( ( selector = compiled.selector || selector ) ); - - results = results || []; - - // Try to minimize operations if there is only one selector in the list and no seed - // (the latter of which guarantees us context) - if ( match.length === 1 ) { - - // Reduce context if the leading compound selector is an ID - tokens = match[ 0 ] = match[ 0 ].slice( 0 ); - if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && - context.nodeType === 9 && documentIsHTML && - jQuery.expr.relative[ tokens[ 1 ].type ] ) { - - context = ( jQuery.expr.find.ID( - unescapeSelector( token.matches[ 0 ] ), - context - ) || [] )[ 0 ]; - if ( !context ) { - return results; - - // Precompiled matchers will still verify ancestry, so step up a level - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr.needsContext.test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[ i ]; - - // Abort if we hit a combinator - if ( jQuery.expr.relative[ ( type = token.type ) ] ) { - break; - } - if ( ( find = jQuery.expr.find[ type ] ) ) { - - // Search, expanding context for leading sibling combinators - if ( ( seed = find( - unescapeSelector( token.matches[ 0 ] ), - rsibling.test( tokens[ 0 ].type ) && - testContext( context.parentNode ) || context - ) ) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - // Compile and execute a filtering function if one is not provided - // Provide `match` to avoid retokenization if we modified the selector above - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - !context || rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -} - -// Initialize against the default document -setDocument(); - -jQuery.find = find; - -// These have always been private, but they used to be documented as part of -// Sizzle so let's maintain them for now for backwards compatibility purposes. -find.compile = compile; -find.select = select; -find.setDocument = setDocument; -find.tokenize = tokenize; - -return jQuery; - -} ); -; diff --git a/src/mock-doc/window.ts b/src/mock-doc/window.ts deleted file mode 100644 index 9edaaba0aac..00000000000 --- a/src/mock-doc/window.ts +++ /dev/null @@ -1,923 +0,0 @@ -import { MockHeaders } from '.'; -import { createConsole } from './console'; -import { MockCustomElementRegistry } from './custom-element-registry'; -import { MockDocument, resetDocument } from './document'; -import { MockDocumentFragment } from './document-fragment'; -import { MockSVGElement } from './element'; -import { - addEventListener, - dispatchEvent, - MockCustomEvent, - MockEvent, - MockFocusEvent, - MockKeyboardEvent, - MockMouseEvent, - removeEventListener, - resetEventListeners, -} from './event'; -import { addGlobalsToWindowPrototype } from './global'; -import { MockHistory } from './history'; -import { MockIntersectionObserver } from './intersection-observer'; -import { MockLocation } from './location'; -import { MockNavigator } from './navigator'; -import { MockElement, MockHTMLElement, MockNode, MockNodeList } from './node'; -import { MockPerformance, resetPerformance } from './performance'; -import { MockResizeObserver } from './resize-observer'; -import { MockShadowRoot } from './shadow-root'; -import { MockStorage } from './storage'; - -const nativeClearInterval = globalThis.clearInterval; -const nativeClearTimeout = globalThis.clearTimeout; -const nativeSetInterval = globalThis.setInterval; -const nativeSetTimeout = globalThis.setTimeout; -const nativeURL = globalThis.URL; -const nativeWindow = globalThis.window; - -export class MockWindow { - __timeouts: Set; - __history: MockHistory; - __elementCstr: any; - __charDataCstr: any; - __docTypeCstr: any; - __docCstr: any; - __docFragCstr: any; - __domTokenListCstr: any; - __nodeCstr: any; - __nodeListCstr: any; - __localStorage: MockStorage; - __sessionStorage: MockStorage; - __location: MockLocation; - __navigator: MockNavigator; - __clearInterval: typeof nativeClearInterval; - __clearTimeout: typeof nativeClearTimeout; - __setInterval: typeof nativeSetInterval; - __setTimeout: typeof nativeSetTimeout; - __maxTimeout: number; - __allowInterval: boolean; - URL: typeof URL; - - console: Console; - customElements: CustomElementRegistry; - document: Document; - performance: Performance; - - devicePixelRatio: number; - innerHeight: number; - innerWidth: number; - pageXOffset: number; - pageYOffset: number; - screen: Screen; - screenLeft: number; - screenTop: number; - screenX: number; - screenY: number; - scrollX: number; - scrollY: number; - - // event handlers - declare CustomEvent: typeof MockCustomEvent; - declare Event: typeof MockEvent; - declare Headers: typeof MockHeaders; - declare FocusEvent: typeof MockFocusEvent; - declare KeyboardEvent: typeof MockKeyboardEvent; - declare MouseEvent: typeof MockMouseEvent; - - constructor(html: string | boolean = null) { - if (html !== false) { - this.document = new MockDocument(html, this) as any; - } else { - this.document = null; - } - this.performance = new MockPerformance(); - this.customElements = new MockCustomElementRegistry(this as any); - this.console = createConsole(); - resetWindowDefaults(this); - resetWindowDimensions(this); - } - - addEventListener(type: string, handler: (ev?: any) => void) { - addEventListener(this, type, handler); - } - - alert(msg: string) { - if (this.console) { - this.console.debug(msg); - } else { - console.debug(msg); - } - } - - blur(): any { - /**/ - } - - cancelAnimationFrame(id: any) { - this.__clearTimeout.call(nativeWindow || this, id); - } - - cancelIdleCallback(id: any) { - this.__clearTimeout.call(nativeWindow || this, id); - } - - get CharacterData() { - if (this.__charDataCstr == null) { - const ownerDocument = this.document; - this.__charDataCstr = class extends MockNode { - constructor() { - super(ownerDocument, 0, 'test', ''); - throw new Error('Illegal constructor: cannot construct CharacterData'); - } - }; - } - return this.__charDataCstr; - } - set CharacterData(charDataCstr: any) { - this.__charDataCstr = charDataCstr; - } - - clearInterval(id: any) { - this.__clearInterval.call(nativeWindow || this, id); - } - - clearTimeout(id: any) { - this.__clearTimeout.call(nativeWindow || this, id); - } - - close() { - resetWindow(this as any); - } - - confirm() { - return false; - } - - get CSS() { - return { - supports: () => true, - }; - } - - get Document() { - if (this.__docCstr == null) { - const win = this; - this.__docCstr = class extends MockDocument { - constructor() { - super(false, win); - throw new Error('Illegal constructor: cannot construct Document'); - } - }; - } - return this.__docCstr; - } - set Document(docCstr: any) { - this.__docCstr = docCstr; - } - - get DocumentFragment() { - if (this.__docFragCstr == null) { - const ownerDocument = this.document; - this.__docFragCstr = class extends MockDocumentFragment { - constructor() { - super(ownerDocument); - throw new Error('Illegal constructor: cannot construct DocumentFragment'); - } - }; - } - return this.__docFragCstr; - } - set DocumentFragment(docFragCstr: any) { - this.__docFragCstr = docFragCstr; - } - - get ShadowRoot() { - return MockShadowRoot; - } - - get DocumentType() { - if (this.__docTypeCstr == null) { - const ownerDocument = this.document; - this.__docTypeCstr = class extends MockNode { - constructor() { - super(ownerDocument, 0, 'test', ''); - throw new Error('Illegal constructor: cannot construct DocumentType'); - } - }; - } - return this.__docTypeCstr; - } - set DocumentType(docTypeCstr: any) { - this.__docTypeCstr = docTypeCstr; - } - - get DOMTokenList() { - if (this.__domTokenListCstr == null) { - this.__domTokenListCstr = class MockDOMTokenList {}; - } - return this.__domTokenListCstr; - } - set DOMTokenList(domTokenListCstr: any) { - this.__domTokenListCstr = domTokenListCstr; - } - - dispatchEvent(ev: MockEvent) { - return dispatchEvent(this, ev); - } - - get Element() { - return MockElement; - } - - fetch(input: any, init?: any): any { - if (typeof fetch === 'function') { - return fetch(input, init); - } - throw new Error(`fetch() not implemented`); - } - - focus(): any { - /**/ - } - - getComputedStyle(_: any) { - return { - cssText: '', - length: 0, - parentRule: null, - getPropertyPriority(): any { - return null; - }, - getPropertyValue(): any { - return ''; - }, - item(): any { - return null; - }, - removeProperty(): any { - return null; - }, - setProperty(): any { - return null; - }, - } as any; - } - - get globalThis() { - return this; - } - - get history() { - if (this.__history == null) { - this.__history = new MockHistory(); - } - return this.__history; - } - set history(hsty: any) { - this.__history = hsty; - } - - get JSON() { - return JSON; - } - - get HTMLElement() { - return MockHTMLElement; - } - - get SVGElement() { - return MockSVGElement; - } - - get IntersectionObserver() { - return MockIntersectionObserver; - } - - get ResizeObserver() { - return MockResizeObserver; - } - - get localStorage() { - if (this.__localStorage == null) { - this.__localStorage = new MockStorage(); - } - return this.__localStorage; - } - set localStorage(locStorage: MockStorage) { - this.__localStorage = locStorage; - } - - get location(): MockLocation { - if (this.__location == null) { - this.__location = new MockLocation(); - } - return this.__location; - } - set location(val: Location | string) { - if (typeof val === 'string') { - if (this.__location == null) { - this.__location = new MockLocation(); - } - this.__location.href = val; - } else { - this.__location = val as any; - } - } - - matchMedia(media: string) { - return { - media, - matches: false, - addListener: (_handler: (ev?: any) => void) => {}, - removeListener: (_handler: (ev?: any) => void) => {}, - addEventListener: (_type: string, _handler: (ev?: any) => void) => {}, - removeEventListener: (_type: string, _handler: (ev?: any) => void) => {}, - dispatchEvent: (_ev: any) => {}, - onchange: null as ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null, - }; - } - - get Node() { - return MockNode; - } - - get NodeList() { - if (this.__nodeListCstr == null) { - const ownerDocument = this.document; - this.__nodeListCstr = class extends MockNodeList { - constructor() { - super(ownerDocument, [], 0); - throw new Error('Illegal constructor: cannot construct NodeList'); - } - }; - } - return this.__nodeListCstr; - } - - get navigator() { - if (this.__navigator == null) { - this.__navigator = new MockNavigator(); - } - return this.__navigator; - } - set navigator(nav: any) { - this.__navigator = nav; - } - - get parent(): any { - return null; - } - - prompt() { - return ''; - } - - open(): any { - return null; - } - - get origin() { - return this.location.origin; - } - - removeEventListener(type: string, handler: any) { - removeEventListener(this, type, handler); - } - - requestAnimationFrame(callback: (timestamp: number) => void) { - return this.setTimeout(() => { - callback(Date.now()); - }, 0) as number; - } - - requestIdleCallback(callback: (deadline: { didTimeout: boolean; timeRemaining: () => number }) => void) { - return this.setTimeout(() => { - callback({ - didTimeout: false, - timeRemaining: () => 0, - }); - }, 0); - } - - scroll(_x?: number, _y?: number) { - /**/ - } - - scrollBy(_x?: number, _y?: number) { - /**/ - } - - scrollTo(_x?: number, _y?: number) { - /**/ - } - - get self() { - return this; - } - - get sessionStorage() { - if (this.__sessionStorage == null) { - this.__sessionStorage = new MockStorage(); - } - return this.__sessionStorage; - } - set sessionStorage(locStorage: any) { - this.__sessionStorage = locStorage; - } - - setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): number { - if (this.__timeouts == null) { - this.__timeouts = new Set(); - } - - ms = Math.min(ms ?? 0, this.__maxTimeout); - - if (this.__allowInterval) { - const intervalId = this.__setInterval(() => { - if (this.__timeouts) { - this.__timeouts.delete(intervalId); - - try { - callback(...args); - } catch (e) { - if (this.console) { - this.console.error(e); - } else { - console.error(e); - } - } - } - }, ms) as any; - - if (this.__timeouts) { - this.__timeouts.add(intervalId); - } - - return intervalId; - } - - const timeoutId = this.__setTimeout.call( - nativeWindow || this, - () => { - if (this.__timeouts) { - this.__timeouts.delete(timeoutId); - - try { - callback(...args); - } catch (e) { - if (this.console) { - this.console.error(e); - } else { - console.error(e); - } - } - } - }, - ms, - ) as any; - - if (this.__timeouts) { - this.__timeouts.add(timeoutId); - } - - return timeoutId; - } - - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): number { - if (this.__timeouts == null) { - this.__timeouts = new Set(); - } - - ms = Math.min(ms ?? 0, this.__maxTimeout); - - const timeoutId = this.__setTimeout.call( - nativeWindow || this, - () => { - if (this.__timeouts) { - this.__timeouts.delete(timeoutId); - - try { - callback(...args); - } catch (e) { - if (this.console) { - this.console.error(e); - } else { - console.error(e); - } - } - } - }, - ms, - ) as any as number; - - if (this.__timeouts) { - this.__timeouts.add(timeoutId); - } - - return timeoutId; - } - - get top() { - return this; - } - - get window() { - return this; - } - - onanimationstart() { - /**/ - } - onanimationend() { - /**/ - } - onanimationiteration() { - /**/ - } - onabort() { - /**/ - } - onauxclick() { - /**/ - } - onbeforecopy() { - /**/ - } - onbeforecut() { - /**/ - } - onbeforepaste() { - /**/ - } - onblur() { - /**/ - } - oncancel() { - /**/ - } - oncanplay() { - /**/ - } - oncanplaythrough() { - /**/ - } - onchange() { - /**/ - } - onclick() { - /**/ - } - onclose() { - /**/ - } - oncontextmenu() { - /**/ - } - oncopy() { - /**/ - } - oncuechange() { - /**/ - } - oncut() { - /**/ - } - ondblclick() { - /**/ - } - ondrag() { - /**/ - } - ondragend() { - /**/ - } - ondragenter() { - /**/ - } - ondragleave() { - /**/ - } - ondragover() { - /**/ - } - ondragstart() { - /**/ - } - ondrop() { - /**/ - } - ondurationchange() { - /**/ - } - onemptied() { - /**/ - } - onended() { - /**/ - } - onerror() { - /**/ - } - onfocus() { - /**/ - } - onfocusin() { - /**/ - } - onfocusout() { - /**/ - } - onformdata() { - /**/ - } - onfullscreenchange() { - /**/ - } - onfullscreenerror() { - /**/ - } - ongotpointercapture() { - /**/ - } - oninput() { - /**/ - } - oninvalid() { - /**/ - } - onkeydown() { - /**/ - } - onkeypress() { - /**/ - } - onkeyup() { - /**/ - } - onload() { - /**/ - } - onloadeddata() { - /**/ - } - onloadedmetadata() { - /**/ - } - onloadstart() { - /**/ - } - onlostpointercapture() { - /**/ - } - onmousedown() { - /**/ - } - onmouseenter() { - /**/ - } - onmouseleave() { - /**/ - } - onmousemove() { - /**/ - } - onmouseout() { - /**/ - } - onmouseover() { - /**/ - } - onmouseup() { - /**/ - } - onmousewheel() { - /**/ - } - onpaste() { - /**/ - } - onpause() { - /**/ - } - onplay() { - /**/ - } - onplaying() { - /**/ - } - onpointercancel() { - /**/ - } - onpointerdown() { - /**/ - } - onpointerenter() { - /**/ - } - onpointerleave() { - /**/ - } - onpointermove() { - /**/ - } - onpointerout() { - /**/ - } - onpointerover() { - /**/ - } - onpointerup() { - /**/ - } - onprogress() { - /**/ - } - onratechange() { - /**/ - } - onreset() { - /**/ - } - onresize() { - /**/ - } - onscroll() { - /**/ - } - onsearch() { - /**/ - } - onseeked() { - /**/ - } - onseeking() { - /**/ - } - onselect() { - /**/ - } - onselectstart() { - /**/ - } - onstalled() { - /**/ - } - onsubmit() { - /**/ - } - onsuspend() { - /**/ - } - ontimeupdate() { - /**/ - } - ontoggle() { - /**/ - } - onvolumechange() { - /**/ - } - onwaiting() { - /**/ - } - onwebkitfullscreenchange() { - /**/ - } - onwebkitfullscreenerror() { - /**/ - } - onwheel() { - /**/ - } -} - -addGlobalsToWindowPrototype(MockWindow.prototype); - -function resetWindowDefaults(win: MockWindow) { - win.__clearInterval = nativeClearInterval; - win.__clearTimeout = nativeClearTimeout; - win.__setInterval = nativeSetInterval; - win.__setTimeout = nativeSetTimeout; - win.__maxTimeout = 60000; - win.__allowInterval = true; - win.URL = nativeURL; -} - -export function createWindow(html: string | boolean = null): Window { - return new MockWindow(html) as any; -} - -export function cloneWindow(srcWin: Window, opts: { customElementProxy?: boolean } = {}): MockWindow | null { - if (srcWin == null) { - return null; - } - - const clonedWin = new MockWindow(false); - if (!opts.customElementProxy) { - // TODO(STENCIL-345) - Evaluate reconciling MockWindow, Window differences - // @ts-ignore - srcWin.customElements = null; - } - - if (srcWin.document != null) { - const clonedDoc = new MockDocument(false, clonedWin); - clonedWin.document = clonedDoc as any; - clonedDoc.documentElement = srcWin.document.documentElement.cloneNode(true) as any; - } else { - clonedWin.document = new MockDocument(null, clonedWin) as any; - } - return clonedWin; -} - -export function cloneDocument(srcDoc: Document) { - if (srcDoc == null || !srcDoc.defaultView) { - return null; - } - - const dstWin = cloneWindow(srcDoc.defaultView); - return dstWin?.document || null; -} - -// TODO(STENCIL-345) - Evaluate reconciling MockWindow, Window differences -/** - * Constrain setTimeout() to 1ms, but still async. Also - * only allow setInterval() to fire once, also constrained to 1ms. - * @param win the mock window instance to update - */ -export function constrainTimeouts(win: any) { - (win as MockWindow).__allowInterval = false; - (win as MockWindow).__maxTimeout = 0; -} - -function resetWindow(win: MockWindow) { - if (win != null) { - if (win.__timeouts) { - win.__timeouts.forEach((timeoutId) => { - nativeClearInterval(timeoutId); - nativeClearTimeout(timeoutId); - }); - win.__timeouts.clear(); - } - if (win.customElements && (win.customElements as MockCustomElementRegistry).clear) { - (win.customElements as MockCustomElementRegistry).clear(); - } - - resetDocument(win.document); - resetPerformance(win.performance); - - for (const key in win) { - if (win.hasOwnProperty(key) && key !== 'document' && key !== 'performance' && key !== 'customElements') { - delete (win as any)[key]; - } - } - resetWindowDefaults(win); - resetWindowDimensions(win); - resetEventListeners(win); - - if (win.document != null) { - try { - (win.document as any).defaultView = win; - } catch (e) {} - } - - // ensure we don't hold onto nodeFetch values - (win as any).fetch = null; - (win as any).Headers = null; - (win as any).Request = null; - (win as any).Response = null; - (win as any).FetchError = null; - } -} - -function resetWindowDimensions(win: MockWindow) { - try { - win.devicePixelRatio = 1; - - win.innerHeight = 768; - win.innerWidth = 1366; - - win.pageXOffset = 0; - win.pageYOffset = 0; - - win.screenLeft = 0; - win.screenTop = 0; - win.screenX = 0; - win.screenY = 0; - win.scrollX = 0; - win.scrollY = 0; - - win.screen = { - availHeight: win.innerHeight, - availLeft: 0, - availTop: 0, - availWidth: win.innerWidth, - colorDepth: 24, - height: win.innerHeight, - keepAwake: false, - orientation: { - angle: 0, - type: 'portrait-primary', - } as any, - pixelDepth: 24, - width: win.innerWidth, - } as any; - } catch (e) {} -} diff --git a/src/runtime/asset-path.ts b/src/runtime/asset-path.ts deleted file mode 100644 index 9c6f6499180..00000000000 --- a/src/runtime/asset-path.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { plt, win } from '@platform'; - -export const getAssetPath = (path: string) => { - const assetUrl = new URL(path, plt.$resourcesUrl$); - return assetUrl.origin !== win.location.origin ? assetUrl.href : assetUrl.pathname; -}; - -export const setAssetPath = (path: string) => (plt.$resourcesUrl$ = path); diff --git a/src/runtime/bootstrap-custom-element.ts b/src/runtime/bootstrap-custom-element.ts deleted file mode 100644 index 1f6676df5bc..00000000000 --- a/src/runtime/bootstrap-custom-element.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { BUILD } from '@app-data'; -import { - addHostEventListeners, - consoleError, - forceUpdate, - getHostRef, - registerHost, - styles, - supportsShadow, - transformTag, -} from '@platform'; - -import type * as d from '../declarations'; -import { CMP_FLAGS } from '../utils/constants'; -import { createShadowRoot } from '../utils/shadow-root'; -import { connectedCallback } from './connected-callback'; -import { disconnectedCallback } from './disconnected-callback'; -import { - patchChildSlotNodes, - patchCloneNode, - patchPseudoShadowDom, - patchSlotAppendChild, - patchTextContent, -} from './dom-extras'; -import { computeMode } from './mode'; -import { normalizeWatchers } from './normalize-watchers'; -import { proxyComponent } from './proxy-component'; -import { PROXY_FLAGS } from './runtime-constants'; -import { attachStyles, getScopeId, hydrateScopedToShadow, registerStyle } from './styles'; - -export const defineCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMetaCompact) => { - customElements.define( - transformTag(compactMeta[1]), - proxyCustomElement(Cstr, compactMeta) as CustomElementConstructor, - ); -}; - -export const proxyCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMetaCompact) => { - const cmpMeta: d.ComponentRuntimeMeta = { - $flags$: compactMeta[0], - $tagName$: compactMeta[1], - }; - try { - if (BUILD.member) { - cmpMeta.$members$ = compactMeta[2]; - } - if (BUILD.hostListener) { - cmpMeta.$listeners$ = compactMeta[3]; - } - if (BUILD.propChangeCallback) { - cmpMeta.$watchers$ = normalizeWatchers(Cstr.$watchers$); - cmpMeta.$deserializers$ = Cstr.$deserializers$; - cmpMeta.$serializers$ = Cstr.$serializers$; - } - if (BUILD.reflect) { - cmpMeta.$attrsToReflect$ = []; - } - if (BUILD.shadowDom && !supportsShadow && cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) { - // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field - cmpMeta.$flags$ |= CMP_FLAGS.needsShadowDomShim; - } - - if (!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && cmpMeta.$flags$ & CMP_FLAGS.hasSlot) { - if (BUILD.experimentalSlotFixes) { - patchPseudoShadowDom(Cstr.prototype); - } else { - if (BUILD.slotChildNodesFix) { - patchChildSlotNodes(Cstr.prototype); - } - if (BUILD.cloneNodeFix) { - patchCloneNode(Cstr.prototype); - } - if (BUILD.appendChildSlotFix) { - patchSlotAppendChild(Cstr.prototype); - } - if (BUILD.scopedSlotTextContentFix && cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation) { - patchTextContent(Cstr.prototype); - } - } - } else if (BUILD.cloneNodeFix) { - patchCloneNode(Cstr.prototype); - } - - if (BUILD.hydrateClientSide && BUILD.shadowDom) { - hydrateScopedToShadow(); - } - - const originalConnectedCallback = Cstr.prototype.connectedCallback; - const originalDisconnectedCallback = Cstr.prototype.disconnectedCallback; - Object.assign(Cstr.prototype, { - __hasHostListenerAttached: false, - __registerHost() { - registerHost(this, cmpMeta); - }, - connectedCallback() { - if (!this.__hasHostListenerAttached) { - const hostRef = getHostRef(this); - if (!hostRef) { - return; - } - addHostEventListeners(this, hostRef, cmpMeta.$listeners$, false); - this.__hasHostListenerAttached = true; - } - - connectedCallback(this); - if (originalConnectedCallback) { - originalConnectedCallback.call(this); - } - }, - disconnectedCallback() { - disconnectedCallback(this); - if (originalDisconnectedCallback) { - originalDisconnectedCallback.call(this); - } - }, - __attachShadow() { - if (supportsShadow) { - if (!this.shadowRoot) { - createShadowRoot.call(this, cmpMeta); - } else { - // we want to check to make sure that the mode for the shadow - // root already attached to the element (i.e. created via DSD) - // is set to 'open' since that's the only mode we support - if (this.shadowRoot.mode !== 'open') { - throw new Error( - `Unable to re-use existing shadow root for ${cmpMeta.$tagName$}! Mode is set to ${this.shadowRoot.mode} but Stencil only supports open shadow roots.`, - ); - } - } - } else { - (this as any).shadowRoot = this; - } - }, - }); - Object.defineProperty(Cstr, 'is', { - value: cmpMeta.$tagName$, - configurable: true, - }); - - return proxyComponent(Cstr, cmpMeta, PROXY_FLAGS.isElementConstructor | PROXY_FLAGS.proxyState); - } catch (e) { - consoleError(e); - return Cstr; - } -}; - -export const forceModeUpdate = (elm: d.RenderNode) => { - if (BUILD.style && BUILD.mode && !BUILD.lazyLoad) { - const mode = computeMode(elm); - const hostRef = getHostRef(elm); - - if (hostRef && hostRef.$modeName$ !== mode) { - const cmpMeta = hostRef.$cmpMeta$; - const oldScopeId = elm['s-sc']; - const scopeId = getScopeId(cmpMeta, mode); - const style = (elm.constructor as any).style[mode]; - const flags = cmpMeta.$flags$; - if (style) { - if (!styles.has(scopeId)) { - registerStyle(scopeId, style, !!(flags & CMP_FLAGS.shadowDomEncapsulation)); - } - hostRef.$modeName$ = mode; - elm.classList.remove(oldScopeId + '-h', oldScopeId + '-s'); - attachStyles(hostRef); - forceUpdate(elm); - } - } - } -}; diff --git a/src/runtime/bootstrap-lazy.ts b/src/runtime/bootstrap-lazy.ts deleted file mode 100644 index 98357ac6769..00000000000 --- a/src/runtime/bootstrap-lazy.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { BUILD } from '@app-data'; -import { getHostRef, plt, registerHost, supportsShadow, transformTag, win } from '@platform'; -import { addHostEventListeners } from '@runtime'; - -import type * as d from '../declarations'; -import { CMP_FLAGS } from '../utils/constants'; -import { queryNonceMetaTagContent } from '../utils/query-nonce-meta-tag-content'; -import { createShadowRoot } from '../utils/shadow-root'; -import { connectedCallback } from './connected-callback'; -import { disconnectedCallback } from './disconnected-callback'; -import { - patchChildSlotNodes, - patchCloneNode, - patchPseudoShadowDom, - patchSlotAppendChild, - patchTextContent, -} from './dom-extras'; -import { hmrStart } from './hmr-component'; -import { normalizeWatchers } from './normalize-watchers'; -import { createTime, installDevTools } from './profile'; -import { proxyComponent } from './proxy-component'; -import { HYDRATED_CSS, PLATFORM_FLAGS, PROXY_FLAGS, SLOT_FB_CSS } from './runtime-constants'; -import { hydrateScopedToShadow } from './styles'; -import { appDidLoad } from './update-component'; -export { setNonce } from '@platform'; - -export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.CustomElementsDefineOptions = {}) => { - if (BUILD.profile && performance.mark) { - performance.mark('st:app:start'); - } - installDevTools(); - - if (!win.document) { - console.warn('Stencil: No document found. Skipping bootstrapping lazy components.'); - return; - } - - const endBootstrap = createTime('bootstrapLazy'); - const cmpTags: string[] = []; - const exclude = options.exclude || []; - const customElements = win.customElements; - const head = win.document.head; - const metaCharset = /*@__PURE__*/ head.querySelector('meta[charset]'); - const dataStyles = /*@__PURE__*/ win.document.createElement('style'); - const deferredConnectedCallbacks: { connectedCallback: () => void }[] = []; - let appLoadFallback: any; - let isBootstrapping = true; - - Object.assign(plt, options); - plt.$resourcesUrl$ = new URL(options.resourcesUrl || './', win.document.baseURI).href; - if (BUILD.asyncQueue) { - if (options.syncQueue) { - plt.$flags$ |= PLATFORM_FLAGS.queueSync; - } - } - if (BUILD.hydrateClientSide) { - // If the app is already hydrated there is not point to disable the - // async queue. This will improve the first input delay - plt.$flags$ |= PLATFORM_FLAGS.appLoaded; - } - - if (BUILD.hydrateClientSide && BUILD.shadowDom) { - hydrateScopedToShadow(); - } - - let hasSlotRelocation = false; - lazyBundles.map((lazyBundle) => { - lazyBundle[1].map((compactMeta) => { - const cmpMeta: d.ComponentRuntimeMeta = { - $flags$: compactMeta[0], - $tagName$: compactMeta[1], - $members$: compactMeta[2], - $listeners$: compactMeta[3], - }; - - // Check if we are using slots outside the shadow DOM in this component. - // We'll use this information later to add styles for `slot-fb` elements - if (cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation) { - hasSlotRelocation = true; - } - - if (BUILD.member) { - cmpMeta.$members$ = compactMeta[2]; - } - if (BUILD.hostListener) { - cmpMeta.$listeners$ = compactMeta[3]; - } - if (BUILD.reflect) { - cmpMeta.$attrsToReflect$ = []; - } - if (BUILD.propChangeCallback) { - // Watchers need normalization because the compiler format changed in - // 4.39.x (string[] → { [method]: flags }[]). Libraries compiled with - // an older Stencil may still emit the legacy format. Serializers and - // deserializers were introduced after that change, so their format is - // always current and only needs a nullish fallback. - cmpMeta.$watchers$ = normalizeWatchers(compactMeta[4]); - cmpMeta.$serializers$ = compactMeta[5] ?? {}; - cmpMeta.$deserializers$ = compactMeta[6] ?? {}; - } - if (BUILD.shadowDom && !supportsShadow && cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) { - // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field - cmpMeta.$flags$ |= CMP_FLAGS.needsShadowDomShim; - } - // TODO: deprecated in favour of `setTagTransformer` and `transformTag`. Remove `BUILD.transformTagName` & `transformTagName` in 5.0 - const tagName = - BUILD.transformTagName && options.transformTagName - ? options.transformTagName(cmpMeta.$tagName$) - : transformTag(cmpMeta.$tagName$); - const HostElement = class extends HTMLElement { - ['s-p']: Promise[]; - ['s-rc']: (() => void)[]; - hasRegisteredEventListeners = false; - - // StencilLazyHost - constructor(self: HTMLElement) { - // @ts-ignore - super(self); - self = this; - - registerHost(self, cmpMeta); - if (BUILD.shadowDom && cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) { - // this component is using shadow dom - // and this browser supports shadow dom - // add the read-only property "shadowRoot" to the host element - // adding the shadow root build conditionals to minimize runtime - if (supportsShadow) { - if (!self.shadowRoot) { - // we don't want to call `attachShadow` if there's already a shadow root - // attached to the component - createShadowRoot.call(self, cmpMeta); - } else { - // we want to check to make sure that the mode for the shadow - // root already attached to the element (i.e. created via DSD) - // is set to 'open' since that's the only mode we support - if (self.shadowRoot.mode !== 'open') { - throw new Error( - `Unable to re-use existing shadow root for ${cmpMeta.$tagName$}! Mode is set to ${self.shadowRoot.mode} but Stencil only supports open shadow roots.`, - ); - } - } - } else if (!BUILD.hydrateServerSide && !('shadowRoot' in self)) { - (self as any).shadowRoot = self; - } - } - } - - connectedCallback() { - const hostRef = getHostRef(this); - if (!hostRef) { - return; - } - - /** - * The `connectedCallback` lifecycle event can potentially be fired multiple times - * if the element is removed from the DOM and re-inserted. This is not a common use case, - * but it can happen in some scenarios. To prevent registering the same event listeners - * multiple times, we will only register them once. - */ - if (!this.hasRegisteredEventListeners) { - this.hasRegisteredEventListeners = true; - addHostEventListeners(this, hostRef, cmpMeta.$listeners$, false); - } - - if (appLoadFallback) { - clearTimeout(appLoadFallback); - appLoadFallback = null; - } - if (isBootstrapping) { - // connectedCallback will be processed once all components have been registered - deferredConnectedCallbacks.push(this); - } else { - plt.jmp(() => connectedCallback(this)); - } - } - - disconnectedCallback() { - plt.jmp(() => disconnectedCallback(this)); - - /** - * Clear up references within the `$vnode$` object to the DOM - * node that was removed. This is necessary to ensure that these - * references used as keys in the `hostRef` object can be properly - * garbage collected. - * - * Also remove the reference from `deferredConnectedCallbacks` array - * otherwise removed instances won't get garbage collected. - */ - plt.raf(() => { - const hostRef = getHostRef(this); - if (!hostRef) { - return; - } - const i = deferredConnectedCallbacks.findIndex((host) => host === this); - if (i > -1) { - deferredConnectedCallbacks.splice(i, 1); - } - if (hostRef?.$vnode$?.$elm$ instanceof Node && !hostRef.$vnode$.$elm$.isConnected) { - delete hostRef.$vnode$.$elm$; - } - }); - } - - componentOnReady() { - return getHostRef(this)?.$onReadyPromise$; - } - }; - - if (!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && cmpMeta.$flags$ & CMP_FLAGS.hasSlot) { - if (BUILD.experimentalSlotFixes) { - patchPseudoShadowDom(HostElement.prototype); - } else { - if (BUILD.slotChildNodesFix) { - patchChildSlotNodes(HostElement.prototype); - } - if (BUILD.cloneNodeFix) { - patchCloneNode(HostElement.prototype); - } - if (BUILD.appendChildSlotFix) { - patchSlotAppendChild(HostElement.prototype); - } - if (BUILD.scopedSlotTextContentFix && cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation) { - patchTextContent(HostElement.prototype); - } - } - } else if (BUILD.cloneNodeFix) { - patchCloneNode(HostElement.prototype); - } - - // if the component is formAssociated we need to set that on the host - // element so that it will be ready for `attachInternals` to be called on - // it later on - if (BUILD.formAssociated && cmpMeta.$flags$ & CMP_FLAGS.formAssociated) { - (HostElement as any).formAssociated = true; - } - - if (BUILD.hotModuleReplacement) { - // if we're in an HMR dev build then we need to set up the callback - // which will carry out the work of actually replacing the module for - // this particular component - ((HostElement as any).prototype as d.HostElement)['s-hmr'] = function (hmrVersionId: string) { - hmrStart(this, cmpMeta, hmrVersionId); - }; - } - - cmpMeta.$lazyBundleId$ = lazyBundle[0]; - - if (!exclude.includes(tagName) && !customElements.get(tagName)) { - cmpTags.push(tagName); - customElements.define( - tagName, - proxyComponent(HostElement as any, cmpMeta, PROXY_FLAGS.isElementConstructor) as any, - ); - } - }); - }); - - // Only bother generating CSS if we have components - // TODO(STENCIL-1118): Add test cases for CSS content based on conditionals - if (cmpTags.length > 0) { - // Add styles for `slot-fb` elements if any of our components are using slots outside the Shadow DOM - if (BUILD.slotRelocation && hasSlotRelocation) { - dataStyles.textContent += SLOT_FB_CSS; - } - - // Add hydration styles - if (BUILD.invisiblePrehydration && (BUILD.hydratedClass || BUILD.hydratedAttribute)) { - dataStyles.textContent += cmpTags.sort() + HYDRATED_CSS; - } - - // If we have styles, add them to the DOM - if (dataStyles.innerHTML.length) { - dataStyles.setAttribute('data-styles', ''); - - // Apply CSP nonce to the style tag if it exists - const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document); - if (nonce != null) { - dataStyles.setAttribute('nonce', nonce); - } - - // Insert the styles into the document head - // NOTE: this _needs_ to happen last so we can ensure the nonce (and other attributes) are applied - head.insertBefore(dataStyles, metaCharset ? metaCharset.nextSibling : head.firstChild); - } - } - - // Process deferred connectedCallbacks now all components have been registered - isBootstrapping = false; - if (deferredConnectedCallbacks.length) { - deferredConnectedCallbacks.map((host) => host.connectedCallback()); - } else { - if (BUILD.profile) { - plt.jmp(() => (appLoadFallback = setTimeout(appDidLoad, 30, 'timeout'))); - } else { - plt.jmp(() => (appLoadFallback = setTimeout(appDidLoad, 30))); - } - } - // Fallback appLoad event - endBootstrap(); -}; diff --git a/src/runtime/connected-callback.ts b/src/runtime/connected-callback.ts deleted file mode 100644 index ab83326c8f4..00000000000 --- a/src/runtime/connected-callback.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { BUILD } from '@app-data'; -import { addHostEventListeners, getHostRef, nextTick, plt, supportsShadow, win } from '@platform'; - -import type * as d from '../declarations'; -import { CMP_FLAGS, HOST_FLAGS, MEMBER_FLAGS } from '../utils/constants'; -import { initializeClientHydrate } from './client-hydrate'; -import { fireConnectedCallback, initializeComponent } from './initialize-component'; -import { createTime } from './profile'; -import { HYDRATE_ID, NODE_TYPE, PLATFORM_FLAGS } from './runtime-constants'; -import { addStyle, getScopeId } from './styles'; -import { attachToAncestor } from './update-component'; -import { insertBefore } from './vdom/vdom-render'; - -export const connectedCallback = (elm: d.HostElement) => { - if ((plt.$flags$ & PLATFORM_FLAGS.isTmpDisconnected) === 0) { - const hostRef = getHostRef(elm); - if (!hostRef) { - return; - } - - const cmpMeta = hostRef.$cmpMeta$; - const endConnected = createTime('connectedCallback', cmpMeta.$tagName$); - - if (BUILD.hostListenerTargetParent) { - // only run if we have listeners being attached to a parent - addHostEventListeners(elm, hostRef, cmpMeta.$listeners$, true); - } - - if (!(hostRef.$flags$ & HOST_FLAGS.hasConnected)) { - // first time this component has connected - hostRef.$flags$ |= HOST_FLAGS.hasConnected; - - let hostId: string; - if (BUILD.hydrateClientSide) { - hostId = elm.getAttribute(HYDRATE_ID); - if (hostId) { - if (BUILD.shadowDom && supportsShadow && cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) { - const scopeId = BUILD.mode - ? addStyle(elm.shadowRoot, cmpMeta, elm.getAttribute('s-mode')) - : addStyle(elm.shadowRoot, cmpMeta); - elm.classList.remove(scopeId + '-h', scopeId + '-s'); - } else if (BUILD.scoped && cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation) { - // set the scope id on the element now. Useful when hydrating, - // to more quickly set the initial scoped classes for scoped css - const scopeId = getScopeId(cmpMeta, BUILD.mode ? elm.getAttribute('s-mode') : undefined); - elm['s-sc'] = scopeId; - } - initializeClientHydrate(elm, cmpMeta.$tagName$, hostId, hostRef); - } - } - - if (BUILD.slotRelocation && !hostId) { - // initUpdate - // if the slot polyfill is required we'll need to put some nodes - // in here to act as original content anchors as we move nodes around - // host element has been connected to the DOM - if ( - BUILD.hydrateServerSide || - ((BUILD.slot || BUILD.shadowDom) && - // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field - cmpMeta.$flags$ & (CMP_FLAGS.hasSlotRelocation | CMP_FLAGS.needsShadowDomShim)) - ) { - setContentReference(elm); - } - } - - if (BUILD.asyncLoading) { - // find the first ancestor component (if there is one) and register - // this component as one of the actively loading child components for its ancestor - let ancestorComponent = elm; - - while ((ancestorComponent = (ancestorComponent.parentNode as any) || (ancestorComponent.host as any))) { - // climb up the ancestors looking for the first - // component that hasn't finished its lifecycle update yet - if ( - (BUILD.hydrateClientSide && - ancestorComponent.nodeType === NODE_TYPE.ElementNode && - ancestorComponent.hasAttribute('s-id') && - ancestorComponent['s-p']) || - ancestorComponent['s-p'] - ) { - // we found this components first ancestor component - // keep a reference to this component's ancestor component - attachToAncestor(hostRef, (hostRef.$ancestorComponent$ = ancestorComponent)); - break; - } - } - } - - // Lazy properties - // https://developers.google.com/web/fundamentals/web-components/best-practices#lazy-properties - if (BUILD.prop && !BUILD.hydrateServerSide && cmpMeta.$members$) { - Object.entries(cmpMeta.$members$).map(([memberName, [memberFlags]]) => { - if (memberFlags & MEMBER_FLAGS.Prop && Object.prototype.hasOwnProperty.call(elm, memberName)) { - const value = (elm as any)[memberName]; - delete (elm as any)[memberName]; - (elm as any)[memberName] = value; - } - }); - } - - if (BUILD.initializeNextTick) { - // connectedCallback, taskQueue, initialLoad - // angular sets attribute AFTER connectCallback - // https://github.com/angular/angular/issues/18909 - // https://github.com/angular/angular/issues/19940 - nextTick(() => initializeComponent(elm, hostRef, cmpMeta)); - } else { - initializeComponent(elm, hostRef, cmpMeta); - } - } else { - // not the first time this has connected - - // reattach any event listeners to the host - // since they would have been removed when disconnected - addHostEventListeners(elm, hostRef, cmpMeta.$listeners$, false); - - // fire off connectedCallback() on component instance - if (hostRef?.$lazyInstance$) { - fireConnectedCallback(hostRef.$lazyInstance$, elm); - } else if (hostRef?.$onReadyPromise$) { - hostRef.$onReadyPromise$.then(() => fireConnectedCallback(hostRef.$lazyInstance$, elm)); - } - } - - endConnected(); - } -}; - -const setContentReference = (elm: d.HostElement) => { - if (!win.document) { - return; - } - - // only required when we're NOT using native shadow dom (slot) - // or this browser doesn't support native shadow dom - // and this host element was NOT created with SSR - // let's pick out the inner content for slot projection - // create a node to represent where the original - // content was first placed, which is useful later on - const contentRefElm = (elm['s-cr'] = win.document.createComment( - BUILD.isDebug ? `content-ref (host=${elm.localName})` : '', - ) as any); - contentRefElm['s-cn'] = true; - insertBefore(elm, contentRefElm, elm.firstChild as d.RenderNode); -}; diff --git a/src/runtime/element.ts b/src/runtime/element.ts deleted file mode 100644 index a5dc8627f5a..00000000000 --- a/src/runtime/element.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { BUILD } from '@app-data'; -import { getHostRef } from '@platform'; - -import type * as d from '../declarations'; - -export const getElement = (ref: any) => (BUILD.lazyLoad ? getHostRef(ref)?.$hostElement$ : (ref as d.HostElement)); diff --git a/src/runtime/hmr-component.ts b/src/runtime/hmr-component.ts deleted file mode 100644 index c405c2a55f4..00000000000 --- a/src/runtime/hmr-component.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { getHostRef } from '@platform'; - -import type * as d from '../declarations'; -import { HOST_FLAGS } from '../utils/constants'; -import { initializeComponent } from './initialize-component'; - -/** - * Kick off hot-module-replacement for a component. In order to replace the - * component in-place we: - * - * 1. get a reference to the {@link d.HostRef} for the element - * 2. reset the element's runtime flags - * 3. re-run the initialization logic for the element (via - * {@link initializeComponent}) - * - * @param hostElement the host element for the component which we want to start - * doing HMR - * @param cmpMeta runtime metadata for the component - * @param hmrVersionId the current HMR version ID - */ -export const hmrStart = (hostElement: d.HostElement, cmpMeta: d.ComponentRuntimeMeta, hmrVersionId: string) => { - // ¯\_(ツ)_/¯ - const hostRef = getHostRef(hostElement); - if (!hostRef) { - return; - } - - // reset state flags to only have been connected - hostRef.$flags$ = HOST_FLAGS.hasConnected; - - // TODO - // detach any event listeners that may have been added - // because we're not passing an exact event name it'll - // remove all of this element's event, which is good - - // re-initialize the component - initializeComponent(hostElement, hostRef, cmpMeta, hmrVersionId); -}; diff --git a/src/runtime/host-listener.ts b/src/runtime/host-listener.ts deleted file mode 100644 index d7fbd203381..00000000000 --- a/src/runtime/host-listener.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { BUILD } from '@app-data'; -import { consoleError, plt, supportsListenerOptions, win } from '@platform'; - -import type * as d from '../declarations'; -import { HOST_FLAGS, LISTENER_FLAGS } from '../utils/constants'; - -export const addHostEventListeners = ( - elm: d.HostElement, - hostRef: d.HostRef, - listeners?: d.ComponentRuntimeHostListener[], - attachParentListeners?: boolean, -) => { - if (BUILD.hostListener && listeners && win.document) { - // this is called immediately within the element's constructor - // initialize our event listeners on the host element - // we do this now so that we can listen to events that may - // have fired even before the instance is ready - - if (BUILD.hostListenerTargetParent) { - // this component may have event listeners that should be attached to the parent - if (attachParentListeners) { - // this is being ran from within the connectedCallback - // which is important so that we know the host element actually has a parent element - // filter out the listeners to only have the ones that ARE being attached to the parent - listeners = listeners.filter(([flags]) => flags & LISTENER_FLAGS.TargetParent); - } else { - // this is being ran from within the component constructor - // everything BUT the parent element listeners should be attached at this time - // filter out the listeners that are NOT being attached to the parent - listeners = listeners.filter(([flags]) => !(flags & LISTENER_FLAGS.TargetParent)); - } - } - - listeners.map(([flags, name, method]) => { - const target = BUILD.hostListenerTarget ? getHostListenerTarget(win.document, elm, flags) : elm; - const handler = hostListenerProxy(hostRef, method); - const opts = hostListenerOpts(flags); - plt.ael(target, name, handler, opts); - (hostRef.$rmListeners$ = hostRef.$rmListeners$ || []).push(() => plt.rel(target, name, handler, opts)); - }); - } -}; - -const hostListenerProxy = (hostRef: d.HostRef, methodName: string) => (ev: Event) => { - try { - if (BUILD.lazyLoad) { - if (hostRef.$flags$ & HOST_FLAGS.isListenReady) { - // instance is ready, let's call it's member method for this event - hostRef.$lazyInstance$?.[methodName](ev); - } else { - (hostRef.$queuedListeners$ = hostRef.$queuedListeners$ || []).push([methodName, ev]); - } - } else { - (hostRef.$hostElement$ as any)[methodName](ev); - } - } catch (e) { - consoleError(e, hostRef.$hostElement$); - } -}; - -const getHostListenerTarget = (doc: Document, elm: Element, flags: number): EventTarget => { - if (BUILD.hostListenerTargetDocument && flags & LISTENER_FLAGS.TargetDocument) { - return doc; - } - if (BUILD.hostListenerTargetWindow && flags & LISTENER_FLAGS.TargetWindow) { - return win; - } - if (BUILD.hostListenerTargetBody && flags & LISTENER_FLAGS.TargetBody) { - return doc.body; - } - if (BUILD.hostListenerTargetParent && flags & LISTENER_FLAGS.TargetParent && elm.parentElement) { - return elm.parentElement; - } - - return elm; -}; - -// prettier-ignore -const hostListenerOpts = (flags: number) => - supportsListenerOptions - ? ({ - passive: (flags & LISTENER_FLAGS.Passive) !== 0, - capture: (flags & LISTENER_FLAGS.Capture) !== 0, - }) - : (flags & LISTENER_FLAGS.Capture) !== 0; diff --git a/src/runtime/index.ts b/src/runtime/index.ts deleted file mode 100644 index 4420de1718d..00000000000 --- a/src/runtime/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -export { getAssetPath, setAssetPath } from './asset-path'; -export { defineCustomElement, forceModeUpdate, proxyCustomElement } from './bootstrap-custom-element'; -export { bootstrapLazy } from './bootstrap-lazy'; -export { connectedCallback } from './connected-callback'; -export { disconnectedCallback } from './disconnected-callback'; -export { getElement } from './element'; -export { createEvent } from './event-emitter'; -export { Fragment } from './fragment'; -export { addHostEventListeners } from './host-listener'; -export { Mixin } from './mixin'; -export { getMode, setMode } from './mode'; -export { setNonce } from './nonce'; -export { normalizeWatchers } from './normalize-watchers'; -export { parsePropertyValue } from './parse-property-value'; -export { setPlatformOptions } from './platform-options'; -export { proxyComponent } from './proxy-component'; -export { render } from './render'; -export { HYDRATED_STYLE_ID } from './runtime-constants'; -export { getValue, setValue } from './set-value'; -export { setTagTransformer, transformTag } from './tag-transform'; -export { forceUpdate, getRenderingRef, postUpdateComponent } from './update-component'; -export { h, Host } from './vdom/h'; -export { jsxDEV } from './vdom/jsx-dev-runtime'; -export { jsx, jsxs } from './vdom/jsx-runtime'; -export { insertVdomAnnotations } from './vdom/vdom-annotations'; -export { renderVdom } from './vdom/vdom-render'; diff --git a/src/runtime/mode.ts b/src/runtime/mode.ts deleted file mode 100644 index 489e7d113d9..00000000000 --- a/src/runtime/mode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getHostRef, modeResolutionChain } from '@platform'; - -import type * as d from '../declarations'; - -// Private -export const computeMode = (elm: d.HostElement) => modeResolutionChain.map((h) => h(elm)).find((m) => !!m); - -// Public -export const setMode = (handler: d.ResolutionHandler) => modeResolutionChain.push(handler); -export const getMode = (ref: d.RuntimeRef) => getHostRef(ref)?.$modeName$; diff --git a/src/runtime/readme.md b/src/runtime/readme.md deleted file mode 100644 index c0003c0027b..00000000000 --- a/src/runtime/readme.md +++ /dev/null @@ -1,126 +0,0 @@ -## Lifecycle Order Of Operations - -Component lifecycle events fire `componentWillLoad` from top to bottom, then fire `componentDidLoad` from bottom to top. It should take into account each component can finish lazy-loaded requests in any random order. Additionally, any `componentWillLoad` can return a promise that all child components should wait on until it's resolved, while still keeping the correct firing order. - -``` - - - - - - -cmp-a - componentWillLoad -cmp-b - componentWillLoad -cmp-c - componentWillLoad -cmp-c - componentDidLoad -cmp-b - componentDidLoad -cmp-a - componentDidLoad -``` - - -## Hydrated CSS Visibility - -By default, components are assigned `visibility: hidden` using their tag name as the css selector. Therefore, before the components and their descendants have finished hydrating, each component is hidden by default. This is done to prevent janky flickering as components hydrate asynchronously. As each component fully loads the `hydrated` css class is then added. - -The `hydrated` css class that's added to the component assigns `visibility: inherit` style to the element. If any parent component is still hydrating then this component will not show until the top most component has added the `hydrated` css class. - - - -## Lifecycle Process - -- **Connect**: Synchronously within `connectedCallback`, each component looks for an ancestor component and adds itself as a child component if an ancestor is found. - - - Climb up the parent elements with a while loop. - - - Stop at the first element that has an `s-init` function. - - - If the ancestor component we found hasn't ran its lifecycle update yet, then add this component to the ancestor's `s-al` set. The `s-al` is a set of child components that are actively loading. - - - If no ancestor component is found then continue without the component setting an ancestor component. - - -- **Initialize Component**: Initialize the component for the first time within `initializeComponent`. - - - If the component has already initialized loading then do nothing. Data to know if the component has started to initialize is in the host ref data, which ensures it doesn't try to initialize more than once. - - - Async request the lazy-loaded component constructor and await the response. - - - After the component implementation constructor request has been received, create a new instance of the component with the lazy-loaded constructor. - - - The constructor will directly wire the host element and lazy-loaded component instance together with the host ref data. - - - If the component has an ancestor component, but the ancestor hasn't ran its lifecycle update yet, then this component should not be initialized at this moment and shouldn't fire its `componentWillLoad` yet. Instead, this component should be added to the ancestor component's array of render callbacks `s-rc`, which would call `initializeComponent` again after its ready. Once the ancestor component has ran its lifecycle update, it'll then call all of its child render callbacks so that the `componentWillLoad` lifecycle events are in the correct order. - - - If there is no ancestor component, or the ancestor component has already rendered, then fire off the first update. - - - When ready, `updateComponent` will be added as an async write task and ran asynchronously. - - -- **First Update**: The first component update and render from within `updateComponent`. - - - Set the lifecycle ready value `s-lr` to `false` signifying that the lifecycle update is not ready for this component. - - - Fire off `componentWillLoad` lifecycle. - - - Fire off `componentWillRender` lifecycle. - - - Add scoped css data and classes for scoped encapsulation or shadow dom encapsulation without shadow dom browser support. - - - Attach shadow root for shadow dom components. - - - Attach styles to shadow root or document depending on encapsulation. - - - First render. - - - Set the lifecycle ready value `s-lr` to `true` signifying that the lifecycle update has happened and the component is now ready for child component lifecycles. - - - Fire off all of this component's child render callbacks within `s-rc`. Each of the child render callbacks will fire off their own initialize component process. - - - All component descendants should fire `componentWillLoad` lifecycle in the correct order, top to bottom. - - - Fire `postUpdateComponent`. The bottom most component will not have any child render callbacks, so at this point the `componentDidLoad` lifecycle events should start firing from bottom to top. - - - Fire off `componentDidLoad` lifecycle. - - - Fire off `componentDidRender` lifecycle. - - - Add `hydrated` css class signifying the component has finished loading. At this point this component has finished updating. - - - If the component has an ancestor component, then remove this component from its set of actively loading children in `s-al`. - - - After removing this component from the ancestor component's `s-al` set, if the set is now empty then fire the ancestor component's `s-init`. - - - Firing `s-init` on the ancestor component allows the ancestor to complete its first update and fire its own `componentDidLoad` lifecycle event, allowing for `componentDidLoad` lifecycles to fire bottom to top. - - - Fire all `componentOnReady` resolves. - - -- **Subsequent Updates**: All subsequent component updates and re-renders from within `updateComponent`. - - - Somehow `setValue` is triggered, either through a `Prop` or `State` update, or calling `forceUpdate()` on a component. If there is a change or a forced update, then `setValue` will add `updateComponent` to an async write task. - - - Fire `updateComponent` from async task queue. - - - Fire off `componentWillUpdate` lifecycle. - - - Fire off `componentWillRender` lifecycle. - - - Patch render. - - - Fire `postUpdateComponent`. - - - Fire off `componentDidUpdate` lifecycle. - - - Fire off `componentDidRender` lifecycle. - - - -## Property Descriptions - -`s-al`: A component's `Set` of child components that are actively loading. - -`s-init`: A function to be called by child components to finish initializing the component. - -`s-lr`: The component's lifecycle ready status. `true` if the component has finished its lifecycle update, falsy if it is actively updating and has not fired off either `componentWillLoad` or `componentWillUpdate`. - -`s-rc`: A component's array of child component render callbacks. After a component renders, it should then fire off all of its child component render callbacks. \ No newline at end of file diff --git a/src/runtime/render.ts b/src/runtime/render.ts deleted file mode 100644 index 1813a33a239..00000000000 --- a/src/runtime/render.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type * as d from '../declarations'; -import { renderVdom } from './vdom/vdom-render'; - -/** - * A WeakMap to persist HostRef objects across multiple render() calls to the - * same container. This enables VNode diffing on re-renders — without it, each - * call creates a fresh HostRef with no previous VNode, causing renderVdom to - * replace the entire DOM subtree instead of patching only what changed. - */ -const hostRefCache = new WeakMap(); - -/** - * Method to render a virtual DOM tree to a container element. - * - * Supports efficient re-renders: calling `render()` again on the same container - * will diff the new VNode tree against the previous one and only update what changed, - * preserving existing DOM elements and their state. - * - * @example - * ```tsx - * import { render } from '@stencil/core'; - * - * const vnode = ( - *
    - *

    Hello, world!

    - *
    - * ); - * render(vnode, document.body); - * ``` - * - * @param vnode - The virtual DOM tree to render - * @param container - The container element to render the virtual DOM tree to - */ -export function render(vnode: d.VNode, container: Element) { - let ref = hostRefCache.get(container); - - if (!ref) { - const cmpMeta: d.ComponentRuntimeMeta = { - $flags$: 0, - $tagName$: container.tagName, - }; - - ref = { - $flags$: 0, - $cmpMeta$: cmpMeta, - $hostElement$: container as d.HostElement, - }; - - hostRefCache.set(container, ref); - } - - renderVdom(ref, vnode); -} diff --git a/src/runtime/runtime-constants.ts b/src/runtime/runtime-constants.ts deleted file mode 100644 index 8af3d699237..00000000000 --- a/src/runtime/runtime-constants.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Bit flags for recording various properties of VDom nodes - */ -export const enum VNODE_FLAGS { - /** - * Whether or not a vdom node is a slot reference - */ - isSlotReference = 1 << 0, - - /** - * Whether or not a slot element has fallback content - */ - isSlotFallback = 1 << 1, - - /** - * Whether or not an element is a host element - */ - isHost = 1 << 2, -} - -export const enum PROXY_FLAGS { - isElementConstructor = 1 << 0, - proxyState = 1 << 1, -} - -export const enum PLATFORM_FLAGS { - /** - * designates a node in the DOM as being actively moved by the runtime - */ - isTmpDisconnected = 1 << 0, - appLoaded = 1 << 1, - queueSync = 1 << 2, - - queueMask = appLoaded | queueSync, -} - -/** - * A (subset) of node types which are relevant for the Stencil runtime. These - * values are based on the values which can possibly be returned by the - * `.nodeType` property of a DOM node. See here for details: - * - * {@link https://dom.spec.whatwg.org/#ref-for-dom-node-nodetype%E2%91%A0} - */ -export const enum NODE_TYPE { - ElementNode = 1, - TextNode = 3, - CommentNode = 8, - DocumentNode = 9, - DocumentTypeNode = 10, - DocumentFragment = 11, -} - -export const CONTENT_REF_ID = 'r'; -export const ORG_LOCATION_ID = 'o'; -export const SLOT_NODE_ID = 's'; -export const TEXT_NODE_ID = 't'; -export const COMMENT_NODE_ID = 'c'; - -export const HYDRATE_ID = 's-id'; -export const HYDRATED_STYLE_ID = 'sty-id'; -export const HYDRATE_CHILD_ID = 'c-id'; -export const HYDRATED_CSS = '{visibility:hidden}.hydrated{visibility:inherit}'; - -export const STENCIL_DOC_DATA = '_stencilDocData'; -export const DEFAULT_DOC_DATA = { - hostIds: 0, - rootLevelIds: 0, - staticComponents: new Set(), -}; - -/** - * Constant for styles to be globally applied to `slot-fb` elements for pseudo-slot behavior. - * - * Two cascading rules must be used instead of a `:not()` selector due to Stencil browser - * support as of Stencil v4. - */ -export const SLOT_FB_CSS = 'slot-fb{display:contents}slot-fb[hidden]{display:none}'; - -export const XLINK_NS = 'http://www.w3.org/1999/xlink'; - -export const FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS = [ - 'formAssociatedCallback', - 'formResetCallback', - 'formDisabledCallback', - 'formStateRestoreCallback', -] as const; diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts deleted file mode 100644 index a4da2e5377f..00000000000 --- a/src/runtime/styles.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { BUILD } from '@app-data'; -import { - plt, - styles, - supportsConstructableStylesheets, - supportsMutableAdoptedStyleSheets, - supportsShadow, - win, - writeTask, -} from '@platform'; - -import type * as d from '../declarations'; -import { CMP_FLAGS } from '../utils/constants'; -import { queryNonceMetaTagContent } from '../utils/query-nonce-meta-tag-content'; -import { createTime } from './profile'; -import { HYDRATED_STYLE_ID, NODE_TYPE, SLOT_FB_CSS } from './runtime-constants'; - -export const rootAppliedStyles: d.RootAppliedStyleMap = /*@__PURE__*/ new WeakMap(); - -/** - * Register the styles for a component by creating a stylesheet and then - * registering it under the component's scope ID in a `WeakMap` for later use. - * - * If constructable stylesheet are not supported or `allowCS` is set to - * `false` then the styles will be registered as a string instead. - * - * @param scopeId the scope ID for the component of interest - * @param cssText styles for the component of interest - * @param allowCS whether or not to use a constructable stylesheet - */ -export const registerStyle = (scopeId: string, cssText: string, allowCS: boolean) => { - let style = styles.get(scopeId); - if (supportsConstructableStylesheets && allowCS) { - style = (style || new CSSStyleSheet()) as CSSStyleSheet; - if (typeof style === 'string') { - style = cssText; - } else { - style.replaceSync(cssText); - } - } else { - style = cssText; - } - styles.set(scopeId, style); -}; - -/** - * Attach the styles for a given component to the DOM - * - * If the element uses shadow or is already attached to the DOM then we can - * create a stylesheet inside of its associated document fragment, otherwise - * we'll stick the stylesheet into the document head. - * - * @param styleContainerNode the node within which a style element for the - * component of interest should be added - * @param cmpMeta runtime metadata for the component of interest - * @param mode an optional current mode - * @returns the scope ID for the component of interest - */ -export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMeta, mode?: string) => { - const scopeId = getScopeId(cmpMeta, mode); - const style = styles.get(scopeId); - - if (!BUILD.attachStyles || !win.document) { - return scopeId; - } - // if an element is NOT connected then getRootNode() will return the wrong root node - // so the fallback is to always use the document for the root node in those cases - styleContainerNode = styleContainerNode.nodeType === NODE_TYPE.DocumentFragment ? styleContainerNode : win.document; - - if (style) { - if (typeof style === 'string') { - styleContainerNode = styleContainerNode.head || (styleContainerNode as HTMLElement); - let appliedStyles = rootAppliedStyles.get(styleContainerNode); - let styleElm; - if (!appliedStyles) { - rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Set())); - } - - // Check if style element already exists (for HMR updates) - // For shadow DOM components, directly update their dedicated style element - // For scoped components, check if they have their own HMR-created style element - const existingStyleElm: HTMLStyleElement = - (BUILD.hydrateClientSide || BUILD.hotModuleReplacement) && - styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`); - - if (existingStyleElm) { - // Update existing style element (for hydration or HMR) - existingStyleElm.textContent = style; - } else if (!appliedStyles.has(scopeId)) { - styleElm = win.document.createElement('style'); - styleElm.textContent = style; - - // Apply CSP nonce to the style tag if it exists - const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document); - if (nonce != null) { - styleElm.setAttribute('nonce', nonce); - } - - if ( - (BUILD.hydrateServerSide || BUILD.hotModuleReplacement) && - (cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation || - cmpMeta.$flags$ & CMP_FLAGS.shadowNeedsScopedCss || - cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) - ) { - styleElm.setAttribute(HYDRATED_STYLE_ID, scopeId); - } - - /** - * attach styles at the end of the head tag if we render scoped components - */ - if (!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation)) { - if (styleContainerNode.nodeName === 'HEAD') { - /** - * if the page contains preconnect links, we want to insert the styles - * after the last preconnect link to ensure the styles are preloaded - */ - const preconnectLinks = styleContainerNode.querySelectorAll('link[rel=preconnect]'); - const referenceNode = - preconnectLinks.length > 0 - ? preconnectLinks[preconnectLinks.length - 1].nextSibling - : styleContainerNode.querySelector('style'); - (styleContainerNode as HTMLElement).insertBefore( - styleElm, - referenceNode?.parentNode === styleContainerNode ? referenceNode : null, - ); - } else if ('host' in styleContainerNode) { - if (supportsConstructableStylesheets) { - /** - * If a scoped component is used within a shadow root then turn the styles into a - * constructable stylesheet and add it to the shadow root's adopted stylesheets. - * - * Note: order of how styles are adopted is important. The new stylesheet should be - * adopted before the existing styles. - * - * Note: constructable stylesheets can't be shared between windows, - * we need to create a new one for the current window if necessary - */ - const currentWindow = styleContainerNode.defaultView ?? styleContainerNode.ownerDocument.defaultView; - const stylesheet = new currentWindow.CSSStyleSheet(); - stylesheet.replaceSync(style); - - /** - * > If the array needs to be modified, use in-place mutations like push(). - * https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets - */ - if (supportsMutableAdoptedStyleSheets) { - styleContainerNode.adoptedStyleSheets.unshift(stylesheet); - } else { - styleContainerNode.adoptedStyleSheets = [stylesheet, ...styleContainerNode.adoptedStyleSheets]; - } - } else { - /** - * If a scoped component is used within a shadow root and constructable stylesheets are - * not supported, we want to insert the styles at the beginning of the shadow root node. - * - * However, if there is already a style node in the shadow root, we just append - * the styles to the existing node. - * - * Note: order of how styles are applied is important. The new style node - * should be inserted before the existing style node. - * - * During HMR, create separate style elements for scoped components so they can be - * updated independently without affecting other components' styles. - */ - const existingStyleContainer: HTMLStyleElement = styleContainerNode.querySelector('style'); - if (existingStyleContainer && !BUILD.hotModuleReplacement) { - existingStyleContainer.textContent = style + existingStyleContainer.textContent; - } else { - (styleContainerNode as HTMLElement).prepend(styleElm); - } - } - } else { - styleContainerNode.append(styleElm); - } - } - - /** - * attach styles at the beginning of a shadow root node if we render shadow components - */ - if (cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) { - styleContainerNode.insertBefore(styleElm, null); - } - - // Add styles for `slot-fb` elements if we're using slots outside the Shadow DOM - if (cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation) { - styleElm.textContent += SLOT_FB_CSS; - } - - if (appliedStyles) { - appliedStyles.add(scopeId); - } - } - } else if (BUILD.constructableCSS) { - let appliedStyles = rootAppliedStyles.get(styleContainerNode); - if (!appliedStyles) { - rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Set())); - } - if (!appliedStyles.has(scopeId)) { - /** - * Constructable stylesheets can't be shared between windows, - * we need to create a new one for the current window if necessary - */ - const currentWindow = styleContainerNode.defaultView ?? styleContainerNode.ownerDocument.defaultView; - let stylesheet: CSSStyleSheet; - if (style.constructor === currentWindow.CSSStyleSheet) { - stylesheet = style; - } else { - stylesheet = new currentWindow.CSSStyleSheet(); - for (let i = 0; i < style.cssRules.length; i++) { - stylesheet.insertRule(style.cssRules[i].cssText, i); - } - } - /** - * > If the array needs to be modified, use in-place mutations like push(). - * https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets - */ - if (supportsMutableAdoptedStyleSheets) { - styleContainerNode.adoptedStyleSheets.push(stylesheet); - } else { - styleContainerNode.adoptedStyleSheets = [...styleContainerNode.adoptedStyleSheets, stylesheet]; - } - - appliedStyles.add(scopeId); - - // Remove SSR style element from shadow root now that adoptedStyleSheets is in use - // Only remove from shadow roots, not from document head (for scoped components) - if (BUILD.hydrateClientSide && 'host' in styleContainerNode) { - const ssrStyleElm = styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`); - if (ssrStyleElm) { - writeTask(() => ssrStyleElm.remove()); - } - } - } - } - } - return scopeId; -}; - -/** - * Add styles for a given component to the DOM, optionally handling 'scoped' - * encapsulation by adding an appropriate class name to the host element. - * - * @param hostRef the host reference for the component of interest - */ -export const attachStyles = (hostRef: d.HostRef) => { - const cmpMeta = hostRef.$cmpMeta$; - const elm = hostRef.$hostElement$; - const flags = cmpMeta.$flags$; - const endAttachStyles = createTime('attachStyles', cmpMeta.$tagName$); - const scopeId = addStyle( - BUILD.shadowDom && supportsShadow && elm.shadowRoot ? elm.shadowRoot : (elm.getRootNode() as ShadowRoot), - cmpMeta, - hostRef.$modeName$, - ); - - if ((BUILD.shadowDom || BUILD.scoped) && BUILD.cssAnnotations && flags & CMP_FLAGS.needsScopedEncapsulation) { - // only required when we're NOT using native shadow dom (slot) - // or this browser doesn't support native shadow dom - // and this host element was NOT created with SSR - // let's pick out the inner content for slot projection - // create a node to represent where the original - // content was first placed, which is useful later on - // DOM WRITE!! - elm['s-sc'] = scopeId; - elm.classList.add(scopeId + '-h'); - } - endAttachStyles(); -}; - -/** - * Get the scope ID for a given component - * - * @param cmp runtime metadata for the component of interest - * @param mode the current mode (optional) - * @returns a scope ID for the component of interest - */ -export const getScopeId = (cmp: d.ComponentRuntimeMeta, mode?: string) => - 'sc-' + (BUILD.mode && mode && cmp.$flags$ & CMP_FLAGS.hasMode ? cmp.$tagName$ + '-' + mode : cmp.$tagName$); - -/** - * Convert a 'scoped' CSS string to one appropriate for use in the shadow DOM. - * - * Given a 'scoped' CSS string that looks like this: - * - * ``` - * /*!@div*\/div.class-name { display: flex }; - * ``` - * - * Convert it to a 'shadow' appropriate string, like so: - * - * ``` - * /*!@div*\/div.class-name { display: flex } - * ─┬─ ────────┬──────── - * │ │ - * │ ┌─────────────────┘ - * ▼ ▼ - * div{ display: flex } - * ``` - * - * Note that forward-slashes in the above are escaped so they don't end the - * comment. - * - * @param css a CSS string to convert - * @returns the converted string - */ -export const convertScopedToShadow = (css: string) => css.replace(/\/\*!@([^\/]+)\*\/[^\{]+\{/g, '$1{'); - -/** - * Hydrate styles after SSR for components *not* using DSD. Convert 'scoped' styles to 'shadow' - * and add them to a constructable stylesheet. - */ -export const hydrateScopedToShadow = () => { - if (!win.document) { - return; - } - - const styles = win.document.querySelectorAll(`[${HYDRATED_STYLE_ID}]`); - let i = 0; - for (; i < styles.length; i++) { - registerStyle(styles[i].getAttribute(HYDRATED_STYLE_ID), convertScopedToShadow(styles[i].innerHTML), true); - } -}; - -declare global { - export interface CSSStyleSheet { - replaceSync(cssText: string): void; - replace(cssText: string): Promise; - } -} diff --git a/src/runtime/tag-transform.ts b/src/runtime/tag-transform.ts deleted file mode 100644 index 09193901be8..00000000000 --- a/src/runtime/tag-transform.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type * as d from '../declarations'; - -export let tagTransformer: d.TagTransformer | undefined = undefined; - -/** - * Transforms a tag name using the current tag transformer - * @param tag - the tag to transform e.g. `my-tag` - * @returns the transformed tag e.g. `new-my-tag` - */ -export function transformTag(tag: T): T { - if (!tagTransformer) return tag; - return tagTransformer(tag) as T; -} - -/** - * Sets the tag transformer to be used when rendering custom elements - * @param transformer the transformer function to use. Must return a string - */ -export function setTagTransformer(transformer: d.TagTransformer) { - if (tagTransformer) { - console.warn(` - A tagTransformer has already been set. - Overwriting it may lead to error and unexpected results if your components have already been defined. - `); - } - tagTransformer = transformer; -} diff --git a/src/runtime/test/assets.spec.tsx b/src/runtime/test/assets.spec.tsx deleted file mode 100644 index 0e9afe68a20..00000000000 --- a/src/runtime/test/assets.spec.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { getAssetPath } from '@stencil/core'; -import { newSpecPage } from '@stencil/core/testing'; - -import { CmpAsset } from './fixtures/cmp-asset'; - -describe('assets', () => { - it('should load asset data', async () => { - const page = await newSpecPage({ - components: [CmpAsset], - html: ``, - }); - - expect(page.root).toEqualHtml(` - - - - - `); - }); - - it('getAssetPath is defined', async () => { - expect(getAssetPath).toBeDefined(); - }); -}); diff --git a/src/runtime/test/dom-extras.spec.tsx b/src/runtime/test/dom-extras.spec.tsx deleted file mode 100644 index 9422201d8e7..00000000000 --- a/src/runtime/test/dom-extras.spec.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { Component, h, Host } from '@stencil/core'; -import { newSpecPage, SpecPage } from '@stencil/core/testing'; - -import { patchPseudoShadowDom, patchSlottedNode } from '../../runtime/dom-extras'; - -describe('dom-extras - patches for non-shadow dom methods and accessors', () => { - let specPage: SpecPage; - - const nodeOrEleContent = (node: Node | Element) => { - return (node as Element)?.outerHTML || node?.nodeValue?.trim(); - }; - - beforeEach(async () => { - @Component({ - tag: 'cmp-a', - scoped: true, - }) - class CmpA { - render() { - return ( - - 'Shadow' first text node -
    -
    - Second slot fallback text -
    -
    - Default slot fallback text -
    -
    - 'Shadow' last text node -
    - ); - } - } - - specPage = await newSpecPage({ - components: [CmpA], - html: ` - - Some default slot, slotted text - a default slot, slotted element -
    - a second slot, slotted element - nested element in the second slot -
    - `, - hydrateClientSide: true, - }); - - patchPseudoShadowDom(specPage.root); - }); - - it('patches `childNodes` to return only nodes that have been slotted', async () => { - const childNodes = specPage.root.childNodes; - - expect(nodeOrEleContent(childNodes[0])).toBe(`Some default slot, slotted text`); - expect(nodeOrEleContent(childNodes[1])).toBe(`a default slot, slotted element`); - expect(nodeOrEleContent(childNodes[2])).toBe(``); - expect(nodeOrEleContent(childNodes[3])).toBe( - `
    a second slot, slotted element nested element in the second slot
    `, - ); - - const innerChildNodes = specPage.root.__childNodes; - - expect(nodeOrEleContent(innerChildNodes[0])).toBe(``); - expect(nodeOrEleContent(innerChildNodes[1])).toBe(``); - expect(nodeOrEleContent(innerChildNodes[2])).toBe(``); - expect(nodeOrEleContent(innerChildNodes[3])).toBe(``); - expect(nodeOrEleContent(innerChildNodes[4])).toBe(``); - expect(nodeOrEleContent(innerChildNodes[5])).toBe(`'Shadow' first text node`); - }); - - it('patches `children` to return only elements that have been slotted', async () => { - const children = specPage.root.children; - - expect(nodeOrEleContent(children[0])).toBe(`a default slot, slotted element`); - expect(nodeOrEleContent(children[1])).toBe( - `
    a second slot, slotted element nested element in the second slot
    `, - ); - expect(nodeOrEleContent(children[2])).toBe(undefined); - }); - - it('patches `childElementCount` to only count elements that have been slotted', async () => { - expect(specPage.root.childElementCount).toBe(2); - }); - - it('patches `textContent` to only return slotted node text', async () => { - expect(specPage.root.textContent.replace(/\s+/g, ' ').trim()).toBe( - `Some default slot, slotted text a default slot, slotted element a second slot, slotted element nested element in the second slot`, - ); - }); - - it('firstChild', async () => { - expect(nodeOrEleContent(specPage.root.firstChild)).toBe(`Some default slot, slotted text`); - }); - - it('lastChild', async () => { - expect(nodeOrEleContent(specPage.root.lastChild)).toBe( - `
    a second slot, slotted element nested element in the second slot
    `, - ); - }); - - it('patches nextSibling / previousSibling accessors of slotted nodes', async () => { - specPage.root.childNodes.forEach((node: Node) => patchSlottedNode(node)); - expect(nodeOrEleContent(specPage.root.firstChild)).toBe('Some default slot, slotted text'); - expect(nodeOrEleContent(specPage.root.firstChild.nextSibling)).toBe('a default slot, slotted element'); - expect(nodeOrEleContent(specPage.root.firstChild.nextSibling.nextSibling)).toBe(``); - expect(nodeOrEleContent(specPage.root.firstChild.nextSibling.nextSibling.nextSibling)).toBe( - `
    a second slot, slotted element nested element in the second slot
    `, - ); - // back we go! - expect(nodeOrEleContent(specPage.root.firstChild.nextSibling.nextSibling.nextSibling.previousSibling)).toBe(``); - expect( - nodeOrEleContent(specPage.root.firstChild.nextSibling.nextSibling.nextSibling.previousSibling.previousSibling), - ).toBe(`a default slot, slotted element`); - expect( - nodeOrEleContent( - specPage.root.firstChild.nextSibling.nextSibling.nextSibling.previousSibling.previousSibling.previousSibling, - ), - ).toBe(`Some default slot, slotted text`); - }); - - it('patches nextElementSibling / previousElementSibling accessors of slotted nodes', async () => { - specPage.root.childNodes.forEach((node: Node) => patchSlottedNode(node)); - expect(nodeOrEleContent(specPage.root.children[0].nextElementSibling)).toBe( - '
    a second slot, slotted element nested element in the second slot
    ', - ); - expect(nodeOrEleContent(specPage.root.children[0].nextElementSibling.previousElementSibling)).toBe( - 'a default slot, slotted element', - ); - }); - - it('patches parentNode of slotted nodes', async () => { - specPage.root.childNodes.forEach((node: Node) => patchSlottedNode(node)); - expect(specPage.root.children[0].parentNode.tagName).toBe('CMP-A'); - expect(specPage.root.children[1].parentNode.tagName).toBe('CMP-A'); - expect(specPage.root.childNodes[0].parentNode.tagName).toBe('CMP-A'); - expect(specPage.root.childNodes[1].parentNode.tagName).toBe('CMP-A'); - expect(specPage.root.children[0].__parentNode.tagName).toBe('DIV'); - expect(specPage.root.childNodes[0].__parentNode.tagName).toBe('DIV'); - }); -}); diff --git a/src/runtime/test/extends-basic.spec.tsx b/src/runtime/test/extends-basic.spec.tsx deleted file mode 100644 index c7a9ec3678a..00000000000 --- a/src/runtime/test/extends-basic.spec.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Component, h, Prop, Watch } from '@stencil/core'; -import { newSpecPage } from '@stencil/core/testing'; - -declare global { - namespace JSX { - interface IntrinsicElements { - [elemName: string]: any; - } - } -} - -describe('extends', () => { - it('renders a component that extends from a base class', async () => { - class Base { - baseProp = 'base'; - } - @Component({ tag: 'cmp-a' }) - class CmpA extends Base { - render() { - return `${this.baseProp}`; - } - } - - const page = await newSpecPage({ - components: [CmpA], - html: ``, - }); - - expect(page.root).toEqualHtml(` - base - `); - }); - - it('should call inherited watch methods when props change', async () => { - let called = 0; - - class BaseWatch { - @Prop() foo: string; - - @Watch('foo') - fooChanged() { - called++; - } - } - - @Component({ tag: 'extended-component' }) - class ExtendedComponent extends BaseWatch { - render() { - return
    {this.foo}
    ; - } - } - - const { root } = await newSpecPage({ - components: [ExtendedComponent], - html: ``, - }); - - expect(called).toBe(0); - - root.foo = '1'; - - expect(called).toBe(1); - expect(root.foo).toBe('1'); - }); -}); diff --git a/src/runtime/test/fetch.spec.tsx b/src/runtime/test/fetch.spec.tsx deleted file mode 100644 index 51e027c4102..00000000000 --- a/src/runtime/test/fetch.spec.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { Component, h, Host, Prop } from '@stencil/core'; -import { mockFetch, MockHeaders, MockResponse, newSpecPage } from '@stencil/core/testing'; - -describe('fetch', () => { - afterEach(() => { - mockFetch.reset(); - }); - - @Component({ - tag: 'cmp-a', - }) - class CmpA { - @Prop() data: string; - names: string[]; - text: string; - headers: string[]; - - async componentWillLoad() { - const url = `/${this.data}`; - const rsp = await fetch(url); - - this.headers = []; - rsp.headers.forEach((v, k) => { - this.headers.push(k + ': ' + v); - }); - - if (url.endsWith('.json')) { - const data = await rsp.json(); - this.text = null; - this.names = data.names; - } else { - this.text = await rsp.text(); - this.names = null; - } - } - render() { - return ( - -
      - {this.headers.map((n) => ( -
    • {n}
    • - ))} -
    - {this.names ? ( -
      - {this.names.map((n) => ( -
    • {n}
    • - ))} -
    - ) : null} - {this.text ?

    {this.text}

    : null} -
    - ); - } - } - - it('should mock json fetch, no input', async () => { - mockFetch.json({ names: ['Marty', 'Doc'] }); - - const page = await newSpecPage({ - components: [CmpA], - html: ``, - }); - - expect(page.root).toEqualHtml(` - -
      -
    • - content-type: application/json -
    • -
    -
      -
    • Marty
    • -
    • Doc
    • -
    -
    - `); - }); - - it('should mock json fetch, url input', async () => { - mockFetch.json({ names: ['Marty', 'Doc'] }, '/hillvalley.json'); - mockFetch.json({ names: ['Bo', 'Luke'] }, '/hazzard.json'); - - const page = await newSpecPage({ - components: [CmpA], - html: ``, - }); - - expect(page.root).toEqualHtml(` - -
      -
    • - content-type: application/json -
    • -
    -
      -
    • Bo
    • -
    • Luke
    • -
    -
    - `); - }); - - it('basic', async () => { - mockFetch.json({ names: ['Marty', 'Doc'] }, '/hillvalley.json'); - mockFetch.json({ names: ['Bo', 'Luke'] }, '/hazzard.json'); - - const page = await newSpecPage({ - components: [CmpA], - html: ``, - }); - - expect(page.root).toEqualHtml(` - -
      -
    • - content-type: application/json -
    • -
    -
      -
    • Bo
    • -
    • Luke
    • -
    -
    - `); - }); - - it('MockRequest text', async () => { - const res = new MockResponse('10:04', { - url: '/hillvalley.txt', - headers: new MockHeaders([ - ['Content-Type', 'text/plain'], - ['Access-Control-Allow-Origin', '*'], - ]), - }); - mockFetch.response(res); - - const page = await newSpecPage({ - components: [CmpA], - html: ``, - }); - - expect(page.root).toEqualHtml(` - -
      -
    • - content-type: text/plain -
    • -
    • - access-control-allow-origin: * -
    • -
    -

    - 10:04 -

    -
    - `); - }); - - it('404', async () => { - const page = await newSpecPage({ - components: [CmpA], - html: ``, - }); - - expect(page.root).toEqualHtml(` - -
      -
    • - content-type: text/plain -
    • -
    -

    - Not Found -

    -
    - `); - }); - - it('global Request/Response/Headers should work', () => { - const headers = new Headers(); - headers.set('x-header', 'value'); - const request = new Request('http://testing.stenciljs.com/some-url', { - headers, - }); - expect(request.url).toBe('http://testing.stenciljs.com/some-url'); - expect(request.headers.get('x-header')).toBe('value'); - }); -}); diff --git a/src/runtime/test/fixtures/cmp-a.css b/src/runtime/test/fixtures/cmp-a.css deleted file mode 100644 index cef256784c7..00000000000 --- a/src/runtime/test/fixtures/cmp-a.css +++ /dev/null @@ -1,4 +0,0 @@ - -:host { - color: red; -} diff --git a/src/runtime/test/fixtures/cmp-a.tsx b/src/runtime/test/fixtures/cmp-a.tsx deleted file mode 100644 index 34ea92464f6..00000000000 --- a/src/runtime/test/fixtures/cmp-a.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Component, Event, EventEmitter, h, Listen, Method, Prop, State, Watch } from '@stencil/core'; - -import { format } from './utils'; - -@Component({ - tag: 'cmp-a', - styleUrl: 'cmp-a.css', - shadow: true, -}) -export class CmpA { - // ************************ - // * Property Definitions * - // ************************ - - /** - * The first name - */ - @Prop() first: string; - - /** - * The middle name - */ - @Prop() middle: string; - - /** - * The last name - */ - @Prop() last: string; - - // ************************ - // * State Definitions * - // ************************ - - @State() innerFirst: string; - @State() innerMiddle: string; - @State() innerLast: string; - - // ***************************** - // * Watch on Property Changes * - // ***************************** - - @Watch('first') - parseFirstProp(newValue: string) { - this.innerFirst = newValue ? newValue : ''; - } - @Watch('middle') - parseMiddleProp(newValue: string) { - this.innerMiddle = newValue ? newValue : ''; - } - @Watch('last') - parseLastProp(newValue: string) { - this.innerLast = newValue ? newValue : ''; - } - - // ********************* - // * Event Definitions * - // ********************* - - /** - * Emitted when the component Loads - */ - @Event() initevent: EventEmitter; - - // ******************************* - // * Listen to Event Definitions * - // ******************************* - - @Listen('testevent', { target: 'document' }) - handleTestEvent(event: CustomEvent) { - this.parseLastProp(event.detail.last ? event.detail.last : ''); - } - - // ********************** - // * Method Definitions * - // ********************** - - @Method() - init(): Promise { - return Promise.resolve(this._init()); - } - - // ********************************* - // * Internal Variable Definitions * - // ********************************* - - // ******************************* - // * Component Lifecycle Methods * - // ******************************* - - async componentWillLoad() { - await this.init(); - } - - // ****************************** - // * Private Method Definitions * - // ****************************** - - private async _init(): Promise { - this.parseFirstProp(this.first ? this.first : ''); - this.parseMiddleProp(this.middle ? this.middle : ''); - this.parseLastProp(this.last ? this.last : ''); - this.initevent.emit({ init: true }); - return; - } - - private getText(): string { - return format(this.innerFirst, this.innerMiddle, this.innerLast); - } - - // ************************* - // * Rendering JSX Element * - // ************************* - - render() { - return
    Hello, World! I'm {this.getText()}
    ; - } -} diff --git a/src/runtime/test/host.spec.tsx b/src/runtime/test/host.spec.tsx deleted file mode 100644 index ca2e42bbb20..00000000000 --- a/src/runtime/test/host.spec.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Component, h, Host, Prop, State } from '@stencil/core'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('hostData', () => { - it('render hostData() attributes', async () => { - @Component({ tag: 'cmp-a' }) - class CmpA { - @Prop() hidden = false; - - hostData() { - return { - value: 'somevalue', - role: 'alert', - 'aria-hidden': this.hidden ? 'true' : null, - hidden: this.hidden, - }; - } - } - - const { root, waitForChanges } = await newSpecPage({ - components: [CmpA], - html: ``, - }); - expect(root).toEqualHtml(` - - `); - - root.hidden = true; - await waitForChanges(); - - expect(root).toEqualHtml(` - - `); - }); - - it('render attributes', async () => { - @Component({ tag: 'cmp-a' }) - class CmpA { - @Prop() hidden = false; - - render() { - return