From be288655242c7aa033d8797158956d4313ff876a Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Sun, 8 Feb 2026 21:08:06 +0000 Subject: [PATCH 001/194] chore: good progress --- V5_PLANNING.md | 1204 ++ build-vite.ts | 113 + package-lock.json | 13891 ---------------- package.json | 3 +- packages/cli/package.json | 23 + packages/cli/src/check-version.ts | 35 + packages/cli/src/config-flags.ts | 353 + packages/cli/src/find-config.ts | 80 + packages/cli/src/index.ts | 4 + packages/cli/src/ionic-config.ts | 64 + packages/cli/src/load-compiler.ts | 7 + packages/cli/src/logs.ts | 130 + packages/cli/src/parse-flags.ts | 517 + packages/cli/src/public.ts | 24 + packages/cli/src/run.ts | 187 + packages/cli/src/task-build.ts | 57 + packages/cli/src/task-docs.ts | 18 + packages/cli/src/task-generate.ts | 383 + packages/cli/src/task-help.ts | 57 + packages/cli/src/task-info.ts | 33 + packages/cli/src/task-prerender.ts | 50 + packages/cli/src/task-serve.ts | 38 + packages/cli/src/task-telemetry.ts | 49 + packages/cli/src/task-test.ts | 66 + packages/cli/src/task-watch.ts | 66 + packages/cli/src/telemetry/helpers.ts | 67 + packages/cli/src/telemetry/shouldTrack.ts | 14 + packages/cli/src/telemetry/telemetry.ts | 493 + .../cli/src/telemetry/test/helpers.spec.ts | 106 + .../cli/src/telemetry/test/telemetry.spec.ts | 285 + packages/cli/src/test/ionic-config.spec.ts | 107 + packages/cli/src/test/parse-flags.spec.ts | 471 + packages/cli/src/test/run.spec.ts | 309 + packages/cli/src/test/task-generate.spec.ts | 188 + packages/cli/vite.config.ts | 56 + packages/core/package.json | 28 + packages/core/src/app-data/index.ts | 109 + packages/core/src/app-globals/index.ts | 5 + packages/core/src/cli/config-flags.ts | 353 + packages/core/src/client/client-build.ts | 10 + packages/core/src/client/client-host-ref.ts | 80 + .../core/src/client/client-load-module.ts | 66 + packages/core/src/client/client-log.ts | 22 + .../core/src/client/client-patch-browser.ts | 54 + packages/core/src/client/client-style.ts | 6 + packages/core/src/client/client-task-queue.ts | 103 + packages/core/src/client/client-window.ts | 72 + packages/core/src/client/index.ts | 9 + packages/core/src/client/polyfills/core-js.js | 11 + packages/core/src/client/polyfills/dom.js | 79 + .../src/client/polyfills/es5-html-element.js | 1 + packages/core/src/client/polyfills/index.js | 34 + packages/core/src/client/polyfills/system.js | 6 + .../core/src/compiler/app-core/app-data.ts | 191 + .../src/compiler/app-core/app-es5-disabled.ts | 152 + .../src/compiler/app-core/app-polyfills.ts | 36 + .../src/compiler/app-core/bundle-app-core.ts | 57 + packages/core/src/compiler/build/build-ctx.ts | 239 + .../core/src/compiler/build/build-finish.ts | 195 + packages/core/src/compiler/build/build-hmr.ts | 248 + .../core/src/compiler/build/build-results.ts | 43 + .../core/src/compiler/build/build-stats.ts | 187 + packages/core/src/compiler/build/build.ts | 80 + .../core/src/compiler/build/compiler-ctx.ts | 161 + .../core/src/compiler/build/full-build.ts | 50 + .../compiler/build/test/build-stats.spec.ts | 71 + .../build/test/write-export-maps.spec.ts | 143 + .../core/src/compiler/build/validate-files.ts | 26 + .../core/src/compiler/build/watch-build.ts | 409 + .../core/src/compiler/build/write-build.ts | 51 + .../src/compiler/build/write-export-maps.ts | 81 + .../src/compiler/bundle/app-data-plugin.ts | 260 + .../src/compiler/bundle/bundle-interface.ts | 61 + .../core/src/compiler/bundle/bundle-output.ts | 211 + .../core/src/compiler/bundle/constants.ts | 3 + .../compiler/bundle/core-resolve-plugin.ts | 167 + .../core/src/compiler/bundle/dev-module.ts | 193 + .../bundle/dev-node-module-resolve.ts | 89 + .../src/compiler/bundle/entry-alias-ids.ts | 13 + .../src/compiler/bundle/ext-format-plugin.ts | 74 + .../compiler/bundle/ext-transforms-plugin.ts | 215 + .../src/compiler/bundle/file-load-plugin.ts | 18 + .../core/src/compiler/bundle/loader-plugin.ts | 43 + .../core/src/compiler/bundle/plugin-helper.ts | 77 + .../core/src/compiler/bundle/server-plugin.ts | 66 + .../bundle/test/app-data-plugin.spec.ts | 46 + .../bundle/test/core-resolve-plugin.spec.ts | 70 + .../bundle/test/ext-transforms-plugin.spec.ts | 105 + .../src/compiler/bundle/typescript-plugin.ts | 99 + .../src/compiler/bundle/user-index-plugin.ts | 30 + .../core/src/compiler/bundle/worker-plugin.ts | 466 + packages/core/src/compiler/cache.ts | 188 + packages/core/src/compiler/compiler.ts | 65 + .../core/src/compiler/config/config-utils.ts | 77 + .../core/src/compiler/config/constants.ts | 13 + .../core/src/compiler/config/load-config.ts | 161 + .../core/src/compiler/config/outputs/index.ts | 42 + .../config/outputs/validate-collection.ts | 27 + .../config/outputs/validate-custom-element.ts | 90 + .../config/outputs/validate-custom-output.ts | 29 + .../compiler/config/outputs/validate-dist.ts | 174 + .../compiler/config/outputs/validate-docs.ts | 137 + .../config/outputs/validate-hydrate-script.ts | 78 + .../compiler/config/outputs/validate-lazy.ts | 20 + .../compiler/config/outputs/validate-stats.ts | 37 + .../compiler/config/outputs/validate-www.ts | 139 + .../config/test/fixtures/stencil.config.ts | 5 + .../config/test/fixtures/stencil.config2.ts | 11 + .../compiler/config/test/load-config.spec.ts | 130 + .../src/compiler/config/test/tsconfig.json | 3 + .../test/validate-config-sourcemap.spec.ts | 140 + .../config/test/validate-config.spec.ts | 629 + .../config/test/validate-copy.spec.ts | 52 + .../config/test/validate-custom.spec.ts | 49 + .../config/test/validate-dev-server.spec.ts | 324 + .../config/test/validate-docs.spec.ts | 72 + .../config/test/validate-hydrated.spec.ts | 37 + .../config/test/validate-namespace.spec.ts | 80 + .../validate-output-dist-collection.spec.ts | 88 + ...alidate-output-dist-custom-element.spec.ts | 480 + .../config/test/validate-output-dist.spec.ts | 291 + .../config/test/validate-output-www.spec.ts | 380 + .../config/test/validate-paths.spec.ts | 176 + .../test/validate-rollup-config.spec.ts | 82 + .../test/validate-service-worker.spec.ts | 193 + .../config/test/validate-stats.spec.ts | 100 + .../config/test/validate-testing.spec.ts | 941 ++ .../config/test/validate-workers.spec.ts | 52 + .../src/compiler/config/transpile-options.ts | 204 + .../src/compiler/config/validate-config.ts | 293 + .../core/src/compiler/config/validate-copy.ts | 28 + .../compiler/config/validate-dev-server.ts | 198 + .../core/src/compiler/config/validate-docs.ts | 42 + .../src/compiler/config/validate-hydrated.ts | 52 + .../src/compiler/config/validate-namespace.ts | 75 + .../src/compiler/config/validate-paths.ts | 85 + .../src/compiler/config/validate-plugins.ts | 43 + .../src/compiler/config/validate-prerender.ts | 38 + .../compiler/config/validate-rollup-config.ts | 52 + .../config/validate-service-worker.ts | 105 + .../src/compiler/config/validate-testing.ts | 217 + .../src/compiler/config/validate-workers.ts | 19 + packages/core/src/compiler/docs/cem/index.ts | 369 + packages/core/src/compiler/docs/constants.ts | 2 + .../core/src/compiler/docs/custom/index.ts | 23 + .../src/compiler/docs/generate-doc-data.ts | 535 + packages/core/src/compiler/docs/json/index.ts | 77 + .../src/compiler/docs/readme/docs-util.ts | 158 + .../core/src/compiler/docs/readme/index.ts | 51 + .../docs/readme/markdown-css-props.ts | 25 + .../docs/readme/markdown-custom-states.ts | 30 + .../docs/readme/markdown-dependencies.ts | 62 + .../compiler/docs/readme/markdown-events.ts | 34 + .../compiler/docs/readme/markdown-methods.ts | 55 + .../compiler/docs/readme/markdown-overview.ts | 18 + .../compiler/docs/readme/markdown-parts.ts | 30 + .../compiler/docs/readme/markdown-props.ts | 56 + .../compiler/docs/readme/markdown-slots.ts | 30 + .../compiler/docs/readme/markdown-usage.ts | 44 + .../src/compiler/docs/readme/output-docs.ts | 206 + packages/core/src/compiler/docs/style-docs.ts | 103 + .../test/custom-elements-manifest.spec.ts | 615 + .../src/compiler/docs/test/docs-util.spec.ts | 73 + .../docs/test/generate-doc-data.spec.ts | 411 + .../docs/test/markdown-dependencies.spec.ts | 153 + .../docs/test/markdown-overview.spec.ts | 50 + .../compiler/docs/test/markdown-props.spec.ts | 130 + .../compiler/docs/test/output-docs.spec.ts | 110 + .../src/compiler/docs/test/style-docs.spec.ts | 185 + .../core/src/compiler/docs/test/tsconfig.json | 3 + .../core/src/compiler/docs/vscode/index.ts | 139 + .../src/compiler/entries/component-bundles.ts | 204 + .../src/compiler/entries/component-graph.ts | 15 + .../src/compiler/entries/default-bundles.ts | 88 + .../entries/resolve-component-dependencies.ts | 126 + packages/core/src/compiler/events.ts | 73 + .../src/compiler/fs-watch/fs-watch-rebuild.ts | 165 + .../core/src/compiler/html/add-script-attr.ts | 23 + .../core/src/compiler/html/canonical-link.ts | 27 + packages/core/src/compiler/html/html-utils.ts | 15 + .../compiler/html/inject-module-preloads.ts | 37 + .../src/compiler/html/inject-sw-script.ts | 39 + .../src/compiler/html/inline-esm-import.ts | 161 + .../src/compiler/html/inline-style-sheets.ts | 34 + .../compiler/html/relocate-meta-charset.ts | 16 + .../src/compiler/html/remove-unused-styles.ts | 72 + .../html/test/remove-unused-styles.spec.ts | 292 + .../core/src/compiler/html/test/tsconfig.json | 3 + .../html/test/update-esm-import-paths.spec.ts | 65 + .../html/update-global-styles-link.ts | 35 + .../core/src/compiler/html/used-components.ts | 25 + .../compiler/html/validate-manifest-json.ts | 73 + packages/core/src/compiler/index.ts | 16 + .../src/compiler/optimize/autoprefixer.ts | 125 + .../core/src/compiler/optimize/minify-css.ts | 57 + .../core/src/compiler/optimize/minify-js.ts | 156 + .../src/compiler/optimize/optimize-css.ts | 32 + .../core/src/compiler/optimize/optimize-js.ts | 40 + .../src/compiler/optimize/optimize-module.ts | 261 + .../output-targets/copy/assets-copy-tasks.ts | 94 + .../output-targets/copy/hashed-copy.ts | 50 + .../output-targets/copy/local-copy-tasks.ts | 27 + .../output-targets/copy/output-copy.ts | 102 + .../output-targets/dist-collection/index.ts | 155 + .../custom-elements-build-conditionals.ts | 34 + .../custom-elements-types.ts | 244 + .../generate-loader-module.ts | 148 + .../dist-custom-elements/index.ts | 371 + .../test/dist-custom-elements.spec.ts | 166 + .../bundle-hydrate-factory.ts | 96 + .../generate-hydrate-app.ts | 159 + .../hydrate-build-conditionals.ts | 51 + .../hydrate-factory-closure.ts | 149 + .../dist-hydrate-script/index.ts | 19 + .../relocate-hydrate-context.ts | 17 + .../test/dist-hydrate-script.spec.ts | 211 + .../update-to-hydrate-components.ts | 30 + .../write-hydrate-outputs.ts | 183 + .../output-targets/dist-lazy/generate-cjs.ts | 74 + .../dist-lazy/generate-esm-browser.ts | 49 + .../output-targets/dist-lazy/generate-esm.ts | 126 + .../dist-lazy/generate-lazy-module.ts | 447 + .../dist-lazy/generate-system.ts | 118 + .../dist-lazy/lazy-build-conditionals.ts | 26 + .../dist-lazy/lazy-bundleid-plugin.ts | 110 + .../dist-lazy/lazy-component-plugin.ts | 39 + .../output-targets/dist-lazy/lazy-output.ts | 203 + .../test/generate-lazy-module.spec.ts | 270 + .../dist-lazy/write-lazy-entry-module.ts | 39 + .../src/compiler/output-targets/empty-dir.ts | 49 + .../core/src/compiler/output-targets/index.ts | 68 + .../compiler/output-targets/output-custom.ts | 32 + .../compiler/output-targets/output-docs.ts | 94 + .../output-targets/output-lazy-loader.ts | 107 + .../output-targets/output-service-workers.ts | 32 + .../compiler/output-targets/output-types.ts | 27 + .../src/compiler/output-targets/output-www.ts | 190 + .../src/compiler/output-targets/readme.md | 115 + .../test/build-conditionals.spec.ts | 198 + .../test/custom-elements-types.spec.ts | 319 + .../test/output-lazy-loader.spec.ts | 73 + .../test/output-targets-collection.spec.ts | 73 + ...utput-targets-dist-custom-elements.spec.ts | 358 + .../test/output-targets-dist.spec.ts | 90 + .../test/output-targets-www-dist.spec.ts | 73 + .../test/output-targets-www.spec.ts | 69 + .../output-targets/test/tsconfig.json | 3 + packages/core/src/compiler/plugin/plugin.ts | 308 + .../src/compiler/plugin/test/plugin.spec.ts | 186 + .../src/compiler/plugin/test/tsconfig.json | 3 + .../core/src/compiler/prerender/crawl-urls.ts | 232 + .../compiler/prerender/prerender-config.ts | 23 + .../prerender/prerender-hydrate-options.ts | 49 + .../src/compiler/prerender/prerender-main.ts | 295 + .../compiler/prerender/prerender-optimize.ts | 412 + .../src/compiler/prerender/prerender-queue.ts | 170 + .../prerender/prerender-template-html.ts | 100 + .../prerender/prerender-worker-ctx.ts | 31 + .../compiler/prerender/prerender-worker.ts | 234 + .../prerender/prerendered-write-path.ts | 51 + .../core/src/compiler/prerender/robots-txt.ts | 72 + .../src/compiler/prerender/sitemap-xml.ts | 106 + .../prerender/test/crawl-urls.spec.ts | 307 + .../prerender/test/prerender-optimize.spec.ts | 51 + .../test/prerendered-write-path.spec.ts | 114 + .../src/compiler/prerender/test/tsconfig.json | 3 + packages/core/src/compiler/public.ts | 122 + .../compiler/service-worker/generate-sw.ts | 139 + .../service-worker/service-worker-util.ts | 21 + .../test/service-worker-util.spec.ts | 34 + .../test/service-worker.spec.ts | 46 + .../service-worker/test/tsconfig.json | 3 + .../core/src/compiler/style/css-imports.ts | 362 + .../css-parser/css-parse-declarations.ts | 70 + .../style/css-parser/get-css-selectors.ts | 54 + .../compiler/style/css-parser/parse-css.ts | 624 + .../src/compiler/style/css-parser/readme.md | 13 + .../style/css-parser/serialize-css.ts | 342 + .../style/css-parser/test/css-nesting.spec.ts | 236 + .../css-parser/test/escaped-selectors.spec.ts | 130 + .../css-parser/test/get-selectors.spec.ts | 68 + .../style/css-parser/test/minify-css.spec.ts | 61 + .../css-parser/test/parse-serialize.spec.ts | 301 + .../style/css-parser/used-selectors.ts | 60 + .../core/src/compiler/style/css-to-esm.ts | 340 + .../core/src/compiler/style/global-styles.ts | 185 + .../src/compiler/style/normalize-styles.ts | 48 + .../core/src/compiler/style/optimize-css.ts | 59 + packages/core/src/compiler/style/scope-css.ts | 16 + .../core/src/compiler/style/style-utils.ts | 45 + .../style/test/build-conditionals.spec.ts | 186 + .../compiler/style/test/css-imports.spec.ts | 770 + .../compiler/style/test/css-to-esm.spec.ts | 453 + .../compiler/style/test/optimize-css.spec.ts | 286 + .../compiler/style/test/style-rebuild.spec.ts | 341 + .../src/compiler/style/test/style.spec.ts | 79 + .../src/compiler/style/test/tsconfig.json | 3 + packages/core/src/compiler/sys/config.ts | 21 + packages/core/src/compiler/sys/environment.ts | 3 + .../compiler/sys/fetch/fetch-module-async.ts | 36 + .../compiler/sys/fetch/fetch-module-sync.ts | 46 + .../src/compiler/sys/fetch/fetch-utils.ts | 89 + .../sys/fetch/tests/fetch-module.spec.ts | 60 + .../compiler/sys/fetch/write-fetch-success.ts | 69 + .../core/src/compiler/sys/in-memory-fs.ts | 1360 ++ .../core/src/compiler/sys/node-require.ts | 73 + .../sys/resolve/resolve-module-async.ts | 100 + .../sys/resolve/resolve-module-sync.ts | 118 + .../src/compiler/sys/resolve/resolve-utils.ts | 68 + .../sys/resolve/tests/resolve-module.spec.ts | 122 + packages/core/src/compiler/sys/stencil-sys.ts | 649 + .../compiler/sys/tests/in-memory-fs.spec.ts | 689 + .../compiler/sys/tests/stencil-sys.spec.ts | 281 + .../tests/typescript-config.spec.ts | 86 + .../tests/typescript-resolve-module.spec.ts | 35 + .../typescript/tests/typescript-sys.spec.ts | 33 + .../sys/typescript/typescript-config.ts | 207 + .../typescript/typescript-resolve-module.ts | 84 + .../compiler/sys/typescript/typescript-sys.ts | 215 + .../src/compiler/sys/worker/sys-worker.ts | 34 + .../transformers/add-component-meta-proxy.ts | 74 + .../transformers/add-component-meta-static.ts | 50 + .../src/compiler/transformers/add-imports.ts | 45 + .../compiler/transformers/add-static-style.ts | 227 + .../transformers/add-tag-transform.ts | 176 + .../automatic-key-insertion.spec.ts | 414 + .../automatic-key-insertion/index.ts | 253 + .../automatic-key-insertion/utils.ts | 35 + .../collections/add-external-import.ts | 114 + .../parse-collection-components.ts | 33 + .../collections/parse-collection-manifest.ts | 73 + .../collections/parse-collection-module.ts | 60 + .../component-build-conditionals.ts | 67 + .../component-hydrate/hydrate-component.ts | 45 + .../hydrate-runtime-cmp-meta.ts | 44 + .../tranform-to-hydrate-component.ts | 45 + .../component-lazy/attach-internals.ts | 194 + .../transformers/component-lazy/constants.ts | 5 + .../component-lazy/lazy-component.ts | 69 + .../component-lazy/lazy-constructor.ts | 58 + .../component-lazy/lazy-element-getter.ts | 56 + .../transform-lazy-component.ts | 71 + .../add-define-custom-element-function.ts | 254 + .../component-native/attach-internals.ts | 100 + .../component-native/native-component.ts | 165 + .../native-connected-callback.ts | 64 + .../component-native/native-constructor.ts | 100 + .../component-native/native-element-getter.ts | 45 + .../component-native/native-meta.ts | 8 + .../component-native/native-static-style.ts | 117 + .../proxy-custom-element-function.ts | 113 + .../tranform-to-native-component.ts | 86 + .../transformers/core-runtime-apis.ts | 68 + .../src/compiler/transformers/create-event.ts | 57 + .../decorators-to-static/attach-internals.ts | 150 + .../component-decorator.ts | 202 + .../convert-decorators.ts | 318 + .../decorators-to-static/decorator-utils.ts | 231 + .../decorators-constants.ts | 75 + .../decorators-to-static/element-decorator.ts | 35 + .../decorators-to-static/event-decorator.ts | 430 + .../decorators-to-static/import-alias-map.ts | 42 + .../decorators-to-static/listen-decorator.ts | 111 + .../decorators-to-static/method-decorator.ts | 135 + .../decorators-to-static/prop-decorator.ts | 381 + .../serialize-decorators.ts | 64 + .../decorators-to-static/state-decorator.ts | 63 + .../decorators-to-static/style-to-static.ts | 105 + .../decorators-to-static/watch-decorator.ts | 48 + .../transformers/define-custom-element.ts | 90 + .../transformers/detect-modern-prop-decls.ts | 63 + .../transformers/host-data-transform.ts | 79 + .../map-imports-to-path-aliases.ts | 129 + .../reactive-handler-meta-transform.ts | 36 + .../transformers/remove-collection-imports.ts | 50 + .../remove-static-meta-properties.ts | 57 + .../transformers/reserved-public-members.ts | 294 + .../transformers/rewrite-aliased-paths.ts | 213 + .../static-to-meta/attach-internals.ts | 40 + .../static-to-meta/call-expression.ts | 134 + .../static-to-meta/class-extension.ts | 502 + .../static-to-meta/class-methods.ts | 30 + .../transformers/static-to-meta/component.ts | 295 + .../static-to-meta/element-ref.ts | 13 + .../static-to-meta/encapsulation.ts | 63 + .../transformers/static-to-meta/events.ts | 24 + .../static-to-meta/form-associated.ts | 14 + .../transformers/static-to-meta/import.ts | 45 + .../transformers/static-to-meta/listeners.ts | 21 + .../transformers/static-to-meta/methods.ts | 25 + .../static-to-meta/parse-static.ts | 81 + .../transformers/static-to-meta/props.ts | 44 + .../static-to-meta/serializers.ts | 21 + .../transformers/static-to-meta/states.ts | 22 + .../static-to-meta/string-literal.ts | 14 + .../transformers/static-to-meta/styles.ts | 100 + .../transformers/static-to-meta/vdom.ts | 80 + .../transformers/static-to-meta/visitor.ts | 56 + .../transformers/static-to-meta/watchers.ts | 19 + .../transformers/stencil-import-path.ts | 101 + .../compiler/transformers/style-imports.ts | 263 + .../test/add-component-meta-proxy.spec.ts | 90 + .../test/add-static-style.spec.ts | 489 + .../test/add-tag-transform.spec.ts | 268 + .../test/convert-decorators.spec.ts | 494 + .../test/core-runtime-apis.spec.ts | 113 + .../transformers/test/decorator-utils.spec.ts | 215 + .../test/detect-modern-prop-decls.spec.ts | 61 + .../transformers/test/fixtures/dessert.ts | 20 + .../transformers/test/fixtures/meal-entry.ts | 48 + .../test/functional-component-deps.spec.ts | 287 + .../transformers/test/lazy-component.spec.ts | 212 + .../test/map-imports-to-path-aliases.spec.ts | 200 + .../test/native-component.spec.ts | 278 + .../test/parse-attach-internals.spec.ts | 143 + .../transformers/test/parse-comments.spec.ts | 109 + .../test/parse-component-tags.spec.ts | 51 + .../transformers/test/parse-component.spec.ts | 162 + .../test/parse-deserializers.spec.ts | 91 + .../transformers/test/parse-element.spec.ts | 15 + .../test/parse-encapsulation.spec.ts | 85 + .../transformers/test/parse-events.spec.ts | 243 + .../test/parse-form-associated.spec.ts | 26 + .../test/parse-import-path.spec.ts | 108 + .../transformers/test/parse-listeners.spec.ts | 263 + .../transformers/test/parse-methods.spec.ts | 120 + .../transformers/test/parse-mixin.spec.ts | 149 + .../transformers/test/parse-props.spec.ts | 949 ++ .../test/parse-serializers.spec.ts | 91 + .../test/parse-slot-assignment.spec.ts | 127 + .../transformers/test/parse-states.spec.ts | 48 + .../transformers/test/parse-styles.spec.ts | 94 + .../transformers/test/parse-vdom.spec.ts | 270 + .../test/parse-virtual-props.spec.ts | 19 + .../transformers/test/parse-watch.spec.ts | 96 + .../proxy-custom-element-function.spec.ts | 194 + .../test/rewrite-aliased-paths.spec.ts | 247 + .../transformers/test/transform-utils.spec.ts | 676 + .../compiler/transformers/test/transpile.ts | 224 + .../compiler/transformers/test/tsconfig.json | 3 + .../transformers/test/type-library.spec.ts | 163 + .../src/compiler/transformers/test/utils.ts | 25 + .../compiler/transformers/transform-utils.ts | 1505 ++ .../src/compiler/transformers/type-library.ts | 365 + .../transformers/update-component-class.ts | 123 + .../update-stencil-core-import.ts | 118 + packages/core/src/compiler/transpile.ts | 172 + .../transpile/create-build-program.ts | 125 + .../transpile/create-watch-program.ts | 117 + .../src/compiler/transpile/run-program.ts | 256 + .../test/create-watch-program.spec.ts | 46 + .../transpile/test/run-program.spec.ts | 47 + .../compiler/transpile/transpile-module.ts | 176 + .../compiler/transpile/transpiled-module.ts | 65 + .../core/src/compiler/transpile/ts-config.ts | 40 + .../compiler/transpile/validate-components.ts | 21 + packages/core/src/compiler/types/constants.ts | 91 + .../src/compiler/types/generate-app-types.ts | 249 + .../types/generate-component-types.ts | 254 + .../types/generate-event-detail-types.ts | 41 + .../types/generate-event-listener-types.ts | 84 + .../compiler/types/generate-event-types.ts | 59 + .../compiler/types/generate-method-types.ts | 44 + .../src/compiler/types/generate-prop-types.ts | 64 + .../core/src/compiler/types/generate-types.ts | 67 + .../compiler/types/package-json-log-utils.ts | 52 + .../core/src/compiler/types/stencil-types.ts | 137 + .../tests/ComponentCompilerEvent.stub.ts | 35 + .../types/tests/ComponentCompilerMeta.stub.ts | 101 + .../tests/ComponentCompilerMethod.stub.ts | 26 + .../tests/ComponentCompilerProperty.stub.ts | 39 + .../ComponentCompilerTypeReference.stub.ts | 19 + .../ComponentCompilerVirtualProperty.stub.ts | 20 + .../types/tests/TypesImportData.stub.ts | 19 + .../generate-app-types.spec.ts.snap | 1266 ++ .../types/tests/generate-app-types.spec.ts | 689 + .../tests/generate-component-types.spec.ts | 334 + .../tests/generate-event-detail-types.spec.ts | 35 + .../generate-event-listener-types.spec.ts | 265 + .../types/tests/generate-event-types.spec.ts | 184 + .../types/tests/generate-method-types.spec.ts | 135 + .../types/tests/generate-prop-types.spec.ts | 275 + .../types/tests/stencil-types.spec.ts | 333 + .../src/compiler/types/tests/tsconfig.json | 3 + .../types/tests/validate-package-json.spec.ts | 132 + ...date-primary-package-output-target.spec.ts | 294 + .../core/src/compiler/types/types-utils.ts | 18 + .../src/compiler/types/update-import-refs.ts | 151 + .../types/validate-build-package-json.ts | 184 + .../validate-primary-package-output-target.ts | 272 + .../core/src/compiler/worker/main-thread.ts | 17 + .../core/src/compiler/worker/worker-thread.ts | 62 + .../core/src/declarations/child_process.ts | 5 + packages/core/src/declarations/index.ts | 3 + packages/core/src/declarations/readme.md | 33 + .../src/declarations/stencil-ext-modules.d.ts | 41 + .../core/src/declarations/stencil-private.ts | 2850 ++++ .../declarations/stencil-public-compiler.ts | 3194 ++++ .../src/declarations/stencil-public-docs.ts | 422 + .../declarations/stencil-public-runtime.ts | 2100 +++ packages/core/src/internal/default.ts | 1 + packages/core/src/internal/index.ts | 1 + packages/core/src/internal/readme.md | 13 + .../core/src/internal/stencil-core/index.cjs | 1 + .../core/src/internal/stencil-core/index.d.ts | 61 + .../core/src/internal/stencil-core/index.js | 18 + .../internal/stencil-core/jsx-dev-runtime.cjs | 7 + .../stencil-core/jsx-dev-runtime.d.ts | 41 + .../internal/stencil-core/jsx-dev-runtime.js | 2 + .../src/internal/stencil-core/jsx-runtime.cjs | 8 + .../internal/stencil-core/jsx-runtime.d.ts | 40 + .../src/internal/stencil-core/jsx-runtime.js | 2 + .../src/internal/testing/jsx-dev-runtime.d.ts | 2 + .../src/internal/testing/jsx-dev-runtime.js | 8 + .../src/internal/testing/jsx-runtime.d.ts | 2 + .../core/src/internal/testing/jsx-runtime.js | 9 + packages/core/src/runtime/asset-path.ts | 8 + .../src/runtime/bootstrap-custom-element.ts | 168 + packages/core/src/runtime/bootstrap-lazy.ts | 294 + packages/core/src/runtime/client-hydrate.ts | 743 + .../core/src/runtime/connected-callback.ts | 150 + .../core/src/runtime/disconnected-callback.ts | 48 + packages/core/src/runtime/dom-extras.ts | 614 + packages/core/src/runtime/element.ts | 6 + packages/core/src/runtime/event-emitter.ts | 36 + packages/core/src/runtime/fragment.ts | 3 + packages/core/src/runtime/hmr-component.ts | 38 + packages/core/src/runtime/host-listener.ts | 85 + packages/core/src/runtime/index.ts | 25 + .../core/src/runtime/initialize-component.ts | 221 + packages/core/src/runtime/mixin.ts | 9 + packages/core/src/runtime/mode.ts | 10 + packages/core/src/runtime/nonce.ts | 9 + .../core/src/runtime/parse-property-value.ts | 85 + packages/core/src/runtime/platform-options.ts | 20 + packages/core/src/runtime/profile.ts | 98 + packages/core/src/runtime/proxy-component.ts | 420 + packages/core/src/runtime/readme.md | 126 + packages/core/src/runtime/render.ts | 35 + .../core/src/runtime/runtime-constants.ts | 86 + packages/core/src/runtime/set-value.ts | 162 + .../core/src/runtime/slot-polyfill-utils.ts | 266 + packages/core/src/runtime/styles.ts | 324 + packages/core/src/runtime/tag-transform.ts | 27 + .../core/src/runtime/test/assets.spec.tsx | 24 + .../runtime/test/attr-deserialize.spec.tsx | 207 + .../runtime/test/attr-prop-prefix.spec.tsx | 409 + packages/core/src/runtime/test/attr.spec.tsx | 435 + .../src/runtime/test/before-each.spec.tsx | 85 + .../src/runtime/test/bootstrap-lazy.spec.tsx | 63 + .../test/client-hydrate-to-vdom.spec.tsx | 86 + .../src/runtime/test/component-class.spec.tsx | 29 + .../test/component-error-handling.spec.tsx | 86 + .../core/src/runtime/test/dom-extras.spec.tsx | 143 + .../core/src/runtime/test/element.spec.tsx | 32 + packages/core/src/runtime/test/event.spec.tsx | 474 + .../src/runtime/test/extends-basic.spec.tsx | 65 + packages/core/src/runtime/test/fetch.spec.tsx | 189 + .../core/src/runtime/test/fixtures/cmp-a.css | 4 + .../core/src/runtime/test/fixtures/cmp-a.tsx | 117 + .../src/runtime/test/fixtures/cmp-asset.tsx | 17 + .../core/src/runtime/test/fixtures/utils.ts | 3 + .../core/src/runtime/test/globals.spec.tsx | 102 + packages/core/src/runtime/test/host.spec.tsx | 101 + .../test/hydrate-no-encapsulation.spec.tsx | 494 + .../src/runtime/test/hydrate-prop.spec.tsx | 89 + .../src/runtime/test/hydrate-scoped.spec.tsx | 201 + .../test/hydrate-shadow-child.spec.tsx | 600 + .../test/hydrate-shadow-in-shadow.spec.tsx | 446 + .../test/hydrate-shadow-parent.spec.tsx | 518 + .../src/runtime/test/hydrate-shadow.spec.tsx | 173 + .../test/hydrate-slot-fallback.spec.tsx | 522 + .../hydrate-slotted-content-order.spec.tsx | 542 + .../test/hydrate-style-element.spec.tsx | 37 + .../test/initialize-component.spec.tsx | 22 + packages/core/src/runtime/test/jsx.spec.tsx | 172 + .../src/runtime/test/lifecycle-async.spec.tsx | 132 + .../src/runtime/test/lifecycle-sync.spec.tsx | 423 + .../core/src/runtime/test/listen.spec.tsx | 308 + .../core/src/runtime/test/method.spec.tsx | 49 + packages/core/src/runtime/test/mixin.spec.tsx | 97 + .../runtime/test/parse-property-value.spec.ts | 248 + .../src/runtime/test/prop-serialize.spec.tsx | 247 + .../src/runtime/test/prop-warnings.spec.tsx | 152 + packages/core/src/runtime/test/prop.spec.tsx | 320 + packages/core/src/runtime/test/queue.spec.tsx | 32 + ...egression-json-string-non-parsing.spec.tsx | 98 + .../src/runtime/test/render-text.spec.tsx | 107 + .../src/runtime/test/render-vdom.spec.tsx | 1259 ++ .../core/src/runtime/test/scoped.spec.tsx | 306 + .../core/src/runtime/test/shadow.spec.tsx | 170 + packages/core/src/runtime/test/state.spec.tsx | 67 + packages/core/src/runtime/test/style.spec.tsx | 107 + .../src/runtime/test/svg-element.spec.tsx | 125 + packages/core/src/runtime/test/tsconfig.json | 29 + .../runtime/test/update-component.spec.tsx | 72 + .../src/runtime/test/vdom-relocation.spec.tsx | 71 + packages/core/src/runtime/test/watch.spec.tsx | 287 + packages/core/src/runtime/update-component.ts | 508 + packages/core/src/runtime/vdom/h.ts | 226 + .../core/src/runtime/vdom/jsx-dev-runtime.ts | 68 + packages/core/src/runtime/vdom/jsx-runtime.ts | 67 + .../core/src/runtime/vdom/set-accessor.ts | 270 + .../vdom-annotations.spec.tsx.snap | 7 + .../src/runtime/vdom/test/attributes.spec.ts | 99 + .../runtime/vdom/test/event-listeners.spec.ts | 103 + packages/core/src/runtime/vdom/test/h.spec.ts | 505 + .../runtime/vdom/test/is-same-vnode.spec.ts | 71 + .../src/runtime/vdom/test/jsx-runtime.spec.ts | 95 + .../src/runtime/vdom/test/patch-svg.spec.ts | 82 + .../core/src/runtime/vdom/test/patch.spec.ts | 869 + .../runtime/vdom/test/scoped-slot.spec.tsx | 824 + .../runtime/vdom/test/set-accessor.spec.ts | 1091 ++ .../core/src/runtime/vdom/test/tsconfig.json | 30 + .../runtime/vdom/test/update-element.spec.ts | 219 + .../core/src/runtime/vdom/test/util.spec.ts | 47 + .../vdom/test/vdom-annotations.spec.tsx | 104 + .../runtime/vdom/test/vdom-render.spec.tsx | 109 + .../core/src/runtime/vdom/update-element.ts | 83 + packages/core/src/runtime/vdom/util.ts | 35 + .../core/src/runtime/vdom/vdom-annotations.ts | 264 + packages/core/src/runtime/vdom/vdom-render.ts | 1328 ++ packages/core/src/server/platform/h-async.ts | 28 + .../core/src/server/platform/hydrate-app.ts | 396 + packages/core/src/server/platform/index.ts | 242 + .../src/server/platform/proxy-host-element.ts | 130 + .../test/__mocks__/@app-globals/index.ts | 3 + .../test/serialize-shadow-root-opts.spec.ts | 51 + .../core/src/server/runner/create-window.ts | 14 + .../core/src/server/runner/hydrate-factory.ts | 29 + packages/core/src/server/runner/index.ts | 6 + .../core/src/server/runner/inspect-element.ts | 106 + .../server/runner/patch-dom-implementation.ts | 72 + .../core/src/server/runner/render-utils.ts | 167 + packages/core/src/server/runner/render.ts | 323 + .../core/src/server/runner/runtime-log.ts | 56 + .../src/server/runner/window-initialize.ts | 75 + .../core/src/sys/node/bundles/autoprefixer.js | 2 + packages/core/src/sys/node/bundles/glob.js | 1 + .../core/src/sys/node/bundles/graceful-fs.js | 1 + .../core/src/sys/node/bundles/node-fetch.js | 7 + packages/core/src/sys/node/bundles/prompts.js | 3 + packages/core/src/sys/node/index.ts | 3 + packages/core/src/sys/node/logger/index.ts | 100 + .../src/sys/node/logger/terminal-logger.ts | 770 + .../node/logger/test/terminal-logger.spec.ts | 240 + packages/core/src/sys/node/node-copy-tasks.ts | 182 + .../core/src/sys/node/node-fs-promisify.ts | 8 + .../core/src/sys/node/node-lazy-require.ts | 112 + .../core/src/sys/node/node-resolve-module.ts | 103 + .../core/src/sys/node/node-setup-process.ts | 21 + .../sys/node/node-stencil-version-checker.ts | 179 + packages/core/src/sys/node/node-sys.ts | 674 + .../src/sys/node/node-worker-controller.ts | 224 + .../core/src/sys/node/node-worker-main.ts | 164 + .../core/src/sys/node/node-worker-thread.ts | 59 + packages/core/src/sys/node/public.ts | 37 + .../sys/node/test/node-lazy-require.spec.ts | 87 + .../src/sys/node/test/test-worker-main.ts | 12 + packages/core/src/sys/node/test/tsconfig.json | 3 + .../src/sys/node/test/worker-manager.spec.ts | 91 + packages/core/src/sys/node/worker.ts | 15 + packages/core/src/utils/byte-size.ts | 7 + packages/core/src/utils/constants.ts | 323 + .../src/utils/es2022-rewire-class-members.ts | 82 + .../utils/format-component-runtime-meta.ts | 265 + packages/core/src/utils/helpers.ts | 234 + packages/core/src/utils/index.ts | 24 + packages/core/src/utils/is-glob.ts | 37 + packages/core/src/utils/is-root-path.ts | 10 + packages/core/src/utils/local-value.ts | 259 + .../core/src/utils/logger/logger-rollup.ts | 170 + .../src/utils/logger/logger-typescript.ts | 218 + .../core/src/utils/logger/logger-utils.ts | 137 + packages/core/src/utils/message-utils.ts | 198 + packages/core/src/utils/output-target.ts | 221 + packages/core/src/utils/path.ts | 275 + .../src/utils/query-nonce-meta-tag-content.ts | 11 + packages/core/src/utils/regular-expression.ts | 9 + packages/core/src/utils/remote-value.ts | 121 + packages/core/src/utils/result.ts | 160 + packages/core/src/utils/serialize.ts | 37 + packages/core/src/utils/shadow-css.ts | 658 + packages/core/src/utils/shadow-root.ts | 54 + packages/core/src/utils/sourcemaps.ts | 83 + packages/core/src/utils/style.ts | 16 + packages/core/src/utils/test/helpers.spec.ts | 169 + .../core/src/utils/test/is-root-path.spec.ts | 30 + .../core/src/utils/test/message-utils.spec.ts | 276 + .../core/src/utils/test/output-target.spec.ts | 290 + packages/core/src/utils/test/path.spec.ts | 179 + .../test/query-nonce-meta-tag-content.spec.ts | 39 + .../src/utils/test/regular-expression.spec.ts | 26 + packages/core/src/utils/test/result.spec.ts | 36 + .../core/src/utils/test/scope-css.spec.ts | 365 + .../core/src/utils/test/serialize.spec.ts | 113 + .../core/src/utils/test/sourcemaps.spec.ts | 261 + packages/core/src/utils/test/tsconfig.json | 3 + .../core/src/utils/test/url-paths.spec.ts | 33 + packages/core/src/utils/test/util.spec.ts | 370 + .../core/src/utils/test/validation.spec.ts | 60 + packages/core/src/utils/types.ts | 95 + packages/core/src/utils/url-paths.ts | 18 + packages/core/src/utils/util.ts | 276 + packages/core/src/utils/validation.ts | 50 + packages/core/src/version.ts | 18 + packages/core/vite.app-data.config.ts | 30 + packages/core/vite.app-globals.config.ts | 30 + packages/core/vite.client.config.ts | 43 + packages/core/vite.config.ts | 62 + packages/core/vite.internal.config.ts | 39 + packages/core/vite.server.config.ts | 44 + packages/mock-doc/package.json | 17 + packages/mock-doc/src/attribute.ts | 174 + packages/mock-doc/src/comment-node.ts | 20 + packages/mock-doc/src/console.ts | 32 + packages/mock-doc/src/constants.ts | 31 + .../mock-doc/src/css-style-declaration.ts | 107 + packages/mock-doc/src/css-style-sheet.ts | 108 + .../mock-doc/src/custom-element-registry.ts | 244 + packages/mock-doc/src/dataset.ts | 50 + packages/mock-doc/src/document-fragment.ts | 44 + packages/mock-doc/src/document-type-node.ts | 10 + packages/mock-doc/src/document.ts | 336 + packages/mock-doc/src/element.ts | 805 + packages/mock-doc/src/event.ts | 253 + packages/mock-doc/src/global.ts | 188 + packages/mock-doc/src/headers.ts | 138 + packages/mock-doc/src/history.ts | 27 + packages/mock-doc/src/index.ts | 14 + .../mock-doc/src/intersection-observer.ts | 21 + packages/mock-doc/src/location.ts | 48 + packages/mock-doc/src/navigator.ts | 7 + packages/mock-doc/src/node.ts | 1297 ++ packages/mock-doc/src/parse-html.ts | 26 + packages/mock-doc/src/parse-util.ts | 247 + packages/mock-doc/src/parser.ts | 18 + packages/mock-doc/src/performance.ts | 98 + packages/mock-doc/src/request-response.ts | 121 + packages/mock-doc/src/resize-observer.ts | 21 + packages/mock-doc/src/selector.ts | 96 + packages/mock-doc/src/serialize-node.ts | 716 + packages/mock-doc/src/shadow-root.ts | 54 + packages/mock-doc/src/storage.ts | 31 + packages/mock-doc/src/test/attribute.spec.ts | 211 + packages/mock-doc/src/test/clone.spec.ts | 52 + .../src/test/css-style-declaration.spec.ts | 32 + .../mock-doc/src/test/css-style-sheet.spec.ts | 80 + .../mock-doc/src/test/custom-elements.spec.ts | 237 + packages/mock-doc/src/test/dataset.spec.ts | 50 + packages/mock-doc/src/test/doc-style.spec.ts | 72 + .../src/test/document-fragment.spec.ts | 48 + packages/mock-doc/src/test/element.spec.ts | 735 + packages/mock-doc/src/test/event.spec.ts | 325 + packages/mock-doc/src/test/global.spec.ts | 34 + packages/mock-doc/src/test/headers.spec.ts | 137 + packages/mock-doc/src/test/html-parse.spec.ts | 305 + packages/mock-doc/src/test/location.spec.ts | 95 + .../mock-doc/src/test/match-media.spec.ts | 40 + .../src/test/request-response.spec.ts | 137 + packages/mock-doc/src/test/selector.spec.ts | 276 + .../mock-doc/src/test/serialize-node.spec.ts | 322 + .../test/shadow-dom-event-bubbling.spec.ts | 95 + packages/mock-doc/src/test/storage.spec.ts | 57 + packages/mock-doc/src/test/token-list.spec.ts | 56 + packages/mock-doc/src/third-party/jquery.ts | 2363 +++ packages/mock-doc/src/token-list.ts | 85 + packages/mock-doc/src/window.ts | 923 + packages/mock-doc/vite.config.ts | 29 + pnpm-lock.yaml | 8707 ++++++++++ pnpm-workspace.yaml | 2 + src/compiler/index.ts | 2 +- src/compiler/public.ts | 2 +- src/mock-doc/index.ts | 6 +- vite-prototype/index.html | 11 + vite-prototype/package-lock.json | 1102 ++ vite-prototype/package.json | 13 + vite-prototype/plugin.js | 55 + vite-prototype/src/components/my-button.tsx | 13 + vite-prototype/src/main.js | 1 + vite-prototype/vite.config.js | 23 + 781 files changed, 133374 insertions(+), 13896 deletions(-) create mode 100644 V5_PLANNING.md create mode 100644 build-vite.ts delete mode 100644 package-lock.json create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/check-version.ts create mode 100644 packages/cli/src/config-flags.ts create mode 100644 packages/cli/src/find-config.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/ionic-config.ts create mode 100644 packages/cli/src/load-compiler.ts create mode 100644 packages/cli/src/logs.ts create mode 100644 packages/cli/src/parse-flags.ts create mode 100644 packages/cli/src/public.ts create mode 100644 packages/cli/src/run.ts create mode 100644 packages/cli/src/task-build.ts create mode 100644 packages/cli/src/task-docs.ts create mode 100644 packages/cli/src/task-generate.ts create mode 100644 packages/cli/src/task-help.ts create mode 100644 packages/cli/src/task-info.ts create mode 100644 packages/cli/src/task-prerender.ts create mode 100644 packages/cli/src/task-serve.ts create mode 100644 packages/cli/src/task-telemetry.ts create mode 100644 packages/cli/src/task-test.ts create mode 100644 packages/cli/src/task-watch.ts create mode 100644 packages/cli/src/telemetry/helpers.ts create mode 100644 packages/cli/src/telemetry/shouldTrack.ts create mode 100644 packages/cli/src/telemetry/telemetry.ts create mode 100644 packages/cli/src/telemetry/test/helpers.spec.ts create mode 100644 packages/cli/src/telemetry/test/telemetry.spec.ts create mode 100644 packages/cli/src/test/ionic-config.spec.ts create mode 100644 packages/cli/src/test/parse-flags.spec.ts create mode 100644 packages/cli/src/test/run.spec.ts create mode 100644 packages/cli/src/test/task-generate.spec.ts create mode 100644 packages/cli/vite.config.ts create mode 100644 packages/core/package.json create mode 100644 packages/core/src/app-data/index.ts create mode 100644 packages/core/src/app-globals/index.ts create mode 100644 packages/core/src/cli/config-flags.ts create mode 100644 packages/core/src/client/client-build.ts create mode 100644 packages/core/src/client/client-host-ref.ts create mode 100644 packages/core/src/client/client-load-module.ts create mode 100644 packages/core/src/client/client-log.ts create mode 100644 packages/core/src/client/client-patch-browser.ts create mode 100644 packages/core/src/client/client-style.ts create mode 100644 packages/core/src/client/client-task-queue.ts create mode 100644 packages/core/src/client/client-window.ts create mode 100644 packages/core/src/client/index.ts create mode 100755 packages/core/src/client/polyfills/core-js.js create mode 100755 packages/core/src/client/polyfills/dom.js create mode 100755 packages/core/src/client/polyfills/es5-html-element.js create mode 100755 packages/core/src/client/polyfills/index.js create mode 100755 packages/core/src/client/polyfills/system.js create mode 100644 packages/core/src/compiler/app-core/app-data.ts create mode 100644 packages/core/src/compiler/app-core/app-es5-disabled.ts create mode 100644 packages/core/src/compiler/app-core/app-polyfills.ts create mode 100644 packages/core/src/compiler/app-core/bundle-app-core.ts create mode 100644 packages/core/src/compiler/build/build-ctx.ts create mode 100644 packages/core/src/compiler/build/build-finish.ts create mode 100644 packages/core/src/compiler/build/build-hmr.ts create mode 100644 packages/core/src/compiler/build/build-results.ts create mode 100644 packages/core/src/compiler/build/build-stats.ts create mode 100644 packages/core/src/compiler/build/build.ts create mode 100644 packages/core/src/compiler/build/compiler-ctx.ts create mode 100644 packages/core/src/compiler/build/full-build.ts create mode 100644 packages/core/src/compiler/build/test/build-stats.spec.ts create mode 100644 packages/core/src/compiler/build/test/write-export-maps.spec.ts create mode 100644 packages/core/src/compiler/build/validate-files.ts create mode 100644 packages/core/src/compiler/build/watch-build.ts create mode 100644 packages/core/src/compiler/build/write-build.ts create mode 100644 packages/core/src/compiler/build/write-export-maps.ts create mode 100644 packages/core/src/compiler/bundle/app-data-plugin.ts create mode 100644 packages/core/src/compiler/bundle/bundle-interface.ts create mode 100644 packages/core/src/compiler/bundle/bundle-output.ts create mode 100644 packages/core/src/compiler/bundle/constants.ts create mode 100644 packages/core/src/compiler/bundle/core-resolve-plugin.ts create mode 100644 packages/core/src/compiler/bundle/dev-module.ts create mode 100644 packages/core/src/compiler/bundle/dev-node-module-resolve.ts create mode 100644 packages/core/src/compiler/bundle/entry-alias-ids.ts create mode 100644 packages/core/src/compiler/bundle/ext-format-plugin.ts create mode 100644 packages/core/src/compiler/bundle/ext-transforms-plugin.ts create mode 100644 packages/core/src/compiler/bundle/file-load-plugin.ts create mode 100644 packages/core/src/compiler/bundle/loader-plugin.ts create mode 100644 packages/core/src/compiler/bundle/plugin-helper.ts create mode 100644 packages/core/src/compiler/bundle/server-plugin.ts create mode 100644 packages/core/src/compiler/bundle/test/app-data-plugin.spec.ts create mode 100644 packages/core/src/compiler/bundle/test/core-resolve-plugin.spec.ts create mode 100644 packages/core/src/compiler/bundle/test/ext-transforms-plugin.spec.ts create mode 100644 packages/core/src/compiler/bundle/typescript-plugin.ts create mode 100644 packages/core/src/compiler/bundle/user-index-plugin.ts create mode 100644 packages/core/src/compiler/bundle/worker-plugin.ts create mode 100644 packages/core/src/compiler/cache.ts create mode 100644 packages/core/src/compiler/compiler.ts create mode 100644 packages/core/src/compiler/config/config-utils.ts create mode 100644 packages/core/src/compiler/config/constants.ts create mode 100644 packages/core/src/compiler/config/load-config.ts create mode 100644 packages/core/src/compiler/config/outputs/index.ts create mode 100644 packages/core/src/compiler/config/outputs/validate-collection.ts create mode 100644 packages/core/src/compiler/config/outputs/validate-custom-element.ts create mode 100644 packages/core/src/compiler/config/outputs/validate-custom-output.ts create mode 100644 packages/core/src/compiler/config/outputs/validate-dist.ts create mode 100644 packages/core/src/compiler/config/outputs/validate-docs.ts create mode 100644 packages/core/src/compiler/config/outputs/validate-hydrate-script.ts create mode 100644 packages/core/src/compiler/config/outputs/validate-lazy.ts create mode 100644 packages/core/src/compiler/config/outputs/validate-stats.ts create mode 100644 packages/core/src/compiler/config/outputs/validate-www.ts create mode 100644 packages/core/src/compiler/config/test/fixtures/stencil.config.ts create mode 100644 packages/core/src/compiler/config/test/fixtures/stencil.config2.ts create mode 100644 packages/core/src/compiler/config/test/load-config.spec.ts create mode 100644 packages/core/src/compiler/config/test/tsconfig.json create mode 100644 packages/core/src/compiler/config/test/validate-config-sourcemap.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-config.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-copy.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-custom.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-dev-server.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-docs.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-hydrated.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-namespace.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-output-dist-collection.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-output-dist-custom-element.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-output-dist.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-output-www.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-paths.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-rollup-config.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-service-worker.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-stats.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-testing.spec.ts create mode 100644 packages/core/src/compiler/config/test/validate-workers.spec.ts create mode 100644 packages/core/src/compiler/config/transpile-options.ts create mode 100644 packages/core/src/compiler/config/validate-config.ts create mode 100644 packages/core/src/compiler/config/validate-copy.ts create mode 100644 packages/core/src/compiler/config/validate-dev-server.ts create mode 100644 packages/core/src/compiler/config/validate-docs.ts create mode 100644 packages/core/src/compiler/config/validate-hydrated.ts create mode 100644 packages/core/src/compiler/config/validate-namespace.ts create mode 100644 packages/core/src/compiler/config/validate-paths.ts create mode 100644 packages/core/src/compiler/config/validate-plugins.ts create mode 100644 packages/core/src/compiler/config/validate-prerender.ts create mode 100644 packages/core/src/compiler/config/validate-rollup-config.ts create mode 100644 packages/core/src/compiler/config/validate-service-worker.ts create mode 100644 packages/core/src/compiler/config/validate-testing.ts create mode 100644 packages/core/src/compiler/config/validate-workers.ts create mode 100644 packages/core/src/compiler/docs/cem/index.ts create mode 100644 packages/core/src/compiler/docs/constants.ts create mode 100644 packages/core/src/compiler/docs/custom/index.ts create mode 100644 packages/core/src/compiler/docs/generate-doc-data.ts create mode 100644 packages/core/src/compiler/docs/json/index.ts create mode 100644 packages/core/src/compiler/docs/readme/docs-util.ts create mode 100644 packages/core/src/compiler/docs/readme/index.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-css-props.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-custom-states.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-dependencies.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-events.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-methods.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-overview.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-parts.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-props.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-slots.ts create mode 100644 packages/core/src/compiler/docs/readme/markdown-usage.ts create mode 100644 packages/core/src/compiler/docs/readme/output-docs.ts create mode 100644 packages/core/src/compiler/docs/style-docs.ts create mode 100644 packages/core/src/compiler/docs/test/custom-elements-manifest.spec.ts create mode 100644 packages/core/src/compiler/docs/test/docs-util.spec.ts create mode 100644 packages/core/src/compiler/docs/test/generate-doc-data.spec.ts create mode 100644 packages/core/src/compiler/docs/test/markdown-dependencies.spec.ts create mode 100644 packages/core/src/compiler/docs/test/markdown-overview.spec.ts create mode 100644 packages/core/src/compiler/docs/test/markdown-props.spec.ts create mode 100644 packages/core/src/compiler/docs/test/output-docs.spec.ts create mode 100644 packages/core/src/compiler/docs/test/style-docs.spec.ts create mode 100644 packages/core/src/compiler/docs/test/tsconfig.json create mode 100644 packages/core/src/compiler/docs/vscode/index.ts create mode 100644 packages/core/src/compiler/entries/component-bundles.ts create mode 100644 packages/core/src/compiler/entries/component-graph.ts create mode 100644 packages/core/src/compiler/entries/default-bundles.ts create mode 100644 packages/core/src/compiler/entries/resolve-component-dependencies.ts create mode 100644 packages/core/src/compiler/events.ts create mode 100644 packages/core/src/compiler/fs-watch/fs-watch-rebuild.ts create mode 100644 packages/core/src/compiler/html/add-script-attr.ts create mode 100644 packages/core/src/compiler/html/canonical-link.ts create mode 100644 packages/core/src/compiler/html/html-utils.ts create mode 100644 packages/core/src/compiler/html/inject-module-preloads.ts create mode 100644 packages/core/src/compiler/html/inject-sw-script.ts create mode 100644 packages/core/src/compiler/html/inline-esm-import.ts create mode 100644 packages/core/src/compiler/html/inline-style-sheets.ts create mode 100644 packages/core/src/compiler/html/relocate-meta-charset.ts create mode 100644 packages/core/src/compiler/html/remove-unused-styles.ts create mode 100644 packages/core/src/compiler/html/test/remove-unused-styles.spec.ts create mode 100644 packages/core/src/compiler/html/test/tsconfig.json create mode 100644 packages/core/src/compiler/html/test/update-esm-import-paths.spec.ts create mode 100644 packages/core/src/compiler/html/update-global-styles-link.ts create mode 100644 packages/core/src/compiler/html/used-components.ts create mode 100644 packages/core/src/compiler/html/validate-manifest-json.ts create mode 100644 packages/core/src/compiler/index.ts create mode 100644 packages/core/src/compiler/optimize/autoprefixer.ts create mode 100644 packages/core/src/compiler/optimize/minify-css.ts create mode 100644 packages/core/src/compiler/optimize/minify-js.ts create mode 100644 packages/core/src/compiler/optimize/optimize-css.ts create mode 100644 packages/core/src/compiler/optimize/optimize-js.ts create mode 100644 packages/core/src/compiler/optimize/optimize-module.ts create mode 100644 packages/core/src/compiler/output-targets/copy/assets-copy-tasks.ts create mode 100644 packages/core/src/compiler/output-targets/copy/hashed-copy.ts create mode 100644 packages/core/src/compiler/output-targets/copy/local-copy-tasks.ts create mode 100644 packages/core/src/compiler/output-targets/copy/output-copy.ts create mode 100644 packages/core/src/compiler/output-targets/dist-collection/index.ts create mode 100644 packages/core/src/compiler/output-targets/dist-custom-elements/custom-elements-build-conditionals.ts create mode 100644 packages/core/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts create mode 100644 packages/core/src/compiler/output-targets/dist-custom-elements/generate-loader-module.ts create mode 100644 packages/core/src/compiler/output-targets/dist-custom-elements/index.ts create mode 100644 packages/core/src/compiler/output-targets/dist-custom-elements/test/dist-custom-elements.spec.ts create mode 100644 packages/core/src/compiler/output-targets/dist-hydrate-script/bundle-hydrate-factory.ts create mode 100644 packages/core/src/compiler/output-targets/dist-hydrate-script/generate-hydrate-app.ts create mode 100644 packages/core/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts create mode 100644 packages/core/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts create mode 100644 packages/core/src/compiler/output-targets/dist-hydrate-script/index.ts create mode 100644 packages/core/src/compiler/output-targets/dist-hydrate-script/relocate-hydrate-context.ts create mode 100644 packages/core/src/compiler/output-targets/dist-hydrate-script/test/dist-hydrate-script.spec.ts create mode 100644 packages/core/src/compiler/output-targets/dist-hydrate-script/update-to-hydrate-components.ts create mode 100644 packages/core/src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/generate-cjs.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/generate-esm-browser.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/generate-esm.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/generate-lazy-module.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/generate-system.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/lazy-build-conditionals.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/lazy-bundleid-plugin.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/lazy-component-plugin.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/lazy-output.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/test/generate-lazy-module.spec.ts create mode 100644 packages/core/src/compiler/output-targets/dist-lazy/write-lazy-entry-module.ts create mode 100644 packages/core/src/compiler/output-targets/empty-dir.ts create mode 100644 packages/core/src/compiler/output-targets/index.ts create mode 100644 packages/core/src/compiler/output-targets/output-custom.ts create mode 100644 packages/core/src/compiler/output-targets/output-docs.ts create mode 100644 packages/core/src/compiler/output-targets/output-lazy-loader.ts create mode 100644 packages/core/src/compiler/output-targets/output-service-workers.ts create mode 100644 packages/core/src/compiler/output-targets/output-types.ts create mode 100644 packages/core/src/compiler/output-targets/output-www.ts create mode 100644 packages/core/src/compiler/output-targets/readme.md create mode 100644 packages/core/src/compiler/output-targets/test/build-conditionals.spec.ts create mode 100644 packages/core/src/compiler/output-targets/test/custom-elements-types.spec.ts create mode 100644 packages/core/src/compiler/output-targets/test/output-lazy-loader.spec.ts create mode 100644 packages/core/src/compiler/output-targets/test/output-targets-collection.spec.ts create mode 100644 packages/core/src/compiler/output-targets/test/output-targets-dist-custom-elements.spec.ts create mode 100644 packages/core/src/compiler/output-targets/test/output-targets-dist.spec.ts create mode 100644 packages/core/src/compiler/output-targets/test/output-targets-www-dist.spec.ts create mode 100644 packages/core/src/compiler/output-targets/test/output-targets-www.spec.ts create mode 100644 packages/core/src/compiler/output-targets/test/tsconfig.json create mode 100644 packages/core/src/compiler/plugin/plugin.ts create mode 100644 packages/core/src/compiler/plugin/test/plugin.spec.ts create mode 100644 packages/core/src/compiler/plugin/test/tsconfig.json create mode 100644 packages/core/src/compiler/prerender/crawl-urls.ts create mode 100644 packages/core/src/compiler/prerender/prerender-config.ts create mode 100644 packages/core/src/compiler/prerender/prerender-hydrate-options.ts create mode 100644 packages/core/src/compiler/prerender/prerender-main.ts create mode 100644 packages/core/src/compiler/prerender/prerender-optimize.ts create mode 100644 packages/core/src/compiler/prerender/prerender-queue.ts create mode 100644 packages/core/src/compiler/prerender/prerender-template-html.ts create mode 100644 packages/core/src/compiler/prerender/prerender-worker-ctx.ts create mode 100644 packages/core/src/compiler/prerender/prerender-worker.ts create mode 100644 packages/core/src/compiler/prerender/prerendered-write-path.ts create mode 100644 packages/core/src/compiler/prerender/robots-txt.ts create mode 100644 packages/core/src/compiler/prerender/sitemap-xml.ts create mode 100644 packages/core/src/compiler/prerender/test/crawl-urls.spec.ts create mode 100644 packages/core/src/compiler/prerender/test/prerender-optimize.spec.ts create mode 100644 packages/core/src/compiler/prerender/test/prerendered-write-path.spec.ts create mode 100644 packages/core/src/compiler/prerender/test/tsconfig.json create mode 100644 packages/core/src/compiler/public.ts create mode 100644 packages/core/src/compiler/service-worker/generate-sw.ts create mode 100644 packages/core/src/compiler/service-worker/service-worker-util.ts create mode 100644 packages/core/src/compiler/service-worker/test/service-worker-util.spec.ts create mode 100644 packages/core/src/compiler/service-worker/test/service-worker.spec.ts create mode 100644 packages/core/src/compiler/service-worker/test/tsconfig.json create mode 100644 packages/core/src/compiler/style/css-imports.ts create mode 100644 packages/core/src/compiler/style/css-parser/css-parse-declarations.ts create mode 100644 packages/core/src/compiler/style/css-parser/get-css-selectors.ts create mode 100644 packages/core/src/compiler/style/css-parser/parse-css.ts create mode 100644 packages/core/src/compiler/style/css-parser/readme.md create mode 100644 packages/core/src/compiler/style/css-parser/serialize-css.ts create mode 100644 packages/core/src/compiler/style/css-parser/test/css-nesting.spec.ts create mode 100644 packages/core/src/compiler/style/css-parser/test/escaped-selectors.spec.ts create mode 100644 packages/core/src/compiler/style/css-parser/test/get-selectors.spec.ts create mode 100644 packages/core/src/compiler/style/css-parser/test/minify-css.spec.ts create mode 100644 packages/core/src/compiler/style/css-parser/test/parse-serialize.spec.ts create mode 100644 packages/core/src/compiler/style/css-parser/used-selectors.ts create mode 100644 packages/core/src/compiler/style/css-to-esm.ts create mode 100644 packages/core/src/compiler/style/global-styles.ts create mode 100644 packages/core/src/compiler/style/normalize-styles.ts create mode 100644 packages/core/src/compiler/style/optimize-css.ts create mode 100644 packages/core/src/compiler/style/scope-css.ts create mode 100644 packages/core/src/compiler/style/style-utils.ts create mode 100644 packages/core/src/compiler/style/test/build-conditionals.spec.ts create mode 100644 packages/core/src/compiler/style/test/css-imports.spec.ts create mode 100644 packages/core/src/compiler/style/test/css-to-esm.spec.ts create mode 100644 packages/core/src/compiler/style/test/optimize-css.spec.ts create mode 100644 packages/core/src/compiler/style/test/style-rebuild.spec.ts create mode 100644 packages/core/src/compiler/style/test/style.spec.ts create mode 100644 packages/core/src/compiler/style/test/tsconfig.json create mode 100644 packages/core/src/compiler/sys/config.ts create mode 100644 packages/core/src/compiler/sys/environment.ts create mode 100644 packages/core/src/compiler/sys/fetch/fetch-module-async.ts create mode 100644 packages/core/src/compiler/sys/fetch/fetch-module-sync.ts create mode 100644 packages/core/src/compiler/sys/fetch/fetch-utils.ts create mode 100644 packages/core/src/compiler/sys/fetch/tests/fetch-module.spec.ts create mode 100644 packages/core/src/compiler/sys/fetch/write-fetch-success.ts create mode 100644 packages/core/src/compiler/sys/in-memory-fs.ts create mode 100644 packages/core/src/compiler/sys/node-require.ts create mode 100644 packages/core/src/compiler/sys/resolve/resolve-module-async.ts create mode 100644 packages/core/src/compiler/sys/resolve/resolve-module-sync.ts create mode 100644 packages/core/src/compiler/sys/resolve/resolve-utils.ts create mode 100644 packages/core/src/compiler/sys/resolve/tests/resolve-module.spec.ts create mode 100644 packages/core/src/compiler/sys/stencil-sys.ts create mode 100644 packages/core/src/compiler/sys/tests/in-memory-fs.spec.ts create mode 100644 packages/core/src/compiler/sys/tests/stencil-sys.spec.ts create mode 100644 packages/core/src/compiler/sys/typescript/tests/typescript-config.spec.ts create mode 100644 packages/core/src/compiler/sys/typescript/tests/typescript-resolve-module.spec.ts create mode 100644 packages/core/src/compiler/sys/typescript/tests/typescript-sys.spec.ts create mode 100644 packages/core/src/compiler/sys/typescript/typescript-config.ts create mode 100644 packages/core/src/compiler/sys/typescript/typescript-resolve-module.ts create mode 100644 packages/core/src/compiler/sys/typescript/typescript-sys.ts create mode 100644 packages/core/src/compiler/sys/worker/sys-worker.ts create mode 100644 packages/core/src/compiler/transformers/add-component-meta-proxy.ts create mode 100644 packages/core/src/compiler/transformers/add-component-meta-static.ts create mode 100644 packages/core/src/compiler/transformers/add-imports.ts create mode 100644 packages/core/src/compiler/transformers/add-static-style.ts create mode 100644 packages/core/src/compiler/transformers/add-tag-transform.ts create mode 100644 packages/core/src/compiler/transformers/automatic-key-insertion/automatic-key-insertion.spec.ts create mode 100644 packages/core/src/compiler/transformers/automatic-key-insertion/index.ts create mode 100644 packages/core/src/compiler/transformers/automatic-key-insertion/utils.ts create mode 100644 packages/core/src/compiler/transformers/collections/add-external-import.ts create mode 100644 packages/core/src/compiler/transformers/collections/parse-collection-components.ts create mode 100644 packages/core/src/compiler/transformers/collections/parse-collection-manifest.ts create mode 100644 packages/core/src/compiler/transformers/collections/parse-collection-module.ts create mode 100644 packages/core/src/compiler/transformers/component-build-conditionals.ts create mode 100644 packages/core/src/compiler/transformers/component-hydrate/hydrate-component.ts create mode 100644 packages/core/src/compiler/transformers/component-hydrate/hydrate-runtime-cmp-meta.ts create mode 100644 packages/core/src/compiler/transformers/component-hydrate/tranform-to-hydrate-component.ts create mode 100644 packages/core/src/compiler/transformers/component-lazy/attach-internals.ts create mode 100644 packages/core/src/compiler/transformers/component-lazy/constants.ts create mode 100644 packages/core/src/compiler/transformers/component-lazy/lazy-component.ts create mode 100644 packages/core/src/compiler/transformers/component-lazy/lazy-constructor.ts create mode 100644 packages/core/src/compiler/transformers/component-lazy/lazy-element-getter.ts create mode 100644 packages/core/src/compiler/transformers/component-lazy/transform-lazy-component.ts create mode 100644 packages/core/src/compiler/transformers/component-native/add-define-custom-element-function.ts create mode 100644 packages/core/src/compiler/transformers/component-native/attach-internals.ts create mode 100644 packages/core/src/compiler/transformers/component-native/native-component.ts create mode 100644 packages/core/src/compiler/transformers/component-native/native-connected-callback.ts create mode 100644 packages/core/src/compiler/transformers/component-native/native-constructor.ts create mode 100644 packages/core/src/compiler/transformers/component-native/native-element-getter.ts create mode 100644 packages/core/src/compiler/transformers/component-native/native-meta.ts create mode 100644 packages/core/src/compiler/transformers/component-native/native-static-style.ts create mode 100644 packages/core/src/compiler/transformers/component-native/proxy-custom-element-function.ts create mode 100644 packages/core/src/compiler/transformers/component-native/tranform-to-native-component.ts create mode 100644 packages/core/src/compiler/transformers/core-runtime-apis.ts create mode 100644 packages/core/src/compiler/transformers/create-event.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/attach-internals.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/component-decorator.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/convert-decorators.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/decorator-utils.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/decorators-constants.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/element-decorator.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/event-decorator.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/import-alias-map.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/listen-decorator.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/method-decorator.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/prop-decorator.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/serialize-decorators.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/state-decorator.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/style-to-static.ts create mode 100644 packages/core/src/compiler/transformers/decorators-to-static/watch-decorator.ts create mode 100644 packages/core/src/compiler/transformers/define-custom-element.ts create mode 100644 packages/core/src/compiler/transformers/detect-modern-prop-decls.ts create mode 100644 packages/core/src/compiler/transformers/host-data-transform.ts create mode 100644 packages/core/src/compiler/transformers/map-imports-to-path-aliases.ts create mode 100644 packages/core/src/compiler/transformers/reactive-handler-meta-transform.ts create mode 100644 packages/core/src/compiler/transformers/remove-collection-imports.ts create mode 100644 packages/core/src/compiler/transformers/remove-static-meta-properties.ts create mode 100644 packages/core/src/compiler/transformers/reserved-public-members.ts create mode 100644 packages/core/src/compiler/transformers/rewrite-aliased-paths.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/attach-internals.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/call-expression.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/class-extension.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/class-methods.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/component.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/element-ref.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/encapsulation.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/events.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/form-associated.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/import.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/listeners.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/methods.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/parse-static.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/props.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/serializers.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/states.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/string-literal.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/styles.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/vdom.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/visitor.ts create mode 100644 packages/core/src/compiler/transformers/static-to-meta/watchers.ts create mode 100644 packages/core/src/compiler/transformers/stencil-import-path.ts create mode 100644 packages/core/src/compiler/transformers/style-imports.ts create mode 100644 packages/core/src/compiler/transformers/test/add-component-meta-proxy.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/add-static-style.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/add-tag-transform.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/convert-decorators.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/core-runtime-apis.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/decorator-utils.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/detect-modern-prop-decls.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/fixtures/dessert.ts create mode 100644 packages/core/src/compiler/transformers/test/fixtures/meal-entry.ts create mode 100644 packages/core/src/compiler/transformers/test/functional-component-deps.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/lazy-component.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/map-imports-to-path-aliases.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/native-component.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-attach-internals.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-comments.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-component-tags.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-component.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-deserializers.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-element.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-encapsulation.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-events.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-form-associated.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-import-path.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-listeners.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-methods.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-mixin.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-props.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-serializers.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-slot-assignment.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-states.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-styles.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-vdom.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-virtual-props.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/parse-watch.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/proxy-custom-element-function.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/rewrite-aliased-paths.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/transform-utils.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/transpile.ts create mode 100644 packages/core/src/compiler/transformers/test/tsconfig.json create mode 100644 packages/core/src/compiler/transformers/test/type-library.spec.ts create mode 100644 packages/core/src/compiler/transformers/test/utils.ts create mode 100644 packages/core/src/compiler/transformers/transform-utils.ts create mode 100644 packages/core/src/compiler/transformers/type-library.ts create mode 100644 packages/core/src/compiler/transformers/update-component-class.ts create mode 100644 packages/core/src/compiler/transformers/update-stencil-core-import.ts create mode 100644 packages/core/src/compiler/transpile.ts create mode 100644 packages/core/src/compiler/transpile/create-build-program.ts create mode 100644 packages/core/src/compiler/transpile/create-watch-program.ts create mode 100644 packages/core/src/compiler/transpile/run-program.ts create mode 100644 packages/core/src/compiler/transpile/test/create-watch-program.spec.ts create mode 100644 packages/core/src/compiler/transpile/test/run-program.spec.ts create mode 100644 packages/core/src/compiler/transpile/transpile-module.ts create mode 100644 packages/core/src/compiler/transpile/transpiled-module.ts create mode 100644 packages/core/src/compiler/transpile/ts-config.ts create mode 100644 packages/core/src/compiler/transpile/validate-components.ts create mode 100644 packages/core/src/compiler/types/constants.ts create mode 100644 packages/core/src/compiler/types/generate-app-types.ts create mode 100644 packages/core/src/compiler/types/generate-component-types.ts create mode 100644 packages/core/src/compiler/types/generate-event-detail-types.ts create mode 100644 packages/core/src/compiler/types/generate-event-listener-types.ts create mode 100644 packages/core/src/compiler/types/generate-event-types.ts create mode 100644 packages/core/src/compiler/types/generate-method-types.ts create mode 100644 packages/core/src/compiler/types/generate-prop-types.ts create mode 100644 packages/core/src/compiler/types/generate-types.ts create mode 100644 packages/core/src/compiler/types/package-json-log-utils.ts create mode 100644 packages/core/src/compiler/types/stencil-types.ts create mode 100644 packages/core/src/compiler/types/tests/ComponentCompilerEvent.stub.ts create mode 100644 packages/core/src/compiler/types/tests/ComponentCompilerMeta.stub.ts create mode 100644 packages/core/src/compiler/types/tests/ComponentCompilerMethod.stub.ts create mode 100644 packages/core/src/compiler/types/tests/ComponentCompilerProperty.stub.ts create mode 100644 packages/core/src/compiler/types/tests/ComponentCompilerTypeReference.stub.ts create mode 100644 packages/core/src/compiler/types/tests/ComponentCompilerVirtualProperty.stub.ts create mode 100644 packages/core/src/compiler/types/tests/TypesImportData.stub.ts create mode 100644 packages/core/src/compiler/types/tests/__snapshots__/generate-app-types.spec.ts.snap create mode 100644 packages/core/src/compiler/types/tests/generate-app-types.spec.ts create mode 100644 packages/core/src/compiler/types/tests/generate-component-types.spec.ts create mode 100644 packages/core/src/compiler/types/tests/generate-event-detail-types.spec.ts create mode 100644 packages/core/src/compiler/types/tests/generate-event-listener-types.spec.ts create mode 100644 packages/core/src/compiler/types/tests/generate-event-types.spec.ts create mode 100644 packages/core/src/compiler/types/tests/generate-method-types.spec.ts create mode 100644 packages/core/src/compiler/types/tests/generate-prop-types.spec.ts create mode 100644 packages/core/src/compiler/types/tests/stencil-types.spec.ts create mode 100644 packages/core/src/compiler/types/tests/tsconfig.json create mode 100644 packages/core/src/compiler/types/tests/validate-package-json.spec.ts create mode 100644 packages/core/src/compiler/types/tests/validate-primary-package-output-target.spec.ts create mode 100644 packages/core/src/compiler/types/types-utils.ts create mode 100644 packages/core/src/compiler/types/update-import-refs.ts create mode 100644 packages/core/src/compiler/types/validate-build-package-json.ts create mode 100644 packages/core/src/compiler/types/validate-primary-package-output-target.ts create mode 100644 packages/core/src/compiler/worker/main-thread.ts create mode 100644 packages/core/src/compiler/worker/worker-thread.ts create mode 100644 packages/core/src/declarations/child_process.ts create mode 100644 packages/core/src/declarations/index.ts create mode 100644 packages/core/src/declarations/readme.md create mode 100644 packages/core/src/declarations/stencil-ext-modules.d.ts create mode 100644 packages/core/src/declarations/stencil-private.ts create mode 100644 packages/core/src/declarations/stencil-public-compiler.ts create mode 100644 packages/core/src/declarations/stencil-public-docs.ts create mode 100644 packages/core/src/declarations/stencil-public-runtime.ts create mode 100644 packages/core/src/internal/default.ts create mode 100644 packages/core/src/internal/index.ts create mode 100644 packages/core/src/internal/readme.md create mode 100644 packages/core/src/internal/stencil-core/index.cjs create mode 100644 packages/core/src/internal/stencil-core/index.d.ts create mode 100644 packages/core/src/internal/stencil-core/index.js create mode 100644 packages/core/src/internal/stencil-core/jsx-dev-runtime.cjs create mode 100644 packages/core/src/internal/stencil-core/jsx-dev-runtime.d.ts create mode 100644 packages/core/src/internal/stencil-core/jsx-dev-runtime.js create mode 100644 packages/core/src/internal/stencil-core/jsx-runtime.cjs create mode 100644 packages/core/src/internal/stencil-core/jsx-runtime.d.ts create mode 100644 packages/core/src/internal/stencil-core/jsx-runtime.js create mode 100644 packages/core/src/internal/testing/jsx-dev-runtime.d.ts create mode 100644 packages/core/src/internal/testing/jsx-dev-runtime.js create mode 100644 packages/core/src/internal/testing/jsx-runtime.d.ts create mode 100644 packages/core/src/internal/testing/jsx-runtime.js create mode 100644 packages/core/src/runtime/asset-path.ts create mode 100644 packages/core/src/runtime/bootstrap-custom-element.ts create mode 100644 packages/core/src/runtime/bootstrap-lazy.ts create mode 100644 packages/core/src/runtime/client-hydrate.ts create mode 100644 packages/core/src/runtime/connected-callback.ts create mode 100644 packages/core/src/runtime/disconnected-callback.ts create mode 100644 packages/core/src/runtime/dom-extras.ts create mode 100644 packages/core/src/runtime/element.ts create mode 100644 packages/core/src/runtime/event-emitter.ts create mode 100644 packages/core/src/runtime/fragment.ts create mode 100644 packages/core/src/runtime/hmr-component.ts create mode 100644 packages/core/src/runtime/host-listener.ts create mode 100644 packages/core/src/runtime/index.ts create mode 100644 packages/core/src/runtime/initialize-component.ts create mode 100644 packages/core/src/runtime/mixin.ts create mode 100644 packages/core/src/runtime/mode.ts create mode 100644 packages/core/src/runtime/nonce.ts create mode 100644 packages/core/src/runtime/parse-property-value.ts create mode 100644 packages/core/src/runtime/platform-options.ts create mode 100644 packages/core/src/runtime/profile.ts create mode 100644 packages/core/src/runtime/proxy-component.ts create mode 100644 packages/core/src/runtime/readme.md create mode 100644 packages/core/src/runtime/render.ts create mode 100644 packages/core/src/runtime/runtime-constants.ts create mode 100644 packages/core/src/runtime/set-value.ts create mode 100644 packages/core/src/runtime/slot-polyfill-utils.ts create mode 100644 packages/core/src/runtime/styles.ts create mode 100644 packages/core/src/runtime/tag-transform.ts create mode 100644 packages/core/src/runtime/test/assets.spec.tsx create mode 100644 packages/core/src/runtime/test/attr-deserialize.spec.tsx create mode 100644 packages/core/src/runtime/test/attr-prop-prefix.spec.tsx create mode 100644 packages/core/src/runtime/test/attr.spec.tsx create mode 100644 packages/core/src/runtime/test/before-each.spec.tsx create mode 100644 packages/core/src/runtime/test/bootstrap-lazy.spec.tsx create mode 100644 packages/core/src/runtime/test/client-hydrate-to-vdom.spec.tsx create mode 100644 packages/core/src/runtime/test/component-class.spec.tsx create mode 100644 packages/core/src/runtime/test/component-error-handling.spec.tsx create mode 100644 packages/core/src/runtime/test/dom-extras.spec.tsx create mode 100644 packages/core/src/runtime/test/element.spec.tsx create mode 100644 packages/core/src/runtime/test/event.spec.tsx create mode 100644 packages/core/src/runtime/test/extends-basic.spec.tsx create mode 100644 packages/core/src/runtime/test/fetch.spec.tsx create mode 100644 packages/core/src/runtime/test/fixtures/cmp-a.css create mode 100644 packages/core/src/runtime/test/fixtures/cmp-a.tsx create mode 100644 packages/core/src/runtime/test/fixtures/cmp-asset.tsx create mode 100644 packages/core/src/runtime/test/fixtures/utils.ts create mode 100644 packages/core/src/runtime/test/globals.spec.tsx create mode 100644 packages/core/src/runtime/test/host.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-no-encapsulation.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-prop.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-scoped.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-shadow-child.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-shadow-in-shadow.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-shadow-parent.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-shadow.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-slot-fallback.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-slotted-content-order.spec.tsx create mode 100644 packages/core/src/runtime/test/hydrate-style-element.spec.tsx create mode 100644 packages/core/src/runtime/test/initialize-component.spec.tsx create mode 100644 packages/core/src/runtime/test/jsx.spec.tsx create mode 100644 packages/core/src/runtime/test/lifecycle-async.spec.tsx create mode 100644 packages/core/src/runtime/test/lifecycle-sync.spec.tsx create mode 100644 packages/core/src/runtime/test/listen.spec.tsx create mode 100644 packages/core/src/runtime/test/method.spec.tsx create mode 100644 packages/core/src/runtime/test/mixin.spec.tsx create mode 100644 packages/core/src/runtime/test/parse-property-value.spec.ts create mode 100644 packages/core/src/runtime/test/prop-serialize.spec.tsx create mode 100644 packages/core/src/runtime/test/prop-warnings.spec.tsx create mode 100644 packages/core/src/runtime/test/prop.spec.tsx create mode 100644 packages/core/src/runtime/test/queue.spec.tsx create mode 100644 packages/core/src/runtime/test/regression-json-string-non-parsing.spec.tsx create mode 100644 packages/core/src/runtime/test/render-text.spec.tsx create mode 100644 packages/core/src/runtime/test/render-vdom.spec.tsx create mode 100644 packages/core/src/runtime/test/scoped.spec.tsx create mode 100644 packages/core/src/runtime/test/shadow.spec.tsx create mode 100644 packages/core/src/runtime/test/state.spec.tsx create mode 100644 packages/core/src/runtime/test/style.spec.tsx create mode 100644 packages/core/src/runtime/test/svg-element.spec.tsx create mode 100644 packages/core/src/runtime/test/tsconfig.json create mode 100644 packages/core/src/runtime/test/update-component.spec.tsx create mode 100644 packages/core/src/runtime/test/vdom-relocation.spec.tsx create mode 100644 packages/core/src/runtime/test/watch.spec.tsx create mode 100644 packages/core/src/runtime/update-component.ts create mode 100644 packages/core/src/runtime/vdom/h.ts create mode 100644 packages/core/src/runtime/vdom/jsx-dev-runtime.ts create mode 100644 packages/core/src/runtime/vdom/jsx-runtime.ts create mode 100644 packages/core/src/runtime/vdom/set-accessor.ts create mode 100644 packages/core/src/runtime/vdom/test/__snapshots__/vdom-annotations.spec.tsx.snap create mode 100755 packages/core/src/runtime/vdom/test/attributes.spec.ts create mode 100644 packages/core/src/runtime/vdom/test/event-listeners.spec.ts create mode 100644 packages/core/src/runtime/vdom/test/h.spec.ts create mode 100644 packages/core/src/runtime/vdom/test/is-same-vnode.spec.ts create mode 100644 packages/core/src/runtime/vdom/test/jsx-runtime.spec.ts create mode 100644 packages/core/src/runtime/vdom/test/patch-svg.spec.ts create mode 100755 packages/core/src/runtime/vdom/test/patch.spec.ts create mode 100644 packages/core/src/runtime/vdom/test/scoped-slot.spec.tsx create mode 100644 packages/core/src/runtime/vdom/test/set-accessor.spec.ts create mode 100644 packages/core/src/runtime/vdom/test/tsconfig.json create mode 100644 packages/core/src/runtime/vdom/test/update-element.spec.ts create mode 100644 packages/core/src/runtime/vdom/test/util.spec.ts create mode 100644 packages/core/src/runtime/vdom/test/vdom-annotations.spec.tsx create mode 100644 packages/core/src/runtime/vdom/test/vdom-render.spec.tsx create mode 100644 packages/core/src/runtime/vdom/update-element.ts create mode 100755 packages/core/src/runtime/vdom/util.ts create mode 100644 packages/core/src/runtime/vdom/vdom-annotations.ts create mode 100644 packages/core/src/runtime/vdom/vdom-render.ts create mode 100644 packages/core/src/server/platform/h-async.ts create mode 100644 packages/core/src/server/platform/hydrate-app.ts create mode 100644 packages/core/src/server/platform/index.ts create mode 100644 packages/core/src/server/platform/proxy-host-element.ts create mode 100644 packages/core/src/server/platform/test/__mocks__/@app-globals/index.ts create mode 100644 packages/core/src/server/platform/test/serialize-shadow-root-opts.spec.ts create mode 100644 packages/core/src/server/runner/create-window.ts create mode 100644 packages/core/src/server/runner/hydrate-factory.ts create mode 100644 packages/core/src/server/runner/index.ts create mode 100644 packages/core/src/server/runner/inspect-element.ts create mode 100644 packages/core/src/server/runner/patch-dom-implementation.ts create mode 100644 packages/core/src/server/runner/render-utils.ts create mode 100644 packages/core/src/server/runner/render.ts create mode 100644 packages/core/src/server/runner/runtime-log.ts create mode 100644 packages/core/src/server/runner/window-initialize.ts create mode 100644 packages/core/src/sys/node/bundles/autoprefixer.js create mode 100644 packages/core/src/sys/node/bundles/glob.js create mode 100644 packages/core/src/sys/node/bundles/graceful-fs.js create mode 100644 packages/core/src/sys/node/bundles/node-fetch.js create mode 100644 packages/core/src/sys/node/bundles/prompts.js create mode 100644 packages/core/src/sys/node/index.ts create mode 100644 packages/core/src/sys/node/logger/index.ts create mode 100644 packages/core/src/sys/node/logger/terminal-logger.ts create mode 100644 packages/core/src/sys/node/logger/test/terminal-logger.spec.ts create mode 100644 packages/core/src/sys/node/node-copy-tasks.ts create mode 100644 packages/core/src/sys/node/node-fs-promisify.ts create mode 100644 packages/core/src/sys/node/node-lazy-require.ts create mode 100644 packages/core/src/sys/node/node-resolve-module.ts create mode 100644 packages/core/src/sys/node/node-setup-process.ts create mode 100644 packages/core/src/sys/node/node-stencil-version-checker.ts create mode 100644 packages/core/src/sys/node/node-sys.ts create mode 100755 packages/core/src/sys/node/node-worker-controller.ts create mode 100644 packages/core/src/sys/node/node-worker-main.ts create mode 100755 packages/core/src/sys/node/node-worker-thread.ts create mode 100644 packages/core/src/sys/node/public.ts create mode 100644 packages/core/src/sys/node/test/node-lazy-require.spec.ts create mode 100644 packages/core/src/sys/node/test/test-worker-main.ts create mode 100644 packages/core/src/sys/node/test/tsconfig.json create mode 100644 packages/core/src/sys/node/test/worker-manager.spec.ts create mode 100644 packages/core/src/sys/node/worker.ts create mode 100644 packages/core/src/utils/byte-size.ts create mode 100644 packages/core/src/utils/constants.ts create mode 100644 packages/core/src/utils/es2022-rewire-class-members.ts create mode 100644 packages/core/src/utils/format-component-runtime-meta.ts create mode 100644 packages/core/src/utils/helpers.ts create mode 100644 packages/core/src/utils/index.ts create mode 100644 packages/core/src/utils/is-glob.ts create mode 100644 packages/core/src/utils/is-root-path.ts create mode 100644 packages/core/src/utils/local-value.ts create mode 100644 packages/core/src/utils/logger/logger-rollup.ts create mode 100644 packages/core/src/utils/logger/logger-typescript.ts create mode 100644 packages/core/src/utils/logger/logger-utils.ts create mode 100644 packages/core/src/utils/message-utils.ts create mode 100644 packages/core/src/utils/output-target.ts create mode 100644 packages/core/src/utils/path.ts create mode 100644 packages/core/src/utils/query-nonce-meta-tag-content.ts create mode 100644 packages/core/src/utils/regular-expression.ts create mode 100644 packages/core/src/utils/remote-value.ts create mode 100644 packages/core/src/utils/result.ts create mode 100644 packages/core/src/utils/serialize.ts create mode 100644 packages/core/src/utils/shadow-css.ts create mode 100644 packages/core/src/utils/shadow-root.ts create mode 100644 packages/core/src/utils/sourcemaps.ts create mode 100644 packages/core/src/utils/style.ts create mode 100644 packages/core/src/utils/test/helpers.spec.ts create mode 100644 packages/core/src/utils/test/is-root-path.spec.ts create mode 100644 packages/core/src/utils/test/message-utils.spec.ts create mode 100644 packages/core/src/utils/test/output-target.spec.ts create mode 100644 packages/core/src/utils/test/path.spec.ts create mode 100644 packages/core/src/utils/test/query-nonce-meta-tag-content.spec.ts create mode 100644 packages/core/src/utils/test/regular-expression.spec.ts create mode 100644 packages/core/src/utils/test/result.spec.ts create mode 100644 packages/core/src/utils/test/scope-css.spec.ts create mode 100644 packages/core/src/utils/test/serialize.spec.ts create mode 100644 packages/core/src/utils/test/sourcemaps.spec.ts create mode 100644 packages/core/src/utils/test/tsconfig.json create mode 100644 packages/core/src/utils/test/url-paths.spec.ts create mode 100644 packages/core/src/utils/test/util.spec.ts create mode 100644 packages/core/src/utils/test/validation.spec.ts create mode 100644 packages/core/src/utils/types.ts create mode 100644 packages/core/src/utils/url-paths.ts create mode 100644 packages/core/src/utils/util.ts create mode 100644 packages/core/src/utils/validation.ts create mode 100644 packages/core/src/version.ts create mode 100644 packages/core/vite.app-data.config.ts create mode 100644 packages/core/vite.app-globals.config.ts create mode 100644 packages/core/vite.client.config.ts create mode 100644 packages/core/vite.config.ts create mode 100644 packages/core/vite.internal.config.ts create mode 100644 packages/core/vite.server.config.ts create mode 100644 packages/mock-doc/package.json create mode 100644 packages/mock-doc/src/attribute.ts create mode 100644 packages/mock-doc/src/comment-node.ts create mode 100644 packages/mock-doc/src/console.ts create mode 100644 packages/mock-doc/src/constants.ts create mode 100644 packages/mock-doc/src/css-style-declaration.ts create mode 100644 packages/mock-doc/src/css-style-sheet.ts create mode 100644 packages/mock-doc/src/custom-element-registry.ts create mode 100644 packages/mock-doc/src/dataset.ts create mode 100644 packages/mock-doc/src/document-fragment.ts create mode 100644 packages/mock-doc/src/document-type-node.ts create mode 100644 packages/mock-doc/src/document.ts create mode 100644 packages/mock-doc/src/element.ts create mode 100644 packages/mock-doc/src/event.ts create mode 100644 packages/mock-doc/src/global.ts create mode 100644 packages/mock-doc/src/headers.ts create mode 100644 packages/mock-doc/src/history.ts create mode 100644 packages/mock-doc/src/index.ts create mode 100644 packages/mock-doc/src/intersection-observer.ts create mode 100644 packages/mock-doc/src/location.ts create mode 100644 packages/mock-doc/src/navigator.ts create mode 100644 packages/mock-doc/src/node.ts create mode 100644 packages/mock-doc/src/parse-html.ts create mode 100644 packages/mock-doc/src/parse-util.ts create mode 100644 packages/mock-doc/src/parser.ts create mode 100644 packages/mock-doc/src/performance.ts create mode 100644 packages/mock-doc/src/request-response.ts create mode 100644 packages/mock-doc/src/resize-observer.ts create mode 100644 packages/mock-doc/src/selector.ts create mode 100644 packages/mock-doc/src/serialize-node.ts create mode 100644 packages/mock-doc/src/shadow-root.ts create mode 100644 packages/mock-doc/src/storage.ts create mode 100644 packages/mock-doc/src/test/attribute.spec.ts create mode 100644 packages/mock-doc/src/test/clone.spec.ts create mode 100644 packages/mock-doc/src/test/css-style-declaration.spec.ts create mode 100644 packages/mock-doc/src/test/css-style-sheet.spec.ts create mode 100644 packages/mock-doc/src/test/custom-elements.spec.ts create mode 100644 packages/mock-doc/src/test/dataset.spec.ts create mode 100644 packages/mock-doc/src/test/doc-style.spec.ts create mode 100644 packages/mock-doc/src/test/document-fragment.spec.ts create mode 100644 packages/mock-doc/src/test/element.spec.ts create mode 100644 packages/mock-doc/src/test/event.spec.ts create mode 100644 packages/mock-doc/src/test/global.spec.ts create mode 100644 packages/mock-doc/src/test/headers.spec.ts create mode 100644 packages/mock-doc/src/test/html-parse.spec.ts create mode 100644 packages/mock-doc/src/test/location.spec.ts create mode 100644 packages/mock-doc/src/test/match-media.spec.ts create mode 100644 packages/mock-doc/src/test/request-response.spec.ts create mode 100644 packages/mock-doc/src/test/selector.spec.ts create mode 100644 packages/mock-doc/src/test/serialize-node.spec.ts create mode 100644 packages/mock-doc/src/test/shadow-dom-event-bubbling.spec.ts create mode 100644 packages/mock-doc/src/test/storage.spec.ts create mode 100644 packages/mock-doc/src/test/token-list.spec.ts create mode 100644 packages/mock-doc/src/third-party/jquery.ts create mode 100644 packages/mock-doc/src/token-list.ts create mode 100644 packages/mock-doc/src/window.ts create mode 100644 packages/mock-doc/vite.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 vite-prototype/index.html create mode 100644 vite-prototype/package-lock.json create mode 100644 vite-prototype/package.json create mode 100644 vite-prototype/plugin.js create mode 100644 vite-prototype/src/components/my-button.tsx create mode 100644 vite-prototype/src/main.js create mode 100644 vite-prototype/vite.config.js diff --git a/V5_PLANNING.md b/V5_PLANNING.md new file mode 100644 index 00000000000..1cd13c6254a --- /dev/null +++ b/V5_PLANNING.md @@ -0,0 +1,1204 @@ +# Stencil v5 Planning Document + +> **Living Document** - This plan will evolve across multiple sessions as we work through the unknowns and refine the approach. + +## Vision + +Stencil v5 represents a major modernization effort after 10 years of development. The goal is to shed accumulated tech debt, embrace modern tooling, and simplify the architecture while maintaining Stencil's core value proposition: a compiler for building fast, reusable web components. + +--- + +## Major Goals + +### 1. 🧪 Remove Integrated Testing (Jest/Puppeteer) +**Status:** Planning + +Remove the deeply integrated Jest runners and Puppeteer integration that have constrained Stencil's evolution. + +### 2. 🗑️ Remove Legacy Features +**Status:** Planning + +- ES5 builds +- CommonJS (internally, potentially keep as output option) +- Ancient browser polyfills +- In-browser compilation + +### 3. ⚡ Move to Vite +**Status:** Exploring + +Replace the entire custom build infrastructure with Vite - including the scripts/esbuild internal bundling, Rollup component bundling, and custom dev server. + +### 4. 📦 Mono-repo Restructure +**Status:** Exploring + +Move to a proper mono-repo structure (potentially using Nx or similar). This enables: +- Extracting self-contained pieces into their own packages (e.g., mock-doc) +- Bringing in external output targets that are currently separate repos +- Better separation of concerns +- Independent versioning where appropriate + +--- + +## Current Architecture Analysis + +### Build Pipeline (What We Have Now) + +``` +Source (.tsx) + ↓ +[TypeScript + Custom Transformers] + ↓ +[esbuild] ─── Internal bundling (CLI, compiler, dev-server, testing) + └── scripts/esbuild/*.ts (TO BE REMOVED) + ↓ +[Rollup + 10+ Custom Plugins] ─── Component bundling + ↓ +[Custom Dev Server + HMR] + ↓ +Output Targets (dist-lazy, dist-custom-elements, etc.) +``` + +### Key Directories + +| Path | Purpose | v5 Impact | +|------|---------|-----------| +| `src/testing/` | Jest/Puppeteer integration | **REMOVE** | +| `src/testing/jest/` | Multi-version Jest support (27-29) | **REMOVE** | +| `src/testing/puppeteer/` | Browser automation | **REMOVE** | +| `src/mock-doc/` | DOM mocking for SSR/hydration | **KEEP** - Move to own package in mono-repo | +| `src/dev-server/` | Custom dev server + HMR | **REPLACE** with Vite | +| `src/client/polyfills/` | ES5/legacy browser polyfills | **REMOVE** | +| `src/compiler/bundle/` | Rollup bundling | **REPLACE** with Vite | +| `scripts/esbuild/` | Internal bundling infrastructure | **REMOVE** - Vite handles this | + +--- + +## Goal 1: Remove Integrated Testing + +### Current State + +The testing integration is extensive: + +``` +src/testing/ +├── jest/ +│ ├── jest-29/ # Jest 29 adapter +│ ├── jest-28/ # Jest 28 adapter +│ ├── jest-27-and-under/ # Legacy Jest adapter +│ ├── jest-apis.ts # newSpecPage(), newE2EPage() +│ └── jest-stencil-connector.ts +├── puppeteer/ +│ ├── puppeteer-browser.ts +│ ├── puppeteer-page.ts +│ ├── puppeteer-element.ts +│ └── ... +└── index.ts +``` + +### Why It's Problematic + +1. **Version lock-in**: Supporting Jest 27-29 simultaneously is maintenance burden +2. **Coupling**: Custom Jest preprocessor reaches deep into compiler internals +3. **Puppeteer fragility**: Browser automation is notoriously flaky +4. **TypeScript on-the-fly**: The Jest preprocessor compiles `.tsx` using Stencil's compiler, creating tight coupling + +### The Replacement Packages ✅ AUDITED + +We have **two packages** that together fully replace the built-in testing: + +| Package | Replaces | Purpose | +|---------|----------|---------| +| `@stencil/vitest` | `newSpecPage()` + Jest | Unit/spec testing (DOM environments) | +| `@stencil/playwright` | `newE2EPage()` + Puppeteer | E2E testing (real browsers) | + +**Key Clarification**: Inline testing (compiling TypeScript on-the-fly within tests) is **only used internally by Stencil**, not by end users. This means: +- User migration path is straightforward +- Internal migration is the complex part + +--- + +#### `@stencil/vitest` - Unit/Spec Testing + +**Core API:** `render()` returns `{ root, instance, waitForChanges, setProps, spyOnEvent, unmount }` + +| Feature | Jest (`@stencil/core`) | Vitest (`@stencil/vitest`) | +|---------|------------------------|----------------------------| +| Component rendering | `newSpecPage()` | `render()` ✅ | +| DOM environments | Fixed (jsdom) | Configurable: mock-doc, jsdom, happy-dom ✅ | +| Event spying | Manual | `spyOnEvent()` + 6 matchers ✅ | +| Custom matchers | 15+ | 19+ ✅ | +| Shadow DOM assertions | ✅ | ✅ (with `` format) | +| Instance access | Via page methods | Via `instance` property ✅ | +| Props/state changes | Properties + methods | `setProps()` + `waitForChanges()` ✅ | + +**Architecture Difference:** +- **Jest (current):** Compiles TypeScript on-the-fly within tests +- **Vitest:** Components pre-compiled by Stencil, then tested + +This is *cleaner* - separates build from test, matching production. + +**CLI:** `stencil-test` orchestrates Stencil build + Vitest execution + +--- + +#### `@stencil/playwright` - E2E Testing + +**Core API:** Extended Playwright with Stencil fixtures + +| Feature | Puppeteer (Core) | Playwright (`@stencil/playwright`) | +|---------|------------------|-------------------------------------| +| Browser support | Chrome only | Chrome, Firefox, WebKit ✅ | +| Test runner | Jest | Playwright test runner ✅ | +| Page API | `E2EPage` with find/findAll | Playwright locators ✅ | +| Event spying | `spyOnEvent()` | `page.spyOnEvent()` + `locator.spyOnEvent()` ✅ | +| Matchers | 5 built-in | 5 custom Playwright matchers ✅ | +| Hydration waiting | `waitForChanges()` | `waitForChanges()` ✅ | + +**Event Testing:** +```typescript +const spy = await page.spyOnEvent('myEvent'); +// ... trigger event ... +expect(spy).toHaveReceivedEvent(); +expect(spy).toHaveReceivedEventDetail({ value: 42 }); +``` + +**Config:** `createConfig()` reads `stencil.config.ts` automatically + +--- + +#### Migration Complexity: LOW for Users + +```typescript +// Before (Jest + newSpecPage) +const page = await newSpecPage({ + components: [MyButton], + html: 'Click' +}); + +// After (Vitest) +const { root } = await render(Click); +``` + +```typescript +// Before (Jest + newE2EPage + Puppeteer) +const page = await newE2EPage(); +await page.setContent('Click'); + +// After (Playwright) +await page.setContent('Click'); +await page.waitForChanges(); +``` + +--- + +### Open Questions + +1. **Migration tooling?** + - Codemod for `newSpecPage()` → `render()` syntax? + - Auto-generate `vitest-setup.ts` from stencil.config? + +2. **Documentation priority?** + - Clear migration guide from Jest → Vitest + Playwright + +### Proposed Approach + +``` +Phase 1: Document current test API surface +Phase 2: Audit @stencil/vitest capabilities +Phase 3: Deprecation warnings in v4.x +Phase 4: Remove in v5.0 +``` + +### Internal Test Migration + +Since inline testing is internal-only, we need to: + +1. Audit which internal tests use `newSpecPage()` / `newE2EPage()` +2. Migrate Stencil's own tests to Vitest +3. This is a prerequisite for removing `src/testing/` + +### Files to Remove + +``` +src/testing/ # Entire directory +scripts/esbuild/testing.ts # Build script +testing/ # Distribution output +``` + +--- + +## Goal 2: Remove Legacy Features + +### 2.1 ES5 Builds + +**Current Implementation:** +- `src/client/polyfills/` - Core polyfills +- `src/compiler/app-core/app-es5-disabled.ts` - Feature flags for ES5 +- Output targets generate dual ES5/ES2017+ builds +- `src/client/polyfills/es5-html-element.ts` - IE11 HTMLElement shim + +**Impact:** +- Simplifies output targets significantly +- Removes need for dual builds +- Removes complex polyfill injection logic + +**Files to Remove:** +``` +src/client/polyfills/ +src/compiler/app-core/app-es5-disabled.ts +# Plus ES5-related code in output targets +``` + +**Open Questions:** +1. What's the browser support floor for v5? (ES2020? ES2022?) +2. Do we still need any polyfills for web component APIs? + +### 2.2 CommonJS + +**Current State:** +- Internal build uses CJS for Node.js entry points +- Output targets support CJS bundles +- Rollup plugins handle CJS interop + +**Proposed Change:** +- Internal: Move to pure ESM +- External: Consider keeping CJS output as an option (for users with legacy bundlers) + +**Open Questions:** +1. What Node.js version is the floor for v5? +2. Do users need CJS output, or can we drop it entirely? + +### 2.3 Ancient Browser Polyfills + +**Files to Remove:** +``` +src/client/polyfills/system.js # SystemJS loader +src/client/polyfills/dom.js # Promise, fetch polyfills +src/client/polyfills/core-js.js # Core.js integration +``` + +**Keep (maybe):** +- ResizeObserver polyfill? (Still patchy support) +- Custom Elements polyfill for specific edge cases? + +### 2.4 In-Browser Compilation + +**Current Implementation:** +- `src/compiler/transpile.ts` - Browser-compatible transpile API +- Bundled TypeScript (~9MB in compiler bundle) +- Used for: playground tools, live editors, REPL experiences + +**Why Remove:** +- Massive bundle size impact +- Rare use case +- Modern tools (StackBlitz, CodeSandbox) handle this better + +**Open Questions:** +1. Who uses in-browser compilation? +2. Can we provide a separate package for this use case? +3. What about the Stencil playground/documentation? + +--- + +## Goal 3: Move to Vite + +### Current Custom Stack (To Be Replaced) + +``` +┌─────────────────────────────────────────────┐ +│ scripts/esbuild/ (REMOVE) │ +│ - compiler.ts, cli.ts, dev-server.ts │ +│ - internal.ts, mock-doc.ts, etc. │ +│ - Custom build orchestration │ +└─────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────┐ +│ Custom Dev Server (src/dev-server/) REMOVE │ +│ - HTTP server │ +│ - WebSocket HMR │ +│ - Worker thread architecture │ +└─────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────┐ +│ Rollup Bundling (src/compiler/bundle/) │ +│ - 10+ custom plugins │ +│ - Component graph analysis │ +│ - Output target generation │ +└─────────────────────────────────────────────┘ +``` + +### Proposed Vite Architecture + +``` +┌─────────────────────────────────────────────┐ +│ Vite │ +│ - Dev server (built-in) │ +│ - HMR (built-in) │ +│ - Rollup bundling (built-in) │ +│ - esbuild transforms (built-in) │ +└─────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────┐ +│ Stencil Vite Plugin │ +│ - Component transformations │ +│ - Build conditionals │ +│ - Output target generation │ +└─────────────────────────────────────────────┘ +``` + +### What Gets Removed + +The entire `scripts/esbuild/` directory: +``` +scripts/esbuild/ +├── compiler.ts # REMOVE - Vite handles compiler bundling +├── internal.ts # REMOVE - Vite handles runtime bundling +├── dev-server.ts # REMOVE - Vite IS the dev server +├── cli.ts # REMOVE - Vite handles CLI bundling +├── mock-doc.ts # REMOVE - Vite handles mock-doc bundling +├── screenshot.ts # REMOVE +├── sys-node.ts # REMOVE +├── testing.ts # REMOVE +├── internal-platform-*.ts # REMOVE +└── utils/ # REMOVE + ├── options.ts + ├── banner.ts + ├── typescript-source.ts + ├── terser.ts + └── parse5.ts +``` + +### Benefits of Vite + +1. **Maintained by others**: Dev server, HMR, bundling maintained by Vite team +2. **Ecosystem**: Access to Vite plugin ecosystem +3. **Performance**: Native ESM dev server, optimized builds +4. **Familiarity**: Developers know Vite +5. **Drastically simpler codebase**: Remove 20+ custom build scripts + +### Challenges + +1. **Custom transformers**: We have complex TypeScript transformers +2. **Build conditionals**: 50+ feature flags that optimize bundles +3. **Output targets**: Multiple output formats (lazy, custom-elements, hydrate) +4. **Component graph**: Dependency analysis for code splitting + +### Open Questions + +1. **Can Vite plugins handle our transformer complexity?** + - Need to prototype + +2. **How do we preserve build conditionals?** + - Dead code elimination in Vite/Rollup? + - Custom plugin approach? + +3. **What happens to output targets?** + - Vite has its own output format opinions + - Can we still support dist-lazy, dist-custom-elements, etc.? + +4. **Migration path?** + - Gradual adoption or big bang? + - Can users opt-in during v4.x? + +5. **Internal builds?** + - Does Vite build Stencil itself, or just user projects? + - Might need Vite for both internal + external + +### Prototype Plan + +1. Create a minimal Vite plugin that compiles a single Stencil component +2. Validate that transformers work within Vite's plugin API +3. Test HMR behavior with Stencil components +4. Evaluate build output against current Rollup output + +--- + +### Vite Plugin Architecture ✅ DESIGNED + +#### What Stencil Is + +**Stencil = Web Component Compiler + Runtime** + +- **Compiler**: Transforms `@Component` TSX → optimized JS (the transformers) +- **Runtime**: Reactivity, vDOM, lifecycle, lazy loading coordination + +The runtime is just JavaScript - Vite/Rollup bundles it like any other dependency. The complexity is in the compiler integration. + +#### Two Modes + +**Dev Mode (Simple - Per-file)** +``` +Request → Vite → transform hook → Stencil compile → Response +``` +- Transform each `.tsx` file on-demand as Vite requests it +- Include full runtime (no tree-shaking needed in dev) +- HMR via Vite's built-in system + +**Build Mode (Complex - Whole-project)** +``` +buildStart → Analyze ALL components → Configure Rollup → Optimized output +``` +- `buildStart` hook: Scan all components, build component graph +- Determine which of 50+ features are actually used +- Configure Rollup with build conditionals for tree-shaking +- Generate lazy-loading manifests if needed + +#### Plugin Hooks + +```typescript +export function stencil(options): Plugin { + let componentRegistry: Map; + let buildConditionals: BuildConditionals; + + return { + name: 'vite-plugin-stencil', + enforce: 'pre', + + // === BUILD MODE: Whole-project analysis === + async buildStart() { + // Scan all .tsx files for @Component + // Build component graph (which components use which) + // Determine which runtime features are used + // Store in buildConditionals for transform phase + }, + + // === DEV + BUILD: Per-file transformation === + async transform(code, id) { + if (!isStencilComponent(code)) return null; + + // In dev: use full runtime, fast compilation + // In build: apply buildConditionals from analysis phase + const result = await compileComponent(code, id, buildConditionals); + + return { code: result.code, map: result.map }; + }, + + // === BUILD MODE: Output generation === + generateBundle(options, bundle) { + // For lazy output: generate loader manifest + // For custom-elements: nothing extra needed + }, + + // === DEV MODE: HMR === + handleHotUpdate(ctx) { + // Component changed → re-transform → browser update + }, + }; +} +``` + +#### What This Replaces + +| v4 (Current) | v5 (Vite) | +|--------------|-----------| +| `scripts/esbuild/*` | Vite builds Stencil packages | +| `src/dev-server/` | Vite dev server | +| `src/compiler/bundle/` (Rollup config) | Vite's Rollup | +| Custom HMR | Vite HMR | +| Custom file watcher | Vite watcher | + +#### What Stays (Moves Into Plugin) + +| Component | Notes | +|-----------|-------| +| `src/compiler/transformers/` | Core compilation - the "secret sauce" | +| `src/runtime/` | Bundled by Vite like any JS dependency | +| Build conditionals logic | Runs in `buildStart`, configures Rollup | +| `src/compiler/sys/in-memory-fs.ts` | Used for user project compilation (until Goal 3 complete) | + +### What Gets Replaced Eventually (Goal 3) + +When we wire Vite behind `stencil.config.ts` for user projects: + +| Component | Replaced By | +|-----------|-------------| +| In-memory FS | Vite's transform pipeline | +| Custom Rollup plugins | Vite plugin hooks | +| File caching | Vite's built-in caching | +| Rollup bundling | Vite's Rollup integration | + +**Key insight:** In-memory FS is for compiling USER projects, not building Stencil itself. +- Level 1 (current): Keep it - still needed for user project compilation +- Goal 3 (later): Remove it - Vite handles file transforms for user projects + +#### Key Questions to Prototype + +| Question | How to Validate | +|----------|-----------------| +| Do transformers work in Vite transform? | Compile a component, verify output | +| Does runtime bundle correctly? | Import @stencil/core, check it's included | +| Can buildStart analyze all components? | Scan project, build feature flags | +| Does lazy loading work with Vite chunks? | Build, verify loader + chunks work | + +--- + +## Goal 4: Mono-repo Restructure + +### Current State + +Stencil is a single repo with everything bundled together. Several related packages live in separate repositories: +- Output targets (external repos) +- `@stencil/vitest` (external) +- Framework integrations (external) + +### Proposed Structure + +``` +stencil/ +├── packages/ +│ ├── core/ # Main Stencil compiler & runtime +│ ├── mock-doc/ # DOM mocking (extracted from src/) +│ ├── cli/ # CLI package +│ ├── vite-plugin/ # Vite integration +│ │ +│ ├── output-angular/ # Angular output target (BRING IN) +│ ├── output-react/ # React output target (BRING IN) +│ ├── output-vue/ # Vue output target (BRING IN) +│ │ +│ ├── vitest/ # Vitest integration (BRING IN) +│ └── ... +├── nx.json # Or turborepo.json, pnpm workspaces, etc. +└── package.json +``` + +### Benefits + +1. **Self-contained packages**: mock-doc, CLI, etc. can be versioned independently +2. **Consolidated ecosystem**: Output targets in one place +3. **Easier contributions**: Clear boundaries between packages +4. **Shared tooling**: Common build, lint, test configuration +5. **Atomic changes**: Cross-package changes in single PR + +### Candidates to Extract + +| Current Location | New Package | Notes | +|------------------|-------------|-------| +| `src/mock-doc/` | `@stencil/mock-doc` | Used for SSR, relatively self-contained | +| `src/cli/` | `@stencil/cli` | Could be separate entry point | +| New | `@stencil/vite-plugin` | Core of v5 architecture | + +### Candidates to Bring In + +| External Repo | New Package | Notes | +|---------------|-------------|-------| +| stencil-ds-output-targets | `@stencil/output-*` | Angular, React, Vue wrappers | +| stencil-test-utils | `@stencil/vitest` | Unit/spec testing | +| stencil-playwright | `@stencil/playwright` | E2E testing | + +### Tooling Options + +| Tool | Pros | Cons | +|------|------|------| +| **Nx** | Powerful, great caching, good DX | Learning curve, config overhead | +| **Turborepo** | Simple, fast, Vercel backed | Less features than Nx | +| **pnpm workspaces** | Minimal, just workspaces | Manual orchestration | +| **Lerna** | Established, simple | Maintenance concerns | + +### Open Questions + +1. **Which mono-repo tool?** Nx vs Turborepo vs pnpm workspaces +2. **What to extract first?** mock-doc seems safest +3. **What to bring in first?** Output targets or vitest? +4. **Versioning strategy?** Independent vs synchronized + +--- + +## Unknown Unknowns (To Investigate) + +### Testing +- [x] Full audit of `@stencil/vitest` capabilities ✅ +- [x] Full audit of `@stencil/playwright` capabilities ✅ +- [ ] Internal test migration scope (how many tests use inline compilation?) + +### Legacy Removal +- [ ] Browser support floor decision (ES2020? ES2022?) +- [ ] Node.js version floor (18? 20?) +- [ ] Survey of CJS usage in user projects +- [ ] In-browser compilation use cases + +### Vite Migration +- [ ] Proof of concept: Stencil component in Vite +- [ ] Transformer compatibility assessment +- [ ] Build conditional preservation strategy +- [ ] Output target compatibility +- [ ] Can Vite build Stencil itself? (replace scripts/esbuild entirely) + +### Mono-repo +- [ ] Evaluate Nx vs Turborepo vs alternatives +- [ ] Identify all external repos to consolidate +- [ ] Plan migration order + +### General +- [ ] Breaking change inventory +- [ ] Migration guide outline +- [ ] Deprecation timeline for v4.x + +--- + +## Migration Strategy + +### For Users + +``` +v4.x (Current) + ├── Deprecation warnings for removed features + ├── Documentation of migration paths + └── Optional Vite mode (if feasible) + +v5.0 + ├── Breaking changes with clear migration guide + ├── Codemod tooling where possible + └── New defaults with escape hatches +``` + +### For Stencil Internal + +``` +Phase 1: Research & Prototyping + ├── Audit current architecture ✓ + ├── Prototype Vite integration + ├── Evaluate mono-repo tools + └── Document all breaking changes + +Phase 2: Infrastructure + ├── Set up mono-repo structure + ├── Migrate internal tests to Vitest + ├── Replace scripts/esbuild with Vite + └── Extract mock-doc to own package + +Phase 3: Consolidation + ├── Bring in external output targets + ├── Bring in @stencil/vitest + └── Remove legacy code paths + +Phase 4: Beta + ├── Public beta for early adopters + ├── Gather feedback + └── Iterate on migration tooling + +Phase 5: Release + ├── v5.0 stable release + └── LTS support for v4.x +``` + +--- + +## Files Inventory + +### To Remove + +| Path | Reason | +|------|--------| +| `src/testing/` | Jest/Puppeteer integration | +| `src/client/polyfills/` | Legacy browser polyfills | +| `src/dev-server/` | Custom dev server (replaced by Vite) | +| `scripts/esbuild/` | Entire internal build infrastructure (replaced by Vite) | + +### To Extract (Mono-repo) + +| Path | New Package | +|------|-------------| +| `src/mock-doc/` | `@stencil/mock-doc` | +| `src/cli/` | `@stencil/cli` (maybe) | + +### To Bring In (Mono-repo) + +| External | New Location | +|----------|--------------| +| stencil-ds-output-targets | `packages/output-*` | +| stencil-test-utils | `packages/vitest` | +| stencil-playwright | `packages/playwright` | + +### To Modify Heavily + +| Path | Changes | +|------|---------| +| `src/compiler/bundle/` | Replace Rollup with Vite | +| `src/compiler/output-targets/` | Adapt for Vite builds | +| `package.json` | Mono-repo root | +| `scripts/build.ts` | Complete rewrite for Vite | + +--- + +## Next Actions + +1. [x] **Audit `@stencil/vitest`** - ✅ Complete replacement for unit testing +2. [x] **Audit `@stencil/playwright`** - ✅ Complete replacement for E2E testing +3. [x] **Prototype Vite plugin** - ✅ Works! See `vite-prototype/` +4. [ ] **Level 1: Build Stencil with Vite** - Replace scripts/esbuild with Vite (pure ESM) + - [ ] **Modernize alias system** - Current aliases are opaque (hard to trace source → output) +5. [ ] **Wire Vite behind stencil.config.ts** - User-facing API stays same +6. [ ] **Evaluate mono-repo tools** - Nx vs Turborepo demo +7. [ ] **Survey internal tests** - Scope of migration +8. [ ] **Decide browser floor** - ES2020 vs ES2022 +9. [ ] **Decide Node.js floor** - 18 vs 20 LTS + +--- + +## Session Log + +### Session 1 (2026-02-07) +- Created initial planning document +- Analyzed current architecture +- Identified major areas of work +- Documented known unknowns +- Clarified: mock-doc stays (needed for SSR) +- Clarified: Goal is to remove ALL of scripts/esbuild with Vite +- Clarified: Inline testing is internal-only, not user-facing +- Added: Goal 4 - Mono-repo restructure (Nx or similar) + - Extract self-contained pieces (mock-doc) + - Bring in external output targets +- **Audited `@stencil/vitest`** (local: `/Users/John.Jenkins/projects/stencil-test-utils`) + - Provides `render()` API replacing `newSpecPage()` + - Supports 3 DOM environments: mock-doc, jsdom, happy-dom + - 19+ custom matchers, event spying, snapshot testing + - `stencil-test` CLI orchestrates build + test +- **Audited `@stencil/playwright`** (local: `/Users/John.Jenkins/projects/stencil-playwright`) + - Provides E2E testing replacing `newE2EPage()` + Puppeteer + - Multi-browser support (Chrome, Firefox, WebKit) + - `createConfig()` reads stencil.config automatically + - 5 event matchers, `spyOnEvent()` on page and locators +- **Conclusion**: Testing replacement story is complete for users + - `@stencil/vitest` → unit/spec tests + - `@stencil/playwright` → E2E tests + - Migration complexity: LOW +- **Explored Vite architecture** + - Analyzed Stencil's TypeScript transformer pipeline (two-phase: decorators→static, static→meta) + - Researched Vite plugin API (transform hook, buildStart, generateBundle) + - Clarified: Stencil = WC Compiler + Runtime (not "just a compiler") + - Runtime bundling is straightforward (Vite/Rollup handles it like any JS) + - **Two modes identified:** + - Dev: Per-file transform, full runtime, simple + - Build: Whole-project analysis first, then optimized output + - Key complexity: build conditionals (50+ flags) require analyzing ALL components before build +- **Built working Vite prototype** (`vite-prototype/`) + - Plugin uses `transpile()` in Vite's transform hook + - Component compiles and loads in browser ✅ + - Runtime bundles correctly via aliases + - Proved: Vite can replace dev server + bundling + - Note: Current compiler is CJS, needs `createRequire` workaround +- **Key clarification: User experience stays the same** + - `stencil.config.ts` still exists (not replaced by vite.config) + - Output targets work the same + - Users don't see Vite - it's internal implementation + - `stencil build` wraps Vite internally + +**Next session: Level 1 (build Stencil itself with Vite) or wire prototype behind stencil.config API** + +### Session 2 (2026-02-08) + +**Level 1: Build Stencil with Vite - STARTED** + +**Current Build Infrastructure Audit:** + +Stencil currently builds 8 packages using esbuild: +1. **compiler** - Main compiler bundle (`scripts/esbuild/compiler.ts`) ✅ KEEP +2. **cli** - CLI entry point (`scripts/esbuild/cli.ts`) ✅ KEEP +3. **dev-server** - Development server (`scripts/esbuild/dev-server.ts`) ❌ REMOVE (Vite IS the dev server) +4. **internal** - Runtime bundles and internal APIs (`scripts/esbuild/internal.ts`) ✅ KEEP +5. **mock-doc** - DOM mocking library (`scripts/esbuild/mock-doc.ts`) ✅ KEEP (extract to mono-repo later) +6. **screenshot** - Screenshot testing utilities (`scripts/esbuild/screenshot.ts`) ❌ REMOVE +7. **sys-node** - Node.js system APIs (`scripts/esbuild/sys-node.ts`) ✅ KEEP +8. **testing** - Jest/Puppeteer integration (`scripts/esbuild/testing.ts`) ❌ REMOVE (replaced by @stencil/vitest) + +**v5 Packages (5 total):** +1. compiler +2. cli +3. internal +4. mock-doc +5. sys-node + +**The Alias Problem:** + +Current aliases in `tsconfig.json` map from source → build output: +```typescript +"@stencil/core/compiler": ["src/compiler/index.ts"] // source +// But at runtime resolved to: +"@stencil/core/compiler": "../compiler/stencil.js" // build output +``` + +This is "opaque" because: +- TypeScript sees source paths +- esbuild sees build outputs +- Hard to trace which source file produces which output +- Requires `getEsbuildAliases()` to manually map them + +**The Modern Approach (Vite + ESM):** + +Use **source-based aliases** that work in both TypeScript and at runtime: +```typescript +// tsconfig.json AND vite.config.ts use same aliases +"@stencil/core/compiler": "src/compiler/index.ts" +``` + +No translation needed! TypeScript and Vite both resolve to source. + +**Level 1 Strategy:** + +``` +Phase 1: Create Vite build configs (CURRENT) + ├── vite.config.compiler.ts - Build compiler package + ├── vite.config.cli.ts - Build CLI package (✅ CREATED) + ├── vite.config.internal.ts - Build runtime bundles + ├── vite.config.mock-doc.ts - Build mock-doc (✅ CREATED) + └── vite.config.sys-node.ts - Build sys-node + +Phase 2: Modernize tsconfig aliases + ├── Update paths to point to source (not build/) + ├── Remove getEsbuildAliases() translation layer + └── Use Vite's resolve.alias for build-time resolution + +Phase 3: Pure ESM + ├── Remove CommonJS from internal builds + ├── Update Node.js floor to 18 LTS (ESM support) + └── Keep CJS as output option for users (compatibility) + +Phase 4: Replace scripts/build.ts + ├── Remove esbuild orchestration + ├── Use Vite's build API programmatically + └── Parallel builds with Promise.all() +``` + +**Key Decisions:** + +1. **Node.js floor: 18 LTS** (first version with stable ESM) +2. **Remove testing, dev-server, screenshot packages in v5** + - testing → replaced by @stencil/vitest + @stencil/playwright + - dev-server → Vite IS the dev server + - screenshot → removed completely +3. **Each package = one Vite config** (clean separation) +4. **5 packages total:** compiler, cli, internal, mock-doc, sys-node +5. **Don't bundle TypeScript/terser/parse5** - Use as normal dependencies + - Saves ~9MB from compiler bundle + - Simplifies build massively + - If TypeScript patching needed, use wrapper functions not source modification + +**Next Actions:** +- [x] Create vite.config.cli.ts ✅ +- [x] Create vite.config.mock-doc.ts ✅ +- [x] Create vite.config.sys-node.ts ✅ +- [x] Create vite.config.internal.ts ✅ +- [x] Create vite.config.internal-client.ts ✅ +- [x] Create vite.config.internal-app-data.ts ✅ +- [x] Create vite.config.internal-app-globals.ts ✅ +- [x] Create vite.config.compiler.ts ✅ +- [x] Create build-vite.ts orchestrator ✅ +- [x] Create vite.config.internal-hydrate.ts ✅ +- [ ] Handle post-build tasks (copy .d.ts files, TypeScript lib files) +- [ ] Update tsconfig.json paths to source-based +- [ ] Test build: `tsc && tsx build-vite.ts` +- [ ] Wire Vite behind user-facing stencil.config.ts API + +**Status: All configs complete! ✨** +- 9 Vite configs created: + - 5 main: cli, mock-doc, sys-node, internal, compiler + - 4 internal sub-bundles: client, hydrate, app-data, app-globals +- Build orchestrator ready (build-vite.ts) +- Next: Post-build tasks, then test run + +**Important distinctions:** +- **Level 1** = Build Stencil itself with Vite (what we're doing now) +- **Goal 3** = Use Vite to compile user projects (later - replaces in-memory FS) + +### Session 3 (Continuation - 2026-02-08) + +**Level 1 - Major progress!** + +✅ **Completed:** +- Created 9 Vite configuration files +- Converted all to pure ESM +- Moved CLI output to bin/ (more modern) +- **Restructured to mono-repo!** 🎉 + - Created `packages/` directory with pnpm workspaces + - **Simplified to 3 standalone packages** (clean composability!) + +**Final mono-repo structure:** +``` +packages/ +├── core/ @stencil/core (compiler + runtime bundles) +│ ├── vite.config.ts (main compiler) +│ └── vite-configs/ +│ ├── internal.config.ts (type definitions) +│ ├── client.config.ts (browser runtime) +│ ├── server.config.ts (SSR runtime - renamed from hydrate!) +│ ├── app-data.config.ts (build conditionals) +│ └── app-globals.config.ts (global state) +├── cli/ @stencil/cli +└── mock-doc/ @stencil/mock-doc +``` + +**Package scope:** +- `@stencil/core` - Compiler + all runtime bundles (internal/*) +- `@stencil/cli` - CLI experience (commands, scaffolding) +- `@stencil/mock-doc` - Independent DOM implementation for SSR/testing + +**Why this is cleaner:** +- Each package is independently installable/usable +- `sys-node` removed (functionality absorbed into core) +- `internal-*` not separate packages - they're build artifacts OF core +- `hydrate` → `server` (clearer naming!) +- Output targets stay in core for now (can extract later when plugin API stabilizes) + +🔑 **Key architectural decisions:** + +1. **Don't bundle TypeScript/terser/parse5** + - Use as normal dependencies instead of bundling source + - Saves ~9MB from compiler bundle + - Eliminates complex source patching process + +2. **Runtime bundles live in @stencil/core** + - `internal/*` are build artifacts, not standalone packages + - Core package has multiple vite configs for different outputs + - Cleaner than pretending they're independent packages + +3. **In-memory FS stays for now** + - Used for compiling USER projects (not building Stencil itself) + - Will be replaced in Goal 3 when we wire Vite behind stencil.config.ts + +4. **Packages removed in v5** + - dev-server (Vite IS the dev server) + - screenshot (removed completely) + - testing (replaced by @stencil/vitest + @stencil/playwright) + - sys-node (absorbed into core) + +5. **Pure ESM everywhere** ✅ + - Node 18+ supports ESM natively + - All Vite configs output ESM only (no CJS) + +6. **hydrate → server** (breaking change) + - `internal/hydrate` → `internal/server` + - Clearer: it's for SSR/hydration/prerendering (all server-side) + +📋 **Status:** +- [x] Installed vite 7.3.1 as dev dependency +- [x] Installed pnpm and set up workspaces +- [x] **Main compiler builds successfully!** (1.2MB output) + - packages/mock-doc/dist/index.js (339KB) + - packages/core/dist/index.js (1.2MB) +- [ ] Runtime bundles (internal/*, internal/client, internal/server, etc.) + - Blocked on circular dependency resolution (@platform/@runtime/@app-data) + - These need different build strategy or splitting + +📋 **Next steps:** +- [ ] Resolve runtime bundle circular dependencies +- [ ] Complete all package builds (cli, runtime bundles) +- [ ] Add post-build tasks (copy .d.ts, generate types) +- [ ] Update tsconfig.json paths (build/ → src/) +- [ ] Update root package.json exports for new structure + +**Build structure:** +``` +vite.config.cli.ts → bin/ +vite.config.mock-doc.ts → mock-doc/ +vite.config.sys-node.ts → sys/node/ +vite.config.compiler.ts → compiler/ +vite.config.internal.ts → internal/ +vite.config.internal-client.ts → internal/client/ +vite.config.internal-hydrate.ts → internal/hydrate/ +vite.config.internal-app-data.ts → internal/app-data/ +vite.config.internal-app-globals.ts → internal/app-globals/ +build-vite.ts (orchestrator) +``` + +**All configs use pure ESM** ✅ + +### Session 4 (Continuation - 2026-02-08) + +**Pivoting approach: Move code, eliminate aliases** + +While debugging the circular dependency issue (@platform/@runtime/@app-data), we realized: +1. Building from `../../../src/` is awkward and error-prone +2. Aliases like `@platform` are **opaque** - hard to trace imports +3. A proper mono-repo should have code IN the packages, not pointing elsewhere + +🔑 **New principle: Simplicity is king. No opaque magic.** + +**The problem with aliases:** +```typescript +// Current - opaque +import { plt } from '@platform'; // Where does this go? Who knows! + +// Better - explicit package import +import { plt } from '@stencil/core/client'; // Clear! +``` + +**New approach:** +1. **Move source code INTO packages** (not just build configs) +2. **Eliminate internal aliases** (`@platform`, `@runtime`, `@utils`, etc.) +3. **Use real package imports** - pnpm workspaces handles resolution +4. **TypeScript paths only mirror package exports** (for IDE support) + +**Migration order:** +1. `@stencil/mock-doc` - Simplest, no internal deps +2. `@stencil/core` - The big one (compiler + runtime) +3. `@stencil/cli` - Depends on core + +**Target structure:** +``` +packages/ +├── mock-doc/ +│ ├── src/ ← MOVE src/mock-doc/* here +│ │ ├── index.ts +│ │ ├── document.ts +│ │ └── ... +│ ├── dist/ ← Build output +│ ├── package.json +│ └── vite.config.ts +├── core/ +│ ├── src/ +│ │ ├── compiler/ ← MOVE src/compiler/* +│ │ ├── runtime/ ← MOVE src/runtime/* +│ │ ├── client/ ← MOVE src/client/* +│ │ ├── declarations/ ← MOVE src/declarations/* +│ │ └── ... +│ ├── dist/ +│ │ ├── compiler/ +│ │ ├── internal/ +│ │ │ ├── client/ +│ │ │ ├── server/ +│ │ │ ├── app-data/ +│ │ │ └── app-globals/ +│ │ └── ... +│ ├── package.json +│ └── vite.config.ts +└── cli/ + ├── src/ ← MOVE src/cli/* + ├── dist/ + ├── package.json + └── vite.config.ts +``` + +**Package exports (example for @stencil/core):** +```json +{ + "name": "@stencil/core", + "exports": { + ".": "./dist/index.js", + "./compiler": "./dist/compiler/index.js", + "./internal": "./dist/internal/index.js", + "./internal/client": "./dist/internal/client/index.js", + "./internal/server": "./dist/internal/server/index.js", + "./internal/app-data": "./dist/internal/app-data/index.js", + "./internal/app-globals": "./dist/internal/app-globals/index.js" + } +} +``` + +**Import rewriting needed:** +| Old (alias) | New (package import) | +|-------------|---------------------| +| `@platform` | `@stencil/core/internal/client` | +| `@runtime` | Relative imports within core | +| `@app-data` | `@stencil/core/internal/app-data` | +| `@app-globals` | `@stencil/core/internal/app-globals` | +| `@utils` | Relative imports within core | +| `@stencil/core/mock-doc` | `@stencil/mock-doc` | + +**Benefits:** +- No hidden mappings - imports are what they say +- IDE "Go to Definition" works naturally +- Easier onboarding for new contributors +- Clear package boundaries +- Proper mono-repo semantics + +📋 **Progress:** +- [x] Move mock-doc source to packages/mock-doc/src/ ✅ +- [x] Update mock-doc imports (remove any aliases) ✅ +- [x] Build and test mock-doc standalone ✅ +- [x] Move core source to packages/core/src/ ✅ +- [x] Build and test core ✅ +- [x] Move cli source to packages/cli/src/ ✅ +- [x] Build and test cli ✅ +- [ ] Rewrite alias imports to package imports (deferred - aliases still needed for build) +- [ ] Delete old src/ directories (or keep as reference during transition) +- [ ] Update root tsconfig.json +- [ ] Update package.json exports + +### Session 5 (Continuation - 2026-02-08) + +**Major milestone: All packages build with Vite!** 🎉 + +**Build output:** +``` +mock-doc: 337.62 kB (53 modules) +core: 883.77 kB (336 modules) + runtime bundles +cli: 56.05 kB (100 modules) +Total: 4.28s +``` + +**Runtime bundles (packages/core/dist/internal/):** +``` +index.js 97.95 kB (45 modules) - type exports +client/ 103.27 kB (54 modules) - browser runtime +server/ 185.55 kB (67 modules) - SSR/hydration +app-data/ 2.25 kB - build conditionals +app-globals/ 0.13 kB - global state +``` + +**What we did:** + +1. **Moved source into packages/** (not just configs) + - packages/mock-doc/src/ ← src/mock-doc/ + - packages/core/src/ ← src/compiler/, runtime/, client/, declarations/, utils/, etc. + - packages/cli/src/ ← src/cli/ + +2. **Fixed cross-package dependencies:** + - mock-doc: Copied hydration constants locally (small duplication, could revisit for shared package) + - core: Copied config-flags.ts, version.ts, sys/node/ into core (shared between CLI/compiler) + - cli: Uses aliases to reference core's utils/declarations + +3. **Aliases still needed (for now):** + - `@platform`, `@runtime`, `@utils`, `@app-data`, `@app-globals` + - These are used heavily in the codebase (~100+ occurrences) + - TODO: Gradually replace with relative imports for transparency + +**Key learnings:** + +1. **Circular dependency pattern works** - `@platform` → `src/client` → `@runtime` resolved correctly +2. **Type exports need `export type`** - Rollup can't find interface exports otherwise +3. **SSR builds need `ssr: true`** - Otherwise Vite tries to externalize Node builtins + +**Additional cleanup completed:** + +4. **Renamed hydrate → server throughout:** + - `vite.hydrate.config.ts` → `vite.server.config.ts` + - `internal/hydrate/` → `internal/server/` + - Updated imports from `../../hydrate/runner/` → `../../server/runner/` + - Clearer naming: SSR/hydration/prerendering are all "server-side" operations + +5. **Flattened Vite configs:** + - Moved from `packages/core/vite-configs/*.config.ts` to `packages/core/vite.*.config.ts` + - Each config at package root for easier discovery: + ``` + packages/core/ + ├── vite.config.ts (main compiler) + ├── vite.internal.config.ts (type exports) + ├── vite.client.config.ts (browser runtime) + ├── vite.server.config.ts (SSR runtime) + ├── vite.app-data.config.ts + └── vite.app-globals.config.ts + ``` + +6. **Analyzed sys/node for future removal:** + - **What it does:** Wraps Node.js APIs (fs, path, os, child_process) in a `CompilerSystem` interface + - **Why it exists:** Originally for cross-platform support (browser, Node, Deno, Workers) + - **v5 decision:** Keep for now (deep integration), but target removal in v5 + - **How to remove:** Replace `sys.readFile()` with `fs.promises.readFile()`, etc. + - This is separate from in-memory-fs (see below) + +7. **Clarified in-memory FS vs sys/node:** + - **sys/node**: Node.js adapter for CompilerSystem interface → remove in v5 by using Node APIs directly + - **in-memory-fs**: Virtual filesystem for caching during USER project compilation → replaced by Vite's transform pipeline when we wire Vite behind stencil.config.ts (Goal 3) + - These are different concerns: sys/node wraps Node APIs, in-memory-fs is a caching layer + +**Next steps:** +- [ ] Post-build tasks (copy .d.ts files) +- [ ] Set up package.json exports for each package +- [ ] Gradually replace aliases with relative imports +- [ ] Test the built packages actually work! +- [ ] Remove sys/node abstraction layer (v5 target) +- [ ] Wire Vite behind stencil.config.ts (Goal 3 - replaces in-memory-fs) + +--- + +*Last updated: 2026-02-08* diff --git a/build-vite.ts b/build-vite.ts new file mode 100644 index 00000000000..379a569c34c --- /dev/null +++ b/build-vite.ts @@ -0,0 +1,113 @@ +/** + * Modern Vite-based build orchestrator for Stencil v5 (mono-repo) + * + * Replaces: scripts/esbuild/* (entire directory) + * + * Usage: + * tsx build-vite.ts [--prod] [--watch] + * + * Or with package.json script: + * npm run build:vite + */ + +import { build as viteBuild, type InlineConfig } from 'vite'; +import { resolve } from 'path'; +import fs from 'fs-extra'; + +const ROOT_DIR = process.cwd(); +const PACKAGES_DIR = resolve(ROOT_DIR, 'packages'); +const SRC_DIR = resolve(ROOT_DIR, 'src'); + +interface PackageBuildConfig { + name: string; + /** Package directory name in packages/ */ + packageDir: string; + /** Additional vite config files to build (relative to package dir) */ + additionalConfigs?: string[]; +} + +/** + * All packages in the mono-repo (build order matters for dependencies) + */ +const PACKAGES: PackageBuildConfig[] = [ + { name: 'mock-doc', packageDir: 'mock-doc' }, + { + name: 'core', + packageDir: 'core', + additionalConfigs: [ + 'vite.internal.config.ts', + 'vite.client.config.ts', + 'vite.server.config.ts', + 'vite.app-data.config.ts', + 'vite.app-globals.config.ts', + ], + }, + { name: 'cli', packageDir: 'cli' }, +]; + +async function buildPackage(pkg: PackageBuildConfig, options: { watch?: boolean; mode?: string }) { + console.log(`\n📦 Building ${pkg.name}...`); + + const packagePath = resolve(PACKAGES_DIR, pkg.packageDir); + + // Build main config + const mainConfig: InlineConfig = { + configFile: resolve(packagePath, 'vite.config.ts'), + mode: options.mode || 'production', + logLevel: 'info', + root: packagePath, + }; + + if (options.watch) { + // TODO: Implement watch mode + console.warn('Watch mode not yet implemented'); + await viteBuild(mainConfig); + } else { + await viteBuild(mainConfig); + } + + // Build additional configs (e.g., core has multiple runtime bundles) + if (pkg.additionalConfigs) { + for (const configPath of pkg.additionalConfigs) { + console.log(` ↳ Building ${configPath}...`); + await viteBuild({ + configFile: resolve(packagePath, configPath), + mode: options.mode || 'production', + logLevel: 'info', + root: packagePath, + }); + } + } + + console.log(`✅ ${pkg.name} built successfully`); +} + +async function buildAll(options: { watch?: boolean; isProd?: boolean }) { + const mode = options.isProd ? 'production' : 'development'; + + console.log(`🚀 Building Stencil v5 mono-repo with Vite (${mode} mode)`); + console.log(` Packages dir: ${PACKAGES_DIR}`); + console.log(` Package count: ${PACKAGES.length}`); + + const startTime = Date.now(); + + try { + // Build packages in order (sequential for now, parallel later when deps resolved) + for (const pkg of PACKAGES) { + await buildPackage(pkg, { watch: options.watch, mode }); + } + + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + console.log(`\n✨ All packages built in ${duration}s`); + } catch (error) { + console.error('\n❌ Build failed:', error); + process.exit(1); + } +} + +// Parse CLI args +const args = process.argv.slice(2); +const isProd = args.includes('--prod'); +const isWatch = args.includes('--watch'); + +buildAll({ isProd, watch: isWatch }); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 473b4ae6252..00000000000 --- a/package-lock.json +++ /dev/null @@ -1,13891 +0,0 @@ -{ - "name": "@stencil/core", - "version": "4.42.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@stencil/core", - "version": "4.42.1", - "license": "MIT", - "bin": { - "stencil": "bin/stencil" - }, - "devDependencies": { - "@ionic/prettier-config": "^4.0.0", - "@jridgewell/source-map": "^0.3.6", - "@rollup/plugin-commonjs": "28.0.2", - "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-node-resolve": "16.0.0", - "@rollup/plugin-replace": "6.0.2", - "@rollup/pluginutils": "5.1.4", - "@types/eslint": "^8.4.6", - "@types/exit": "^0.1.31", - "@types/fs-extra": "^11.0.0", - "@types/graceful-fs": "^4.1.5", - "@types/jest": "^27.0.3", - "@types/listr": "^0.14.4", - "@types/node": "^24.6.2", - "@types/pixelmatch": "^5.2.4", - "@types/pngjs": "^6.0.1", - "@types/prompts": "^2.0.9", - "@types/semver": "^7.3.12", - "@types/ws": "^8.5.4", - "@types/yarnpkg__lockfile": "^1.1.5", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@yarnpkg/lockfile": "^1.1.0", - "ansi-colors": "4.1.3", - "autoprefixer": "10.4.19", - "conventional-changelog-cli": "^5.0.0", - "cspell": "^8.0.0", - "css-what": "^7.0.0", - "dts-bundle-generator": "~9.5.0", - "esbuild": "^0.25.0", - "esbuild-plugin-replace": "^1.4.0", - "eslint": "^8.23.1", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-jest": "^28.0.0", - "eslint-plugin-jsdoc": "^50.0.0", - "eslint-plugin-simple-import-sort": "^12.0.0", - "eslint-plugin-wdio": "^8.24.12", - "execa": "9.3.0", - "exit": "^0.1.2", - "fs-extra": "^11.0.0", - "glob": "10.5.0", - "graceful-fs": "~4.2.6", - "jest": "^27.4.5", - "jest-cli": "^27.4.5", - "jest-environment-node": "^27.4.4", - "jquery": "https://github.com/jquery/jquery/tarball/c98597eaf5e144ee5e549cb41984687cd1033068", - "listr": "^0.14.3", - "magic-string": "^0.30.0", - "merge-source-map": "^1.1.0", - "mime-db": "^1.46.0", - "minimatch": "9.0.4", - "node-fetch": "3.3.2", - "open": "^9.0.0", - "open-in-editor": "2.2.0", - "parse5": "7.2.1", - "pixelmatch": "5.3.0", - "postcss": "^8.2.8", - "postcss-safe-parser": "^7.0.1", - "postcss-selector-parser": "^7.1.0", - "prettier": "3.3.1", - "prompts": "2.4.2", - "puppeteer": "^24.1.0", - "rimraf": "^6.0.1", - "rollup": "4.34.9", - "semver": "^7.3.7", - "terser": "5.37.0", - "tsx": "^4.19.2", - "typescript": "~5.8.3", - "webpack": "^5.75.0", - "ws": "8.17.1" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=7.10.0" - }, - "optionalDependencies": { - "@rollup/rollup-darwin-arm64": "4.34.9", - "@rollup/rollup-darwin-x64": "4.34.9", - "@rollup/rollup-linux-arm64-gnu": "4.34.9", - "@rollup/rollup-linux-arm64-musl": "4.34.9", - "@rollup/rollup-linux-x64-gnu": "4.34.9", - "@rollup/rollup-linux-x64-musl": "4.34.9", - "@rollup/rollup-win32-arm64-msvc": "4.34.9", - "@rollup/rollup-win32-x64-msvc": "4.34.9" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@conventional-changelog/git-client": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-1.0.1.tgz", - "integrity": "sha512-PJEqBwAleffCMETaVm/fUgHldzBE35JFk3/9LL6NUA5EXa3qednu+UT6M7E5iBu3zIQZCULYIiZ90fBYHt6xUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/semver": "^7.5.5", - "semver": "^7.5.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.0.0" - }, - "peerDependenciesMeta": { - "conventional-commits-filter": { - "optional": true - }, - "conventional-commits-parser": { - "optional": true - } - } - }, - "node_modules/@cspell/cspell-bundled-dicts": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.19.4.tgz", - "integrity": "sha512-2ZRcZP/ncJ5q953o8i+R0fb8+14PDt5UefUNMrFZZHvfTI0jukAASOQeLY+WT6ASZv6CgbPrApAdbppy9FaXYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/dict-ada": "^4.1.0", - "@cspell/dict-al": "^1.1.0", - "@cspell/dict-aws": "^4.0.10", - "@cspell/dict-bash": "^4.2.0", - "@cspell/dict-companies": "^3.1.15", - "@cspell/dict-cpp": "^6.0.8", - "@cspell/dict-cryptocurrencies": "^5.0.4", - "@cspell/dict-csharp": "^4.0.6", - "@cspell/dict-css": "^4.0.17", - "@cspell/dict-dart": "^2.3.0", - "@cspell/dict-data-science": "^2.0.8", - "@cspell/dict-django": "^4.1.4", - "@cspell/dict-docker": "^1.1.13", - "@cspell/dict-dotnet": "^5.0.9", - "@cspell/dict-elixir": "^4.0.7", - "@cspell/dict-en_us": "^4.4.3", - "@cspell/dict-en-common-misspellings": "^2.0.10", - "@cspell/dict-en-gb": "1.1.33", - "@cspell/dict-filetypes": "^3.0.11", - "@cspell/dict-flutter": "^1.1.0", - "@cspell/dict-fonts": "^4.0.4", - "@cspell/dict-fsharp": "^1.1.0", - "@cspell/dict-fullstack": "^3.2.6", - "@cspell/dict-gaming-terms": "^1.1.1", - "@cspell/dict-git": "^3.0.4", - "@cspell/dict-golang": "^6.0.20", - "@cspell/dict-google": "^1.0.8", - "@cspell/dict-haskell": "^4.0.5", - "@cspell/dict-html": "^4.0.11", - "@cspell/dict-html-symbol-entities": "^4.0.3", - "@cspell/dict-java": "^5.0.11", - "@cspell/dict-julia": "^1.1.0", - "@cspell/dict-k8s": "^1.0.10", - "@cspell/dict-kotlin": "^1.1.0", - "@cspell/dict-latex": "^4.0.3", - "@cspell/dict-lorem-ipsum": "^4.0.4", - "@cspell/dict-lua": "^4.0.7", - "@cspell/dict-makefile": "^1.0.4", - "@cspell/dict-markdown": "^2.0.10", - "@cspell/dict-monkeyc": "^1.0.10", - "@cspell/dict-node": "^5.0.7", - "@cspell/dict-npm": "^5.2.1", - "@cspell/dict-php": "^4.0.14", - "@cspell/dict-powershell": "^5.0.14", - "@cspell/dict-public-licenses": "^2.0.13", - "@cspell/dict-python": "^4.2.17", - "@cspell/dict-r": "^2.1.0", - "@cspell/dict-ruby": "^5.0.8", - "@cspell/dict-rust": "^4.0.11", - "@cspell/dict-scala": "^5.0.7", - "@cspell/dict-shell": "^1.1.0", - "@cspell/dict-software-terms": "^5.0.5", - "@cspell/dict-sql": "^2.2.0", - "@cspell/dict-svelte": "^1.0.6", - "@cspell/dict-swift": "^2.0.5", - "@cspell/dict-terraform": "^1.1.1", - "@cspell/dict-typescript": "^3.2.1", - "@cspell/dict-vue": "^3.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@cspell/cspell-json-reporter": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.19.4.tgz", - "integrity": "sha512-pOlUtLUmuDdTIOhDTvWxxta0Wm8RCD/p1V0qUqeP6/Ups1ajBI4FWEpRFd7yMBTUHeGeSNicJX5XeX7wNbAbLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-types": "8.19.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@cspell/cspell-pipe": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.19.4.tgz", - "integrity": "sha512-GNAyk+7ZLEcL2fCMT5KKZprcdsq3L1eYy3e38/tIeXfbZS7Sd1R5FXUe6CHXphVWTItV39TvtLiDwN/2jBts9A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@cspell/cspell-resolver": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.19.4.tgz", - "integrity": "sha512-S8vJMYlsx0S1D60glX8H2Jbj4mD8519VjyY8lu3fnhjxfsl2bDFZvF3ZHKsLEhBE+Wh87uLqJDUJQiYmevHjDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-directory": "^4.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@cspell/cspell-service-bus": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.19.4.tgz", - "integrity": "sha512-uhY+v8z5JiUogizXW2Ft/gQf3eWrh5P9036jN2Dm0UiwEopG/PLshHcDjRDUiPdlihvA0RovrF0wDh4ptcrjuQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@cspell/cspell-types": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.19.4.tgz", - "integrity": "sha512-ekMWuNlFiVGfsKhfj4nmc8JCA+1ZltwJgxiKgDuwYtR09ie340RfXFF6YRd2VTW5zN7l4F1PfaAaPklVz6utSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@cspell/dict-ada": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-ada/-/dict-ada-4.1.1.tgz", - "integrity": "sha512-E+0YW9RhZod/9Qy2gxfNZiHJjCYFlCdI69br1eviQQWB8yOTJX0JHXLs79kOYhSW0kINPVUdvddEBe6Lu6CjGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-al": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-al/-/dict-al-1.1.1.tgz", - "integrity": "sha512-sD8GCaZetgQL4+MaJLXqbzWcRjfKVp8x+px3HuCaaiATAAtvjwUQ5/Iubiqwfd1boIh2Y1/3EgM3TLQ7Q8e0wQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-aws": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-aws/-/dict-aws-4.0.15.tgz", - "integrity": "sha512-aPY7VVR5Os4rz36EaqXBAEy14wR4Rqv+leCJ2Ug/Gd0IglJpM30LalF3e2eJChnjje3vWoEC0Rz3+e5gpZG+Kg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-bash": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-bash/-/dict-bash-4.2.1.tgz", - "integrity": "sha512-SBnzfAyEAZLI9KFS7DUG6Xc1vDFuLllY3jz0WHvmxe8/4xV3ufFE3fGxalTikc1VVeZgZmxYiABw4iGxVldYEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/dict-shell": "1.1.1" - } - }, - "node_modules/@cspell/dict-companies": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.2.5.tgz", - "integrity": "sha512-H51R0w7c6RwJJPqH7Gs65tzP6ouZsYDEHmmol6MIIk0kQaOIBuFP2B3vIxHLUr2EPRVFZsMW8Ni7NmVyaQlwsg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-cpp": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-6.0.12.tgz", - "integrity": "sha512-N4NsCTttVpMqQEYbf0VQwCj6np+pJESov0WieCN7R/0aByz4+MXEiDieWWisaiVi8LbKzs1mEj4ZTw5K/6O2UQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-cryptocurrencies": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-cryptocurrencies/-/dict-cryptocurrencies-5.0.5.tgz", - "integrity": "sha512-R68hYYF/rtlE6T/dsObStzN5QZw+0aQBinAXuWCVqwdS7YZo0X33vGMfChkHaiCo3Z2+bkegqHlqxZF4TD3rUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-csharp": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-csharp/-/dict-csharp-4.0.7.tgz", - "integrity": "sha512-H16Hpu8O/1/lgijFt2lOk4/nnldFtQ4t8QHbyqphqZZVE5aS4J/zD/WvduqnLY21aKhZS6jo/xF5PX9jyqPKUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-css": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz", - "integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-dart": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-dart/-/dict-dart-2.3.1.tgz", - "integrity": "sha512-xoiGnULEcWdodXI6EwVyqpZmpOoh8RA2Xk9BNdR7DLamV/QMvEYn8KJ7NlRiTSauJKPNkHHQ5EVHRM6sTS7jdg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-data-science": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-2.0.9.tgz", - "integrity": "sha512-wTOFMlxv06veIwKdXUwdGxrQcK44Zqs426m6JGgHIB/GqvieZQC5n0UI+tUm5OCxuNyo4OV6mylT4cRMjtKtWQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-django": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-django/-/dict-django-4.1.5.tgz", - "integrity": "sha512-AvTWu99doU3T8ifoMYOMLW2CXKvyKLukPh1auOPwFGHzueWYvBBN+OxF8wF7XwjTBMMeRleVdLh3aWCDEX/ZWg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-docker": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@cspell/dict-docker/-/dict-docker-1.1.16.tgz", - "integrity": "sha512-UiVQ5RmCg6j0qGIxrBnai3pIB+aYKL3zaJGvXk1O/ertTKJif9RZikKXCEgqhaCYMweM4fuLqWSVmw3hU164Iw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-dotnet": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/@cspell/dict-dotnet/-/dict-dotnet-5.0.10.tgz", - "integrity": "sha512-ooar8BP/RBNP1gzYfJPStKEmpWy4uv/7JCq6FOnJLeD1yyfG3d/LFMVMwiJo+XWz025cxtkM3wuaikBWzCqkmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-elixir": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-elixir/-/dict-elixir-4.0.8.tgz", - "integrity": "sha512-CyfphrbMyl4Ms55Vzuj+mNmd693HjBFr9hvU+B2YbFEZprE5AG+EXLYTMRWrXbpds4AuZcvN3deM2XVB80BN/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-en_us": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.4.19.tgz", - "integrity": "sha512-JYYgzhGqSGuIMNY1cTlmq3zrNpehrExMHqLmLnSM2jEGFeHydlL+KLBwBYxMy4e73w+p1+o/rmAiGsMj9g3MCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-en-common-misspellings": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.1.6.tgz", - "integrity": "sha512-xV9yryOqZizbSqxRS7kSVRrxVEyWHUqwdY56IuT7eAWGyTCJNmitXzXa4p+AnEbhL+AB2WLynGVSbNoUC3ceFA==", - "dev": true, - "license": "CC BY-SA 4.0" - }, - "node_modules/@cspell/dict-en-gb": { - "version": "1.1.33", - "resolved": "https://registry.npmjs.org/@cspell/dict-en-gb/-/dict-en-gb-1.1.33.tgz", - "integrity": "sha512-tKSSUf9BJEV+GJQAYGw5e+ouhEe2ZXE620S7BLKe3ZmpnjlNG9JqlnaBhkIMxKnNFkLY2BP/EARzw31AZnOv4g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-filetypes": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.13.tgz", - "integrity": "sha512-g6rnytIpQlMNKGJT1JKzWkC+b3xCliDKpQ3ANFSq++MnR4GaLiifaC4JkVON11Oh/UTplYOR1nY3BR4X30bswA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-flutter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-flutter/-/dict-flutter-1.1.1.tgz", - "integrity": "sha512-UlOzRcH2tNbFhZmHJN48Za/2/MEdRHl2BMkCWZBYs+30b91mWvBfzaN4IJQU7dUZtowKayVIF9FzvLZtZokc5A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-fonts": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-fonts/-/dict-fonts-4.0.5.tgz", - "integrity": "sha512-BbpkX10DUX/xzHs6lb7yzDf/LPjwYIBJHJlUXSBXDtK/1HaeS+Wqol4Mlm2+NAgZ7ikIE5DQMViTgBUY3ezNoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-fsharp": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-fsharp/-/dict-fsharp-1.1.1.tgz", - "integrity": "sha512-imhs0u87wEA4/cYjgzS0tAyaJpwG7vwtC8UyMFbwpmtw+/bgss+osNfyqhYRyS/ehVCWL17Ewx2UPkexjKyaBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-fullstack": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-3.2.7.tgz", - "integrity": "sha512-IxEk2YAwAJKYCUEgEeOg3QvTL4XLlyArJElFuMQevU1dPgHgzWElFevN5lsTFnvMFA1riYsVinqJJX0BanCFEg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-gaming-terms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-gaming-terms/-/dict-gaming-terms-1.1.2.tgz", - "integrity": "sha512-9XnOvaoTBscq0xuD6KTEIkk9hhdfBkkvJAIsvw3JMcnp1214OCGW8+kako5RqQ2vTZR3Tnf3pc57o7VgkM0q1Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-git": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-git/-/dict-git-3.0.7.tgz", - "integrity": "sha512-odOwVKgfxCQfiSb+nblQZc4ErXmnWEnv8XwkaI4sNJ7cNmojnvogYVeMqkXPjvfrgEcizEEA4URRD2Ms5PDk1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-golang": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/@cspell/dict-golang/-/dict-golang-6.0.23.tgz", - "integrity": "sha512-oXqUh/9dDwcmVlfUF5bn3fYFqbUzC46lXFQmi5emB0vYsyQXdNWsqi6/yH3uE7bdRE21nP7Yo0mR1jjFNyLamg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-google": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@cspell/dict-google/-/dict-google-1.0.9.tgz", - "integrity": "sha512-biL65POqialY0i4g6crj7pR6JnBkbsPovB2WDYkj3H4TuC/QXv7Pu5pdPxeUJA6TSCHI7T5twsO4VSVyRxD9CA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-haskell": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@cspell/dict-haskell/-/dict-haskell-4.0.6.tgz", - "integrity": "sha512-ib8SA5qgftExpYNjWhpYIgvDsZ/0wvKKxSP+kuSkkak520iPvTJumEpIE+qPcmJQo4NzdKMN8nEfaeci4OcFAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-html": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz", - "integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-html-symbol-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz", - "integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-java": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-java/-/dict-java-5.0.12.tgz", - "integrity": "sha512-qPSNhTcl7LGJ5Qp6VN71H8zqvRQK04S08T67knMq9hTA8U7G1sTKzLmBaDOFhq17vNX/+rT+rbRYp+B5Nwza1A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-julia": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-julia/-/dict-julia-1.1.1.tgz", - "integrity": "sha512-WylJR9TQ2cgwd5BWEOfdO3zvDB+L7kYFm0I9u0s9jKHWQ6yKmfKeMjU9oXxTBxIufhCXm92SKwwVNAC7gjv+yA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-k8s": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-k8s/-/dict-k8s-1.0.12.tgz", - "integrity": "sha512-2LcllTWgaTfYC7DmkMPOn9GsBWsA4DZdlun4po8s2ysTP7CPEnZc1ZfK6pZ2eI4TsZemlUQQ+NZxMe9/QutQxg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-kotlin": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-kotlin/-/dict-kotlin-1.1.1.tgz", - "integrity": "sha512-J3NzzfgmxRvEeOe3qUXnSJQCd38i/dpF9/t3quuWh6gXM+krsAXP75dY1CzDmS8mrJAlBdVBeAW5eAZTD8g86Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-latex": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-latex/-/dict-latex-4.0.4.tgz", - "integrity": "sha512-YdTQhnTINEEm/LZgTzr9Voz4mzdOXH7YX+bSFs3hnkUHCUUtX/mhKgf1CFvZ0YNM2afjhQcmLaR9bDQVyYBvpA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-lorem-ipsum": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-lorem-ipsum/-/dict-lorem-ipsum-4.0.5.tgz", - "integrity": "sha512-9a4TJYRcPWPBKkQAJ/whCu4uCAEgv/O2xAaZEI0n4y1/l18Yyx8pBKoIX5QuVXjjmKEkK7hi5SxyIsH7pFEK9Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-lua": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-lua/-/dict-lua-4.0.8.tgz", - "integrity": "sha512-N4PkgNDMu9JVsRu7JBS/3E/dvfItRgk9w5ga2dKq+JupP2Y3lojNaAVFhXISh4Y0a6qXDn2clA6nvnavQ/jjLA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-makefile": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-makefile/-/dict-makefile-1.0.5.tgz", - "integrity": "sha512-4vrVt7bGiK8Rx98tfRbYo42Xo2IstJkAF4tLLDMNQLkQ86msDlYSKG1ZCk8Abg+EdNcFAjNhXIiNO+w4KflGAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-markdown": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.12.tgz", - "integrity": "sha512-ufwoliPijAgWkD/ivAMC+A9QD895xKiJRF/fwwknQb7kt7NozTLKFAOBtXGPJAB4UjhGBpYEJVo2elQ0FCAH9A==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@cspell/dict-css": "^4.0.18", - "@cspell/dict-html": "^4.0.12", - "@cspell/dict-html-symbol-entities": "^4.0.4", - "@cspell/dict-typescript": "^3.2.3" - } - }, - "node_modules/@cspell/dict-monkeyc": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cspell/dict-monkeyc/-/dict-monkeyc-1.0.11.tgz", - "integrity": "sha512-7Q1Ncu0urALI6dPTrEbSTd//UK0qjRBeaxhnm8uY5fgYNFYAG+u4gtnTIo59S6Bw5P++4H3DiIDYoQdY/lha8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-node": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-node/-/dict-node-5.0.8.tgz", - "integrity": "sha512-AirZcN2i84ynev3p2/1NCPEhnNsHKMz9zciTngGoqpdItUb2bDt1nJBjwlsrFI78GZRph/VaqTVFwYikmncpXg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-npm": { - "version": "5.2.17", - "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.2.17.tgz", - "integrity": "sha512-0yp7lBXtN3CtxBrpvTu/yAuPdOHR2ucKzPxdppc3VKO068waZNpKikn1NZCzBS3dIAFGVITzUPtuTXxt9cxnSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-php": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-php/-/dict-php-4.0.15.tgz", - "integrity": "sha512-iepGB2gtToMWSTvybesn4/lUp4LwXcEm0s8vasJLP76WWVkq1zYjmeS+WAIzNgsuURyZ/9mGqhS0CWMuo74ODw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-powershell": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-powershell/-/dict-powershell-5.0.15.tgz", - "integrity": "sha512-l4S5PAcvCFcVDMJShrYD0X6Huv9dcsQPlsVsBGbH38wvuN7gS7+GxZFAjTNxDmTY1wrNi1cCatSg6Pu2BW4rgg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-public-licenses": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.15.tgz", - "integrity": "sha512-cJEOs901H13Pfy0fl4dCD1U+xpWIMaEPq8MeYU83FfDZvellAuSo4GqWCripfIqlhns/L6+UZEIJSOZnjgy7Wg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-python": { - "version": "4.2.19", - "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.2.19.tgz", - "integrity": "sha512-9S2gTlgILp1eb6OJcVZeC8/Od83N8EqBSg5WHVpx97eMMJhifOzePkE0kDYjyHMtAFznCQTUu0iQEJohNQ5B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/dict-data-science": "^2.0.9" - } - }, - "node_modules/@cspell/dict-r": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-r/-/dict-r-2.1.1.tgz", - "integrity": "sha512-71Ka+yKfG4ZHEMEmDxc6+blFkeTTvgKbKAbwiwQAuKl3zpqs1Y0vUtwW2N4b3LgmSPhV3ODVY0y4m5ofqDuKMw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-ruby": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.0.9.tgz", - "integrity": "sha512-H2vMcERMcANvQshAdrVx0XoWaNX8zmmiQN11dZZTQAZaNJ0xatdJoSqY8C8uhEMW89bfgpN+NQgGuDXW2vmXEw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-rust": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-rust/-/dict-rust-4.0.12.tgz", - "integrity": "sha512-z2QiH+q9UlNhobBJArvILRxV8Jz0pKIK7gqu4TgmEYyjiu1TvnGZ1tbYHeu9w3I/wOP6UMDoCBTty5AlYfW0mw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-scala": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-scala/-/dict-scala-5.0.8.tgz", - "integrity": "sha512-YdftVmumv8IZq9zu1gn2U7A4bfM2yj9Vaupydotyjuc+EEZZSqAafTpvW/jKLWji2TgybM1L2IhmV0s/Iv9BTw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-shell": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-shell/-/dict-shell-1.1.1.tgz", - "integrity": "sha512-T37oYxE7OV1x/1D4/13Y8JZGa1QgDCXV7AVt3HLXjn0Fe3TaNDvf5sU0fGnXKmBPqFFrHdpD3uutAQb1dlp15g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-software-terms": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-5.1.8.tgz", - "integrity": "sha512-iwCHLP11OmVHEX2MzE8EPxpPw7BelvldxWe5cJ3xXIDL8TjF2dBTs2noGcrqnZi15SLYIlO8897BIOa33WHHZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-sql": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-sql/-/dict-sql-2.2.1.tgz", - "integrity": "sha512-qDHF8MpAYCf4pWU8NKbnVGzkoxMNrFqBHyG/dgrlic5EQiKANCLELYtGlX5auIMDLmTf1inA0eNtv74tyRJ/vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-svelte": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-svelte/-/dict-svelte-1.0.7.tgz", - "integrity": "sha512-hGZsGqP0WdzKkdpeVLBivRuSNzOTvN036EBmpOwxH+FTY2DuUH7ecW+cSaMwOgmq5JFSdTcbTNFlNC8HN8lhaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-swift": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@cspell/dict-swift/-/dict-swift-2.0.6.tgz", - "integrity": "sha512-PnpNbrIbex2aqU1kMgwEKvCzgbkHtj3dlFLPMqW1vSniop7YxaDTtvTUO4zA++ugYAEL+UK8vYrBwDPTjjvSnA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-terraform": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@cspell/dict-terraform/-/dict-terraform-1.1.3.tgz", - "integrity": "sha512-gr6wxCydwSFyyBKhBA2xkENXtVFToheqYYGFvlMZXWjviynXmh+NK/JTvTCk/VHk3+lzbO9EEQKee6VjrAUSbA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-typescript": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", - "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-vue": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-vue/-/dict-vue-3.0.5.tgz", - "integrity": "sha512-Mqutb8jbM+kIcywuPQCCaK5qQHTdaByoEO2J9LKFy3sqAdiBogNkrplqUK0HyyRFgCfbJUgjz3N85iCMcWH0JA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dynamic-import": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.19.4.tgz", - "integrity": "sha512-0LLghC64+SiwQS20Sa0VfFUBPVia1rNyo0bYeIDoB34AA3qwguDBVJJkthkpmaP1R2JeR/VmxmJowuARc4ZUxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/url": "8.19.4", - "import-meta-resolve": "^4.1.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@cspell/filetypes": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-8.19.4.tgz", - "integrity": "sha512-D9hOCMyfKtKjjqQJB8F80PWsjCZhVGCGUMiDoQpcta0e+Zl8vHgzwaC0Ai4QUGBhwYEawHGiWUd7Y05u/WXiNQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@cspell/strong-weak-map": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.19.4.tgz", - "integrity": "sha512-MUfFaYD8YqVe32SQaYLI24/bNzaoyhdBIFY5pVrvMo1ZCvMl8AlfI2OcBXvcGb5aS5z7sCNCJm11UuoYbLI1zw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@cspell/url": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/@cspell/url/-/url-8.19.4.tgz", - "integrity": "sha512-Pa474iBxS+lxsAL4XkETPGIq3EgMLCEb9agj3hAd2VGMTCApaiUvamR4b+uGXIPybN70piFxvzrfoxsG2uIP6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@es-joy/jsdoccomment": { - "version": "0.50.2", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.50.2.tgz", - "integrity": "sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6", - "@typescript-eslint/types": "^8.11.0", - "comment-parser": "1.4.1", - "esquery": "^1.6.0", - "jsdoc-type-pratt-parser": "~4.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hutson/parse-repository-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-5.0.0.tgz", - "integrity": "sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@ionic/prettier-config": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@ionic/prettier-config/-/prettier-config-4.0.0.tgz", - "integrity": "sha512-0DqL6CggVdgeJAWOLPUT73rF1VD5p0tVlCpC5GXz5vTIUBxNwsJ5085Q7wXjKiE5Odx3aOHGTcuRWCawFsLFag==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": "^2.4.0 || ^3.0.0" - } - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", - "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", - "micromatch": "^4.0.4", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/core/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@jest/core/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", - "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/reporters/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", - "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@puppeteer/browsers": { - "version": "2.10.12", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.12.tgz", - "integrity": "sha512-mP9iLFZwH+FapKJLeA7/fLqOlSUwYpMwjR1P5J23qd4e7qGJwecJccJqHYrjw33jmIZYV4dtiTHPD/J+1e7cEw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.3", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.2.tgz", - "integrity": "sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", - "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", - "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", - "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", - "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", - "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", - "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", - "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", - "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", - "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", - "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", - "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", - "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", - "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", - "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", - "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", - "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", - "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", - "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", - "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@samverschueren/stream-to-observable": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", - "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-observable": "^0.3.0" - }, - "engines": { - "node": ">=6" - }, - "peerDependenciesMeta": { - "rxjs": { - "optional": true - }, - "zen-observable": { - "optional": true - } - } - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.12", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", - "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/exit": { - "version": "0.1.33", - "resolved": "https://registry.npmjs.org/@types/exit/-/exit-0.1.33.tgz", - "integrity": "sha512-1/NNW0tyaodminOWDq8snoPHGvf4f9srYKVwCpOukpTesAbJmYSzxa9l+10fHl1xTFOF+l9JXmyRReS+qhSN/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", - "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/jsonfile": "*", - "@types/node": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "27.5.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", - "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsonfile": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", - "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/listr": { - "version": "0.14.9", - "resolved": "https://registry.npmjs.org/@types/listr/-/listr-0.14.9.tgz", - "integrity": "sha512-Ncsy/jtO/HZYrupLGcnp1BOswZVsNvggjIjnf2EZ1xECfU4hxcQ3FWvFEyR+/DXssz0HDm74Op/tEsyrB3eV5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "rxjs": "^6.5.1" - } - }, - "node_modules/@types/node": { - "version": "24.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", - "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.14.0" - } - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/pixelmatch": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.6.tgz", - "integrity": "sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/pngjs": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz", - "integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prompts": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", - "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "kleur": "^3.0.3" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yarnpkg__lockfile": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.9.tgz", - "integrity": "sha512-GD4Fk15UoP5NLCNor51YdfL9MSdldKCqOC9EssrRw3HVfar9wUZ5y8Lfnp+qVD6hIinLr8ygklDYnmlnlQo12Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/add-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", - "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", - "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/are-docs-informative": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", - "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true, - "license": "MIT" - }, - "node_modules/array-timsort": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", - "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/b4a": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", - "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, - "node_modules/babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^27.5.1", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/bare-events": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz", - "integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.11.tgz", - "integrity": "sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", - "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", - "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.21.0" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.0.tgz", - "integrity": "sha512-c+RCqMSZbkz97Mw1LWR0gcOqwK82oyYKfLoHJ8k13ybi1+I80ffdDzUy0TdAburdrR/kI0/VuN8YgEnJqX+Nyw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "dev": true, - "license": "Unlicense", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "big-integer": "^1.6.44" - }, - "engines": { - "node": ">= 5.10.0" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bundle-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "run-applescript": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001768", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", - "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk-template": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.2.tgz", - "integrity": "sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.2.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/chalk/chalk-template?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/chromium-bidi": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-9.1.0.tgz", - "integrity": "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "mitt": "^3.0.1", - "zod": "^3.24.1" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/clap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", - "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.1.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/clap/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clap/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/clear-module": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/clear-module/-/clear-module-4.1.2.tgz", - "integrity": "sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^2.0.0", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha512-f4r4yJnbT++qUPI9NR4XLDLq41gQ+uqnPItWG0F5ZkehuNiTTa3EY0S4AqTSUOeJ7/zU41oWPQSNkW5BqPL9bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/comment-json": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", - "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-timsort": "^1.0.3", - "core-util-is": "^1.0.3", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/comment-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", - "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "license": "MIT" - }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/conventional-changelog": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-6.0.0.tgz", - "integrity": "sha512-tuUH8H/19VjtD9Ig7l6TQRh+Z0Yt0NZ6w/cCkkyzUbGQTnUEmKfGtkC9gGfVgCfOL1Rzno5NgNF4KY8vR+Jo3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "conventional-changelog-angular": "^8.0.0", - "conventional-changelog-atom": "^5.0.0", - "conventional-changelog-codemirror": "^5.0.0", - "conventional-changelog-conventionalcommits": "^8.0.0", - "conventional-changelog-core": "^8.0.0", - "conventional-changelog-ember": "^5.0.0", - "conventional-changelog-eslint": "^6.0.0", - "conventional-changelog-express": "^5.0.0", - "conventional-changelog-jquery": "^6.0.0", - "conventional-changelog-jshint": "^5.0.0", - "conventional-changelog-preset-loader": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-angular": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.0.0.tgz", - "integrity": "sha512-CLf+zr6St0wIxos4bmaKHRXWAcsCXrJU6F4VdNDrGRK3B8LDLKoX3zuMV5GhtbGkVR/LohZ6MT6im43vZLSjmA==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-atom": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-5.0.0.tgz", - "integrity": "sha512-WfzCaAvSCFPkznnLgLnfacRAzjgqjLUjvf3MftfsJzQdDICqkOOpcMtdJF3wTerxSpv2IAAjX8doM3Vozqle3g==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-cli": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-cli/-/conventional-changelog-cli-5.0.0.tgz", - "integrity": "sha512-9Y8fucJe18/6ef6ZlyIlT2YQUbczvoQZZuYmDLaGvcSBP+M6h+LAvf7ON7waRxKJemcCII8Yqu5/8HEfskTxJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "add-stream": "^1.0.0", - "conventional-changelog": "^6.0.0", - "meow": "^13.0.0", - "tempfile": "^5.0.0" - }, - "bin": { - "conventional-changelog": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-codemirror": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-5.0.0.tgz", - "integrity": "sha512-8gsBDI5Y3vrKUCxN6Ue8xr6occZ5nsDEc4C7jO/EovFGozx8uttCAyfhRrvoUAWi2WMm3OmYs+0mPJU7kQdYWQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-8.0.0.tgz", - "integrity": "sha512-eOvlTO6OcySPyyyk8pKz2dP4jjElYunj9hn9/s0OB+gapTO8zwS9UQWrZ1pmF2hFs3vw1xhonOLGcGjy/zgsuA==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-core": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-8.0.0.tgz", - "integrity": "sha512-EATUx5y9xewpEe10UEGNpbSHRC6cVZgO+hXQjofMqpy+gFIrcGvH3Fl6yk2VFKh7m+ffenup2N7SZJYpyD9evw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@hutson/parse-repository-url": "^5.0.0", - "add-stream": "^1.0.0", - "conventional-changelog-writer": "^8.0.0", - "conventional-commits-parser": "^6.0.0", - "git-raw-commits": "^5.0.0", - "git-semver-tags": "^8.0.0", - "hosted-git-info": "^7.0.0", - "normalize-package-data": "^6.0.0", - "read-package-up": "^11.0.0", - "read-pkg": "^9.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-ember": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-5.0.0.tgz", - "integrity": "sha512-RPflVfm5s4cSO33GH/Ey26oxhiC67akcxSKL8CLRT3kQX2W3dbE19sSOM56iFqUJYEwv9mD9r6k79weWe1urfg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-eslint": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-6.0.0.tgz", - "integrity": "sha512-eiUyULWjzq+ybPjXwU6NNRflApDWlPEQEHvI8UAItYW/h22RKkMnOAtfCZxMmrcMO1OKUWtcf2MxKYMWe9zJuw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-5.0.0.tgz", - "integrity": "sha512-D8Q6WctPkQpvr2HNCCmwU5GkX22BVHM0r4EW8vN0230TSyS/d6VQJDAxGb84lbg0dFjpO22MwmsikKL++Oo/oQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-jquery": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-6.0.0.tgz", - "integrity": "sha512-2kxmVakyehgyrho2ZHBi90v4AHswkGzHuTaoH40bmeNqUt20yEkDOSpw8HlPBfvEQBwGtbE+5HpRwzj6ac2UfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-jshint": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-5.0.0.tgz", - "integrity": "sha512-gGNphSb/opc76n2eWaO6ma4/Wqu3tpa2w7i9WYqI6Cs2fncDSI2/ihOfMvXveeTTeld0oFvwMVNV+IYQIk3F3g==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-preset-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-5.0.0.tgz", - "integrity": "sha512-SetDSntXLk8Jh1NOAl1Gu5uLiCNSYenB5tm0YVeZKePRIgDW9lQImromTwLa3c/Gae298tsgOM+/CYT9XAl0NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-writer": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.2.0.tgz", - "integrity": "sha512-Y2aW4596l9AEvFJRwFGJGiQjt2sBYTjPD18DdvxX9Vpz0Z7HQ+g1Z+6iYDAm1vR3QOJrDBkRHixHK/+FhkR6Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "conventional-commits-filter": "^5.0.0", - "handlebars": "^4.7.7", - "meow": "^13.0.0", - "semver": "^7.5.2" - }, - "bin": { - "conventional-changelog-writer": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-commits-filter": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-5.0.0.tgz", - "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-commits-parser": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.2.0.tgz", - "integrity": "sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag==", - "dev": true, - "license": "MIT", - "dependencies": { - "meow": "^13.0.0" - }, - "bin": { - "conventional-commits-parser": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig/node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cspell": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.19.4.tgz", - "integrity": "sha512-toaLrLj3usWY0Bvdi661zMmpKW2DVLAG3tcwkAv4JBTisdIRn15kN/qZDrhSieUEhVgJgZJDH4UKRiq29mIFxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-json-reporter": "8.19.4", - "@cspell/cspell-pipe": "8.19.4", - "@cspell/cspell-types": "8.19.4", - "@cspell/dynamic-import": "8.19.4", - "@cspell/url": "8.19.4", - "chalk": "^5.4.1", - "chalk-template": "^1.1.0", - "commander": "^13.1.0", - "cspell-dictionary": "8.19.4", - "cspell-gitignore": "8.19.4", - "cspell-glob": "8.19.4", - "cspell-io": "8.19.4", - "cspell-lib": "8.19.4", - "fast-json-stable-stringify": "^2.1.0", - "file-entry-cache": "^9.1.0", - "semver": "^7.7.1", - "tinyglobby": "^0.2.13" - }, - "bin": { - "cspell": "bin.mjs", - "cspell-esm": "bin.mjs" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/streetsidesoftware/cspell?sponsor=1" - } - }, - "node_modules/cspell-config-lib": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.19.4.tgz", - "integrity": "sha512-LtFNZEWVrnpjiTNgEDsVN05UqhhJ1iA0HnTv4jsascPehlaUYVoyucgNbFeRs6UMaClJnqR0qT9lnPX+KO1OLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-types": "8.19.4", - "comment-json": "^4.2.5", - "yaml": "^2.7.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cspell-dictionary": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.19.4.tgz", - "integrity": "sha512-lr8uIm7Wub8ToRXO9f6f7in429P1Egm3I+Ps3ZGfWpwLTCUBnHvJdNF/kQqF7PL0Lw6acXcjVWFYT7l2Wdst2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-pipe": "8.19.4", - "@cspell/cspell-types": "8.19.4", - "cspell-trie-lib": "8.19.4", - "fast-equals": "^5.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cspell-gitignore": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.19.4.tgz", - "integrity": "sha512-KrViypPilNUHWZkMV0SM8P9EQVIyH8HvUqFscI7+cyzWnlglvzqDdV4N5f+Ax5mK+IqR6rTEX8JZbCwIWWV7og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/url": "8.19.4", - "cspell-glob": "8.19.4", - "cspell-io": "8.19.4" - }, - "bin": { - "cspell-gitignore": "bin.mjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cspell-glob": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.19.4.tgz", - "integrity": "sha512-042uDU+RjAz882w+DXKuYxI2rrgVPfRQDYvIQvUrY1hexH4sHbne78+OMlFjjzOCEAgyjnm1ktWUCCmh08pQUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/url": "8.19.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cspell-grammar": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.19.4.tgz", - "integrity": "sha512-lzWgZYTu/L7DNOHjxuKf8H7DCXvraHMKxtFObf8bAzgT+aBmey5fW2LviXUkZ2Lb2R0qQY+TJ5VIGoEjNf55ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-pipe": "8.19.4", - "@cspell/cspell-types": "8.19.4" - }, - "bin": { - "cspell-grammar": "bin.mjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cspell-io": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.19.4.tgz", - "integrity": "sha512-W48egJqZ2saEhPWf5ftyighvm4mztxEOi45ILsKgFikXcWFs0H0/hLwqVFeDurgELSzprr12b6dXsr67dV8amg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-service-bus": "8.19.4", - "@cspell/url": "8.19.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cspell-lib": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.19.4.tgz", - "integrity": "sha512-NwfdCCYtIBNQuZcoMlMmL3HSv2olXNErMi/aOTI9BBAjvCHjhgX5hbHySMZ0NFNynnN+Mlbu5kooJ5asZeB3KA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-bundled-dicts": "8.19.4", - "@cspell/cspell-pipe": "8.19.4", - "@cspell/cspell-resolver": "8.19.4", - "@cspell/cspell-types": "8.19.4", - "@cspell/dynamic-import": "8.19.4", - "@cspell/filetypes": "8.19.4", - "@cspell/strong-weak-map": "8.19.4", - "@cspell/url": "8.19.4", - "clear-module": "^4.1.2", - "comment-json": "^4.2.5", - "cspell-config-lib": "8.19.4", - "cspell-dictionary": "8.19.4", - "cspell-glob": "8.19.4", - "cspell-grammar": "8.19.4", - "cspell-io": "8.19.4", - "cspell-trie-lib": "8.19.4", - "env-paths": "^3.0.0", - "fast-equals": "^5.2.2", - "gensequence": "^7.0.0", - "import-fresh": "^3.3.1", - "resolve-from": "^5.0.0", - "vscode-languageserver-textdocument": "^1.0.12", - "vscode-uri": "^3.1.0", - "xdg-basedir": "^5.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cspell-trie-lib": { - "version": "8.19.4", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.19.4.tgz", - "integrity": "sha512-yIPlmGSP3tT3j8Nmu+7CNpkPh/gBO2ovdnqNmZV+LNtQmVxqFd2fH7XvR1TKjQyctSH1ip0P5uIdJmzY1uhaYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-pipe": "8.19.4", - "@cspell/cspell-types": "8.19.4", - "gensequence": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/css-what": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", - "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true, - "license": "MIT" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", - "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^3.0.0", - "default-browser-id": "^3.0.0", - "execa": "^7.1.1", - "titleize": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/default-browser/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/default-browser/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/default-browser/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/devtools-protocol": { - "version": "0.0.1508733", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz", - "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "license": "MIT", - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dts-bundle-generator": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-9.5.1.tgz", - "integrity": "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "typescript": ">=5.0.2", - "yargs": "^17.6.0" - }, - "bin": { - "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", - "dev": true, - "license": "ISC" - }, - "node_modules/elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" - } - }, - "node_modules/esbuild-plugin-replace": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esbuild-plugin-replace/-/esbuild-plugin-replace-1.4.0.tgz", - "integrity": "sha512-lP3ZAyzyRa5JXoOd59lJbRKNObtK8pJ/RO7o6vdjwLi71GfbL32NR22ZuS7/cLZkr10/L1lutoLma8E4DLngYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.25.7" - } - }, - "node_modules/esbuild-plugin-replace/node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", - "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-jest": { - "version": "28.14.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", - "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "engines": { - "node": "^16.10.0 || ^18.12.0 || >=20.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", - "jest": "*" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-jsdoc": { - "version": "50.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.8.0.tgz", - "integrity": "sha512-UyGb5755LMFWPrZTEqqvTJ3urLz1iqj+bYOHFNag+sw3NvaMWP9K2z+uIn37XfNALmQLQyrBlJ5mkiVPL7ADEg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@es-joy/jsdoccomment": "~0.50.2", - "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.1", - "debug": "^4.4.1", - "escape-string-regexp": "^4.0.0", - "espree": "^10.3.0", - "esquery": "^1.6.0", - "parse-imports-exports": "^0.2.4", - "semver": "^7.7.2", - "spdx-expression-parse": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-jsdoc/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-simple-import-sort": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", - "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=5.0.0" - } - }, - "node_modules/eslint-plugin-wdio": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-wdio/-/eslint-plugin-wdio-8.37.0.tgz", - "integrity": "sha512-X217zXxSqj1IPWu3bxN7D/xEUmNk7Jg5lBf2JwYH3mCogaqL2tnHZnwt0EQ5D9oEejfEl2+4zqHSzhXq1X7F2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.13 || >=18" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/eslint/node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/eslint/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/execa": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.3.0.tgz", - "integrity": "sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.3", - "figures": "^6.1.0", - "get-stream": "^9.0.0", - "human-signals": "^7.0.0", - "is-plain-obj": "^4.1.0", - "is-stream": "^4.0.1", - "npm-run-path": "^5.2.0", - "pretty-ms": "^9.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.5.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-equals": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", - "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-entry-cache": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", - "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up-simple": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", - "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.3.1", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", - "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensequence": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-7.0.0.tgz", - "integrity": "sha512-47Frx13aZh01afHJTB3zTtKIlFI6vWY+MYCN9Qpew6i52rfKjnhCF/l1YlC8UmEMvvntZZ6z4PiCcmyuedR2aQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-tsconfig": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz", - "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/git-raw-commits": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.0.tgz", - "integrity": "sha512-I2ZXrXeOc0KrCvC7swqtIFXFN+rbjnC7b2T943tvemIOVNl+XP8YnA9UVwqFhzzLClnSA60KR/qEjLpXzs73Qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@conventional-changelog/git-client": "^1.0.0", - "meow": "^13.0.0" - }, - "bin": { - "git-raw-commits": "src/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/git-semver-tags": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-8.0.0.tgz", - "integrity": "sha512-N7YRIklvPH3wYWAR2vysaqGLPRcpwQ0GKdlqTiVN5w1UmCdaeY3K8s6DMKRCh54DDdzyt/OAB6C8jgVtb7Y2Fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@conventional-changelog/git-client": "^1.0.0", - "meow": "^13.0.0" - }, - "bin": { - "git-semver-tags": "src/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^1.0.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-7.0.0.tgz", - "integrity": "sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", - "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/index-to-position": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "symbol-observable": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-wsl/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^27.5.1", - "import-local": "^3.0.2", - "jest-cli": "^27.5.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", - "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "execa": "^5.0.0", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-changed-files/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/jest-changed-files/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-changed-files/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/jest-changed-files/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-changed-files/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/jest-changed-files/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-changed-files/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-changed-files/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/jest-changed-files/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/jest-circus": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", - "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/jest-cli/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-cli/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-cli/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-config": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", - "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-config/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-docblock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", - "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", - "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-jasmine2": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", - "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-jasmine2/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-leak-detector": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", - "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", - "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", - "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runner": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", - "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-runtime/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-runtime/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/jest-runtime/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-runtime/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/jest-runtime/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/jest-runtime/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-runtime/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/jest-runtime/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/jest-validate": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", - "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^27.5.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "leven": "^3.1.0", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watcher": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", - "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.5.1", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jquery": { - "version": "4.0.0-pre", - "resolved": "https://github.com/jquery/jquery/tarball/c98597eaf5e144ee5e549cb41984687cd1033068", - "integrity": "sha512-U1l+pjigfVQzAfwDVYPWAoZvkWiJwlo1sYyGYS2/sf0LDUG2up+pBDgj2lug9S8jd7dvMBtqZpJ8GogXeYRYyA==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdoc-type-pratt-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", - "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsdom/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", - "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@samverschueren/stream-to-observable": "^0.3.0", - "is-observable": "^1.1.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.5.0", - "listr-verbose-renderer": "^0.5.0", - "p-map": "^2.0.0", - "rxjs": "^6.3.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha512-L26cIFm7/oZeSNVhWB6faeorXhMg4HNlb/dS/7jHhr708jxlXrtrBWo4YUxZQkc6dGoxEAe6J/D3juTRBUzjtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", - "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^2.3.0", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "listr": "^0.14.2" - } - }, - "node_modules/listr-update-renderer/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/listr-update-renderer/node_modules/figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/listr-verbose-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", - "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^2.4.1", - "cli-cursor": "^2.1.0", - "date-fns": "^1.27.2", - "figures": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/listr-verbose-renderer/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr-verbose-renderer/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/listr-verbose-renderer/node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-symbols/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-symbols/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/log-symbols/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nwsapi": { - "version": "2.2.22", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", - "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/open": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", - "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^4.0.0", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open-in-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/open-in-editor/-/open-in-editor-2.2.0.tgz", - "integrity": "sha512-ZQJDm2lmIgR2GkuwzjrlkVmT2KpDVp0Nnnb3LtYLe3Xi3cQhDa1vnh4IIlrT35a46OLZ8nlKJNOsx2B85FOS+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "clap": "^1.1.3", - "os-homedir": "~1.0.2" - }, - "bin": { - "oe": "bin/oe" - }, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-2.0.0.tgz", - "integrity": "sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parse-imports-exports": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", - "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-statements": "1.0.11" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-statements": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", - "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", - "dev": true, - "license": "MIT" - }, - "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^4.5.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pixelmatch": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", - "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "pngjs": "^6.0.0" - }, - "bin": { - "pixelmatch": "bin/pixelmatch" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pngjs": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", - "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-safe-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", - "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", - "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/puppeteer": { - "version": "24.25.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.25.0.tgz", - "integrity": "sha512-P3rUaom2w/Ubrnz3v3kSbxGkN7SpbtQeGRPb7iO86Bv/dAz2WUmGQBHr37W/Rp1fbAocMvu0rHFbCIJvjiNhGw==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.10.12", - "chromium-bidi": "9.1.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1508733", - "puppeteer-core": "24.25.0", - "typed-query-selector": "^2.12.0" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core": { - "version": "24.25.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.25.0.tgz", - "integrity": "sha512-8Xs6q3Ut+C8y7sAaqjIhzv1QykGWG4gc2mEZ2mYE7siZFuRp4xQVehOf8uQKSQAkeL7jXUs3mknEeiqnRqUKvQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.10.12", - "chromium-bidi": "9.1.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1508733", - "typed-query-selector": "^2.12.0", - "webdriver-bidi-protocol": "0.3.7", - "ws": "^8.18.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-package-up/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/resolve.exports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", - "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", - "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.9", - "@rollup/rollup-android-arm64": "4.34.9", - "@rollup/rollup-darwin-arm64": "4.34.9", - "@rollup/rollup-darwin-x64": "4.34.9", - "@rollup/rollup-freebsd-arm64": "4.34.9", - "@rollup/rollup-freebsd-x64": "4.34.9", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", - "@rollup/rollup-linux-arm-musleabihf": "4.34.9", - "@rollup/rollup-linux-arm64-gnu": "4.34.9", - "@rollup/rollup-linux-arm64-musl": "4.34.9", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", - "@rollup/rollup-linux-riscv64-gnu": "4.34.9", - "@rollup/rollup-linux-s390x-gnu": "4.34.9", - "@rollup/rollup-linux-x64-gnu": "4.34.9", - "@rollup/rollup-linux-x64-musl": "4.34.9", - "@rollup/rollup-win32-arm64-msvc": "4.34.9", - "@rollup/rollup-win32-ia32-msvc": "4.34.9", - "@rollup/rollup-win32-x64-msvc": "4.34.9", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-applescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", - "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/run-applescript/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/run-applescript/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/run-applescript/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/run-applescript/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/run-applescript/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true, - "license": "MIT" - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-correct/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/temp-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", - "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/tempfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-5.0.0.tgz", - "integrity": "sha512-bX655WZI/F7EoTDw9JvQURqAXiPHi8o8+yFxPF2lWYyz1aHnmMRuXWqL6YB6GmeO0o4DIYWHLgGNi/X64T+X4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "temp-dir": "^3.0.0" - }, - "engines": { - "node": ">=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/throat": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", - "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/titleize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", - "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", - "dev": true, - "license": "MIT" - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "dev": true, - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", - "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/v8-to-istanbul/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", - "dev": true, - "license": "MIT", - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/webdriver-bidi-protocol": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.7.tgz", - "integrity": "sha512-wIx5Gu/LLTeexxilpk8WxU2cpGAKlfbWRO5h+my6EMD1k5PYqM1qQO1MHUFf4f3KRnhBvpbZU7VkizAgeSEf7g==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=10.4" - } - }, - "node_modules/webpack": { - "version": "5.104.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", - "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.4", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.4.24" - } - }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/write-file-atomic/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xdg-basedir": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/package.json b/package.json index 87493bb7143..f70d9147832 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ "@types/graceful-fs": "^4.1.5", "@types/jest": "^27.0.3", "@types/listr": "^0.14.4", - "@types/node": "^24.6.2", + "@types/node": "^24.10.12", "@types/pixelmatch": "^5.2.4", "@types/pngjs": "^6.0.1", "@types/prompts": "^2.0.9", @@ -222,6 +222,7 @@ "terser": "5.37.0", "tsx": "^4.19.2", "typescript": "~5.8.3", + "vite": "^7.3.1", "webpack": "^5.75.0", "ws": "8.17.1" }, diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000000..20c3a346abf --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,23 @@ +{ + "name": "@stencil/cli", + "version": "5.0.0-alpha.0", + "description": "CLI for Stencil - Web component compiler", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "stencil": "./dist/stencil" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist/" + ], + "dependencies": { + "@stencil/core": "workspace:*" + } +} diff --git a/packages/cli/src/check-version.ts b/packages/cli/src/check-version.ts new file mode 100644 index 00000000000..7b03e1cb206 --- /dev/null +++ b/packages/cli/src/check-version.ts @@ -0,0 +1,35 @@ +import { isFunction } from '@utils'; + +import type { ValidatedConfig } from '../declarations'; + +/** + * Retrieve a reference to the active `CompilerSystem`'s `checkVersion` function + * @param config the Stencil configuration associated with the currently compiled project + * @param currentVersion the Stencil compiler's version string + * @returns a reference to `checkVersion`, or `null` if one does not exist on the current `CompilerSystem` + */ +export const startCheckVersion = async ( + config: ValidatedConfig, + currentVersion: string, +): Promise<(() => void) | null> => { + if (config.devMode && !config.flags.ci && !currentVersion.includes('-dev.') && isFunction(config.sys.checkVersion)) { + return config.sys.checkVersion(config.logger, currentVersion); + } + return null; +}; + +/** + * Print the results of running the provided `versionChecker`. + * + * Does not print if no `versionChecker` is provided. + * + * @param versionChecker the function to invoke. + */ +export const printCheckVersionResults = async (versionChecker: Promise<(() => void) | null>): Promise => { + if (versionChecker) { + const checkVersionResults = await versionChecker; + if (isFunction(checkVersionResults)) { + checkVersionResults(); + } + } +}; diff --git a/packages/cli/src/config-flags.ts b/packages/cli/src/config-flags.ts new file mode 100644 index 00000000000..01ba12bddeb --- /dev/null +++ b/packages/cli/src/config-flags.ts @@ -0,0 +1,353 @@ +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', + 'e2e', + 'es5', + 'esm', + 'help', + 'log', + 'open', + 'prerender', + 'prerenderExternal', + 'prod', + 'profile', + 'serviceWorker', + 'screenshot', + 'serve', + 'skipNodeCheck', + 'spec', + 'ssr', + 'updateScreenshot', + 'verbose', + 'version', + 'watch', + + // 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', + // 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', + 'screenshotConnector', + + // 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; + +export const STRING_ARRAY_CLI_FLAGS = [ + 'collectCoverageOnlyFrom', + 'coveragePathIgnorePatterns', + 'coverageReporters', + 'moduleDirectories', + 'moduleFileExtensions', + 'modulePathIgnorePatterns', + 'modulePaths', + 'projects', + 'reporters', + 'roots', + 'selectProjects', + 'setupFiles', + 'setupFilesAfterEnv', + 'snapshotSerializers', + 'testMatch', + 'testPathIgnorePatterns', + 'testPathPattern', + 'testRegex', + 'transformIgnorePatterns', + 'unmockedModulePathPatterns', + 'watchPathIgnorePatterns', +] as const; + +/** + * All the CLI arguments which may have string or number values + * + * `maxWorkers` is an argument which is used both by Stencil _and_ by Jest, + * which means that we need to support parsing both string and number values. + */ +export const STRING_NUMBER_CLI_FLAGS = ['maxWorkers'] as const; + +/** + * All the CLI arguments which may have boolean or string values. + */ +export const BOOLEAN_STRING_CLI_FLAGS = [ + /** + * `headless` is an argument passed through to Puppeteer (which is passed to Chrome) for end-to-end testing. + * + * {@see https://developer.chrome.com/blog/chrome-headless-shell/} + */ + 'headless', + /** + * `stats` is an argument that can optionally accept a file path where stats should be written. + * When used as a boolean (--stats), it defaults to 'stencil-stats.json'. + * When used with a path (--stats dist/stats.json), it writes to that path. + */ + 'stats', +] as const; + +/** + * All the LogLevel-type options supported by the Stencil CLI + * + * This is a bit silly since there's only one such argument atm, + * but this approach lets us make sure that we're handling all + * our arguments in a type-safe way. + */ +export const LOG_LEVEL_CLI_FLAGS = ['logLevel'] as const; + +/** + * A type which gives the members of a `ReadonlyArray` as + * an enum-like type which can be used for e.g. keys in a `Record` + * (as in the `AliasMap` type below) + */ +type ArrayValuesAsUnion> = T[number]; + +export type BooleanCLIFlag = ArrayValuesAsUnion; +export type StringCLIFlag = ArrayValuesAsUnion; +export type StringArrayCLIFlag = ArrayValuesAsUnion; +export type NumberCLIFlag = ArrayValuesAsUnion; +export type StringNumberCLIFlag = ArrayValuesAsUnion; +export type BooleanStringCLIFlag = ArrayValuesAsUnion; +export type LogCLIFlag = ArrayValuesAsUnion; + +export type KnownCLIFlag = + | BooleanCLIFlag + | StringCLIFlag + | StringArrayCLIFlag + | NumberCLIFlag + | StringNumberCLIFlag + | BooleanStringCLIFlag + | LogCLIFlag; + +type AliasMap = Partial>; + +/** + * For a small subset of CLI options we support a short alias e.g. `'h'` for `'help'` + */ +export const CLI_FLAG_ALIASES: AliasMap = { + c: 'config', + h: 'help', + p: 'port', + v: 'version', + + // JEST SPECIFIC CLI FLAGS + // these are defined in + // https://github.com/facebook/jest/blob/4156f86/packages/jest-cli/src/args.ts + b: 'bail', + e: 'expand', + f: 'onlyFailures', + i: 'runInBand', + o: 'onlyChanged', + t: 'testNamePattern', + u: 'updateSnapshot', + w: 'maxWorkers', +}; + +/** + * A regular expression which can be used to match a CLI flag for one of our + * short aliases. + */ +export const CLI_FLAG_REGEX = new RegExp(`^-[chpvbewofitu]{1}$`); + +/** + * Given two types `K` and `T` where `K` extends `ReadonlyArray`, + * construct a type which maps the strings in `K` as keys to values of type `T`. + * + * Because we use types derived this way to construct an interface (`ConfigFlags`) + * for which we want optional keys, we make all the properties optional (w/ `'?'`) + * and possibly null. + */ +type ObjectFromKeys, T> = { + [key in K[number]]?: T | null; +}; + +/** + * Type containing the possible Boolean configuration flags, to be included + * in ConfigFlags, below + */ +type BooleanConfigFlags = ObjectFromKeys; + +/** + * Type containing the possible String configuration flags, to be included + * in ConfigFlags, below + */ +type StringConfigFlags = ObjectFromKeys; + +/** + * Type containing the possible String Array configuration flags. This is + * one of the 'constituent types' for `ConfigFlags`. + */ +type StringArrayConfigFlags = ObjectFromKeys; + +/** + * Type containing the possible numeric configuration flags, to be included + * in ConfigFlags, below + */ +type NumberConfigFlags = ObjectFromKeys; + +/** + * Type containing the configuration flags which may be set to either string + * or number values. + */ +type StringNumberConfigFlags = ObjectFromKeys; + +/** + * Type containing the configuration flags which may be set to either string + * or boolean values. + */ +type BooleanStringConfigFlags = ObjectFromKeys; + +/** + * Type containing the possible LogLevel configuration flags, to be included + * in ConfigFlags, below + */ +type LogLevelFlags = ObjectFromKeys; + +/** + * The configuration flags which can be set by the user on the command line. + * This interface captures both known arguments (which are enumerated and then + * parsed according to their types) and unknown arguments which the user may + * pass at the CLI. + * + * Note that this interface is constructed by extending `BooleanConfigFlags`, + * `StringConfigFlags`, etc. These types are in turn constructed from types + * extending `ReadonlyArray` which we declare in another module. This + * allows us to record our known CLI arguments in one place, using a + * `ReadonlyArray` to get both a type-level representation of what CLI + * options we support and a runtime list of strings which can be used to match + * on actual flags passed by the user. + */ +export interface ConfigFlags + extends BooleanConfigFlags, + StringConfigFlags, + StringArrayConfigFlags, + NumberConfigFlags, + StringNumberConfigFlags, + BooleanStringConfigFlags, + LogLevelFlags { + task: TaskCommand | null; + args: string[]; + knownArgs: string[]; + unknownArgs: string[]; +} + +/** + * Helper function for initializing a `ConfigFlags` object. Provide any overrides + * for default values and off you go! + * + * @param init an object with any overrides for default values + * @returns a complete CLI flag object + */ +export const createConfigFlags = (init: Partial = {}): ConfigFlags => { + const flags: ConfigFlags = { + task: null, + args: [], + knownArgs: [], + unknownArgs: [], + ...init, + }; + + return flags; +}; diff --git a/packages/cli/src/find-config.ts b/packages/cli/src/find-config.ts new file mode 100644 index 00000000000..5a1c2a75de9 --- /dev/null +++ b/packages/cli/src/find-config.ts @@ -0,0 +1,80 @@ +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); +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 00000000000..a6c68c10a30 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,4 @@ +export { BOOLEAN_CLI_FLAGS } from './config-flags'; +export type { ConfigFlags } from './config-flags'; +export { parseFlags } from './parse-flags'; +export { run, runTask } from './run'; diff --git a/packages/cli/src/ionic-config.ts b/packages/cli/src/ionic-config.ts new file mode 100644 index 00000000000..0014d490bff --- /dev/null +++ b/packages/cli/src/ionic-config.ts @@ -0,0 +1,64 @@ +import type * as d from '../declarations'; +import { readJson, UUID_REGEX, uuidv4 } from './telemetry/helpers'; + +export const isTest = () => process.env.JEST_WORKER_ID !== undefined; + +export const defaultConfig = (sys: d.CompilerSystem) => + sys.resolvePath(`${sys.homeDir()}/.ionic/${isTest() ? 'tmp-config.json' : 'config.json'}`); + +export const defaultConfigDirectory = (sys: d.CompilerSystem) => sys.resolvePath(`${sys.homeDir()}/.ionic`); + +/** + * Reads an Ionic configuration file from disk, parses it, and performs any necessary corrections to it if certain + * values are deemed to be malformed + * @param sys The system where the command is invoked + * @returns the config read from disk that has been potentially been updated + */ +export async function readConfig(sys: d.CompilerSystem): Promise { + let config: d.TelemetryConfig = await readJson(sys, defaultConfig(sys)); + + if (!config) { + config = { + 'tokens.telemetry': uuidv4(), + 'telemetry.stencil': true, + }; + + await writeConfig(sys, config); + } else if (!config['tokens.telemetry'] || !UUID_REGEX.test(config['tokens.telemetry'])) { + const newUuid = uuidv4(); + await writeConfig(sys, { ...config, 'tokens.telemetry': newUuid }); + config['tokens.telemetry'] = newUuid; + } + + return config; +} + +/** + * Writes an Ionic configuration file to disk. + * @param sys The system where the command is invoked + * @param config The config passed into the Stencil command + * @returns boolean If the command was successful + */ +export async function writeConfig(sys: d.CompilerSystem, config: d.TelemetryConfig): Promise { + let result = false; + try { + await sys.createDir(defaultConfigDirectory(sys), { recursive: true }); + await sys.writeFile(defaultConfig(sys), JSON.stringify(config, null, 2)); + result = true; + } catch (error) { + console.error(`Stencil Telemetry: couldn't write configuration file to ${defaultConfig(sys)} - ${error}.`); + } + + return result; +} + +/** + * Update a subset of the Ionic config. + * @param sys The system where the command is invoked + * @param newOptions The new options to save + * @returns boolean If the command was successful + */ +export async function updateConfig(sys: d.CompilerSystem, newOptions: d.TelemetryConfig): Promise { + const config = await readConfig(sys); + return await writeConfig(sys, Object.assign(config, newOptions)); +} diff --git a/packages/cli/src/load-compiler.ts b/packages/cli/src/load-compiler.ts new file mode 100644 index 00000000000..96fbcb7bf19 --- /dev/null +++ b/packages/cli/src/load-compiler.ts @@ -0,0 +1,7 @@ +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'); diff --git a/packages/cli/src/logs.ts b/packages/cli/src/logs.ts new file mode 100644 index 00000000000..fadf4098ccc --- /dev/null +++ b/packages/cli/src/logs.ts @@ -0,0 +1,130 @@ +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.`); + } + } +}; diff --git a/packages/cli/src/parse-flags.ts b/packages/cli/src/parse-flags.ts new file mode 100644 index 00000000000..4f3da3cc5c0 --- /dev/null +++ b/packages/cli/src/parse-flags.ts @@ -0,0 +1,517 @@ +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)); diff --git a/packages/cli/src/public.ts b/packages/cli/src/public.ts new file mode 100644 index 00000000000..75337990120 --- /dev/null +++ b/packages/cli/src/public.ts @@ -0,0 +1,24 @@ +import type { CliInitOptions, Config, Logger, TaskCommand } from '@stencil/core/internal'; + +import type { ConfigFlags } from './config-flags'; + +/** + * Runs the CLI with the given options. This is used by Stencil's default `bin/stencil` file, + * but can be used externally too. + * @param init a set of initialization options needed to run Stencil from its CLI + * @returns an empty promise + */ +export declare function run(init: CliInitOptions): Promise; + +/** + * Run individual CLI tasks. + * @param coreCompiler The core Stencil compiler to be used. The `run()` method handles loading the core compiler, however, `runTask()` must be passed it. + * @param config Assumes the config has already been validated and has the "sys" and "logger" properties. + * @param task The task command to run, such as `build`. + * @returns an empty promise + */ +export declare function runTask(coreCompiler: any, config: Config, task: TaskCommand): Promise; + +export declare function parseFlags(args: string[]): ConfigFlags; + +export { Config, ConfigFlags, Logger, TaskCommand }; diff --git a/packages/cli/src/run.ts b/packages/cli/src/run.ts new file mode 100644 index 00000000000..ea727235974 --- /dev/null +++ b/packages/cli/src/run.ts @@ -0,0 +1,187 @@ +import { hasError, isFunction, result, shouldIgnoreError } from '@utils'; + +import type * as d from '../declarations'; +import { ValidatedConfig } from '../declarations'; +import { createConfigFlags } from './config-flags'; +import { findConfig } from './find-config'; +import { CoreCompiler, loadCoreCompiler } from './load-compiler'; +import { loadedCompilerLog, startupLog, startupLogVersion } from './logs'; +import { parseFlags } from './parse-flags'; +import { taskBuild } from './task-build'; +import { taskDocs } from './task-docs'; +import { taskGenerate } from './task-generate'; +import { taskHelp } from './task-help'; +import { taskInfo } from './task-info'; +import { taskPrerender } from './task-prerender'; +import { taskServe } from './task-serve'; +import { taskTelemetry } from './task-telemetry'; +import { taskTest } from './task-test'; +import { telemetryAction } from './telemetry/telemetry'; + +/** + * Main entry point for the Stencil CLI + * + * Take care of parsing CLI arguments, initializing various components needed + * by the rest of the program, and kicking off the correct task (build, test, + * etc). + * + * @param init initial CLI options + * @returns an empty promise + */ +export const run = async (init: d.CliInitOptions) => { + const { args, logger, sys } = init; + + try { + const flags = parseFlags(args); + const task = flags.task; + + if (flags.debug || flags.verbose) { + logger.setLevel('debug'); + } + + if (flags.ci) { + logger.enableColors(false); + } + + if (isFunction(sys.applyGlobalPatch)) { + sys.applyGlobalPatch(sys.getCurrentDirectory()); + } + + if ((task && task === 'version') || flags.version) { + // we need to load the compiler here to get the version, but we don't + // want to load it in the case that we're going to just log the help + // message and then exit below (if there's no `task` defined) so we load + // it just within our `if` scope here. + const coreCompiler = await loadCoreCompiler(sys); + console.log(coreCompiler.version); + return; + } + + if (!task || task === 'help' || flags.help) { + await taskHelp(createConfigFlags({ task: 'help', args }), logger, sys); + + return; + } + + startupLog(logger, task); + + const findConfigResults = await findConfig({ sys, configPath: flags.config }); + if (findConfigResults.isErr) { + logger.printDiagnostics(findConfigResults.value); + return sys.exit(1); + } + + const coreCompiler = await loadCoreCompiler(sys); + + startupLogVersion(logger, task, coreCompiler); + + loadedCompilerLog(sys, logger, flags, coreCompiler); + + if (task === 'info') { + taskInfo(coreCompiler, sys, logger); + return; + } + + const foundConfig = result.unwrap(findConfigResults); + const validated = await coreCompiler.loadConfig({ + config: { + flags, + }, + configPath: foundConfig.configPath, + logger, + sys, + }); + + if (validated.diagnostics.length > 0) { + logger.printDiagnostics(validated.diagnostics); + if (hasError(validated.diagnostics)) { + return sys.exit(1); + } + } + + if (isFunction(sys.applyGlobalPatch)) { + sys.applyGlobalPatch(validated.config.rootDir); + } + + await telemetryAction(sys, validated.config, coreCompiler, async () => { + await runTask(coreCompiler, validated.config, task, sys); + }); + } catch (e) { + if (!shouldIgnoreError(e)) { + const details = `${logger.getLevel() === 'debug' && e instanceof Error ? e.stack : ''}`; + logger.error(`uncaught cli error: ${e}${details}`); + return sys.exit(1); + } + } +}; + +/** + * Run a specified task + * + * @param coreCompiler an instance of a minimal, bootstrap compiler for running the specified task + * @param config a configuration for the Stencil project to apply to the task run + * @param task the task to run + * @param sys the {@link d.CompilerSystem} for interacting with the operating system + * @public + * @returns a void promise + */ +export const runTask = async ( + coreCompiler: CoreCompiler, + config: d.Config, + task: d.TaskCommand, + sys: d.CompilerSystem, +): Promise => { + const flags = createConfigFlags(config.flags ?? { task }); + config.flags = flags; + + if (!config.sys) { + config.sys = sys; + } + const strictConfig: ValidatedConfig = coreCompiler.validateConfig(config, {}).config; + + switch (task) { + case 'build': + await taskBuild(coreCompiler, strictConfig); + break; + + case 'docs': + await taskDocs(coreCompiler, strictConfig); + break; + + case 'generate': + case 'g': + await taskGenerate(strictConfig); + break; + + case 'help': + await taskHelp(strictConfig.flags, strictConfig.logger, sys); + break; + + case 'prerender': + await taskPrerender(coreCompiler, strictConfig); + break; + + case 'serve': + await taskServe(strictConfig); + break; + + case 'telemetry': + await taskTelemetry(strictConfig.flags, sys, strictConfig.logger); + break; + + case 'test': + await taskTest(strictConfig); + break; + + case 'version': + console.log(coreCompiler.version); + break; + + default: + strictConfig.logger.error( + `${strictConfig.logger.emoji('❌ ')}Invalid stencil command, please see the options below:`, + ); + await taskHelp(strictConfig.flags, strictConfig.logger, sys); + return config.sys.exit(1); + } +}; diff --git a/packages/cli/src/task-build.ts b/packages/cli/src/task-build.ts new file mode 100644 index 00000000000..cbe03761b1c --- /dev/null +++ b/packages/cli/src/task-build.ts @@ -0,0 +1,57 @@ +import type * as d from '../declarations'; +import { printCheckVersionResults, startCheckVersion } from './check-version'; +import type { CoreCompiler } from './load-compiler'; +import { startupCompilerLog } from './logs'; +import { runPrerenderTask } from './task-prerender'; +import { taskWatch } from './task-watch'; +import { telemetryBuildFinishedAction } from './telemetry/telemetry'; + +export const taskBuild = async (coreCompiler: CoreCompiler, config: d.ValidatedConfig) => { + if (config.flags.watch) { + // watch build + await taskWatch(coreCompiler, config); + return; + } + + // one-time build + let exitCode = 0; + + try { + startupCompilerLog(coreCompiler, config); + + const versionChecker = startCheckVersion(config, coreCompiler.version); + + const compiler = await coreCompiler.createCompiler(config); + const results = await compiler.build(); + + await telemetryBuildFinishedAction(config.sys, config, coreCompiler, results); + + await compiler.destroy(); + + if (results.hasError) { + exitCode = 1; + } else if (config.flags.prerender) { + const prerenderDiagnostics = await runPrerenderTask( + coreCompiler, + config, + results.hydrateAppFilePath, + results.componentGraph, + undefined, + ); + config.logger.printDiagnostics(prerenderDiagnostics); + + if (prerenderDiagnostics.some((d) => d.level === 'error')) { + exitCode = 1; + } + } + + await printCheckVersionResults(versionChecker); + } catch (e) { + exitCode = 1; + config.logger.error(e); + } + + if (exitCode > 0) { + return config.sys.exit(exitCode); + } +}; diff --git a/packages/cli/src/task-docs.ts b/packages/cli/src/task-docs.ts new file mode 100644 index 00000000000..ecd64177efa --- /dev/null +++ b/packages/cli/src/task-docs.ts @@ -0,0 +1,18 @@ +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(); +}; diff --git a/packages/cli/src/task-generate.ts b/packages/cli/src/task-generate.ts new file mode 100644 index 00000000000..0f0189a4d55 --- /dev/null +++ b/packages/cli/src/task-generate.ts @@ -0,0 +1,383 @@ +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; +} diff --git a/packages/cli/src/task-help.ts b/packages/cli/src/task-help.ts new file mode 100644 index 00000000000..67a4b994db6 --- /dev/null +++ b/packages/cli/src/task-help.ts @@ -0,0 +1,57 @@ +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')} +`); +}; diff --git a/packages/cli/src/task-info.ts b/packages/cli/src/task-info.ts new file mode 100644 index 00000000000..eb97b584ff3 --- /dev/null +++ b/packages/cli/src/task-info.ts @@ -0,0 +1,33 @@ +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(``); +}; diff --git a/packages/cli/src/task-prerender.ts b/packages/cli/src/task-prerender.ts new file mode 100644 index 00000000000..1e4bc341641 --- /dev/null +++ b/packages/cli/src/task-prerender.ts @@ -0,0 +1,50 @@ +import { catchError } from '@utils'; + +import type { BuildResultsComponentGraph, Diagnostic, ValidatedConfig } from '../declarations'; +import type { CoreCompiler } from './load-compiler'; +import { startupCompilerLog } from './logs'; + +export const taskPrerender = async (coreCompiler: CoreCompiler, config: ValidatedConfig) => { + startupCompilerLog(coreCompiler, config); + + const hydrateAppFilePath = config.flags.unknownArgs[0]; + + if (typeof hydrateAppFilePath !== 'string') { + config.logger.error(`Missing hydrate app script path`); + return config.sys.exit(1); + } + + const srcIndexHtmlPath = config.srcIndexHtml; + + const diagnostics = await runPrerenderTask(coreCompiler, config, hydrateAppFilePath, undefined, srcIndexHtmlPath); + config.logger.printDiagnostics(diagnostics); + + if (diagnostics.some((d) => d.level === 'error')) { + return config.sys.exit(1); + } +}; + +export const runPrerenderTask = async ( + coreCompiler: CoreCompiler, + config: ValidatedConfig, + hydrateAppFilePath?: string, + componentGraph?: BuildResultsComponentGraph, + srcIndexHtmlPath?: string, +) => { + const diagnostics: Diagnostic[] = []; + + try { + const prerenderer = await coreCompiler.createPrerenderer(config); + const results = await prerenderer.start({ + hydrateAppFilePath, + componentGraph, + srcIndexHtmlPath, + }); + + diagnostics.push(...results.diagnostics); + } catch (e: any) { + catchError(diagnostics, e); + } + + return diagnostics; +}; diff --git a/packages/cli/src/task-serve.ts b/packages/cli/src/task-serve.ts new file mode 100644 index 00000000000..80e4a9e6376 --- /dev/null +++ b/packages/cli/src/task-serve.ts @@ -0,0 +1,38 @@ +import { isString } from '@utils'; + +import type { ValidatedConfig } from '../declarations'; + +export const taskServe = async (config: ValidatedConfig) => { + config.suppressLogs = true; + + config.flags.serve = true; + config.devServer.openBrowser = !!config.flags.open; + config.devServer.reloadStrategy = null; + config.devServer.initialLoadUrl = '/'; + config.devServer.websocket = false; + config.maxConcurrentWorkers = 1; + config.devServer.root = isString(config.flags.root) ? config.flags.root : config.sys.getCurrentDirectory(); + + if (!config.sys.getDevServerExecutingPath || !config.sys.dynamicImport || !config.sys.onProcessInterrupt) { + throw new Error( + `Environment doesn't provide required functions: getDevServerExecutingPath, dynamicImport, onProcessInterrupt`, + ); + } + + const devServerPath = config.sys.getDevServerExecutingPath(); + const { start }: typeof import('@stencil/core/dev-server') = await config.sys.dynamicImport(devServerPath); + const devServer = await start(config.devServer, config.logger); + + console.log(`${config.logger.cyan(' Root:')} ${devServer.root}`); + console.log(`${config.logger.cyan(' Address:')} ${devServer.address}`); + console.log(`${config.logger.cyan(' Port:')} ${devServer.port}`); + console.log(`${config.logger.cyan(' Server:')} ${devServer.browserUrl}`); + console.log(``); + + config.sys.onProcessInterrupt(() => { + if (devServer) { + config.logger.debug(`dev server close: ${devServer.browserUrl}`); + devServer.close(); + } + }); +}; diff --git a/packages/cli/src/task-telemetry.ts b/packages/cli/src/task-telemetry.ts new file mode 100644 index 00000000000..107ab9a3cf7 --- /dev/null +++ b/packages/cli/src/task-telemetry.ts @@ -0,0 +1,49 @@ +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 + `); +}; diff --git a/packages/cli/src/task-test.ts b/packages/cli/src/task-test.ts new file mode 100644 index 00000000000..88a5723110c --- /dev/null +++ b/packages/cli/src/task-test.ts @@ -0,0 +1,66 @@ +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.buildDocs = false; + const testingRunOpts: TestingRunOptions = { + e2e: !!config.flags.e2e, + screenshot: !!config.flags.screenshot, + spec: !!config.flags.spec, + updateScreenshot: !!config.flags.updateScreenshot, + }; + + // always ensure we have jest modules installed + const ensureModuleIds = ['@types/jest', 'jest', 'jest-cli']; + + if (testingRunOpts.e2e) { + // if it's an e2e test, also make sure we're got + // puppeteer modules installed and if browserExecutablePath is provided don't download Chromium use only puppeteer-core instead + const puppeteer = config.testing.browserExecutablePath ? 'puppeteer-core' : 'puppeteer'; + + ensureModuleIds.push(puppeteer); + + if (testingRunOpts.screenshot) { + // ensure we've got pixelmatch for screenshots + config.logger.warn( + config.logger.yellow( + `EXPERIMENTAL: screenshot visual diff testing is currently under heavy development and has not reached a stable status. However, any assistance testing would be appreciated.`, + ), + ); + } + } + + // ensure we've got the required modules installed + const diagnostics = await config.sys.lazyRequire?.ensure(config.rootDir, ensureModuleIds); + if (diagnostics && diagnostics.length > 0) { + config.logger.printDiagnostics(diagnostics); + return config.sys.exit(1); + } + + try { + /** + * We dynamically import the testing submodule here in order for Stencil's lazy module checking to work properly. + * + * Prior to this call, we create a collection of string-based node module names and ensure that they're installed & + * on disk. The testing submodule includes `jest` (amongst other) testing libraries in its dependency chain. We need + * to run the lazy module check _before_ we include `jest` et al. in our dependency chain otherwise, the lazy module + * checking would fail to run properly (because we'd import `jest`, which wouldn't exist, before we even checked if + * it was installed). + */ + const { createTesting } = await import('@stencil/core/testing'); + const testing = await createTesting(config); + const passed = await testing.run(testingRunOpts); + await testing.destroy(); + + if (!passed) { + return config.sys.exit(1); + } + } catch (e) { + config.logger.error(e); + return config.sys.exit(1); + } +}; diff --git a/packages/cli/src/task-watch.ts b/packages/cli/src/task-watch.ts new file mode 100644 index 00000000000..64305be4e35 --- /dev/null +++ b/packages/cli/src/task-watch.ts @@ -0,0 +1,66 @@ +import type { DevServer, ValidatedConfig } from '../declarations'; +import { printCheckVersionResults, startCheckVersion } from './check-version'; +import type { CoreCompiler } from './load-compiler'; +import { startupCompilerLog } from './logs'; + +export const taskWatch = async (coreCompiler: CoreCompiler, config: ValidatedConfig) => { + let devServer: DevServer | null = null; + let exitCode = 0; + + try { + startupCompilerLog(coreCompiler, config); + + const versionChecker = startCheckVersion(config, coreCompiler.version); + + const compiler = await coreCompiler.createCompiler(config); + const watcher = await compiler.createWatcher(); + + if (!config.sys.getDevServerExecutingPath || !config.sys.dynamicImport || !config.sys.onProcessInterrupt) { + throw new Error( + `Environment doesn't provide required functions: getDevServerExecutingPath, dynamicImport, onProcessInterrupt`, + ); + } + + if (config.flags.serve) { + const devServerPath = config.sys.getDevServerExecutingPath(); + const { start }: typeof import('@stencil/core/dev-server') = await config.sys.dynamicImport(devServerPath); + devServer = await start(config.devServer, config.logger, watcher); + } + + config.sys.onProcessInterrupt(() => { + config.logger.debug(`close watch`); + compiler && compiler.destroy(); + }); + + const rmVersionCheckerLog = watcher.on('buildFinish', async () => { + // log the version check one time + rmVersionCheckerLog(); + printCheckVersionResults(versionChecker); + }); + + if (devServer) { + const rmDevServerLog = watcher.on('buildFinish', () => { + // log the dev server url one time + rmDevServerLog(); + const url = devServer?.browserUrl ?? 'UNKNOWN URL'; + config.logger.info(`${config.logger.cyan(url)}\n`); + }); + } + + const closeResults = await watcher.start(); + if (closeResults.exitCode > 0) { + exitCode = closeResults.exitCode; + } + } catch (e) { + exitCode = 1; + config.logger.error(e); + } + + if (devServer) { + await devServer.close(); + } + + if (exitCode > 0) { + return config.sys.exit(exitCode); + } +}; diff --git a/packages/cli/src/telemetry/helpers.ts b/packages/cli/src/telemetry/helpers.ts new file mode 100644 index 00000000000..b74733c036f --- /dev/null +++ b/packages/cli/src/telemetry/helpers.ts @@ -0,0 +1,67 @@ +import type * as d from '../../declarations'; +import { ConfigFlags } from '../config-flags'; + +export const tryFn = async Promise, R>(fn: T, ...args: any[]): Promise => { + try { + return await fn(...args); + } catch { + // ignore + } + + return null; +}; + +export const isInteractive = (sys: d.CompilerSystem, flags: ConfigFlags, object?: d.TerminalInfo): boolean => { + const terminalInfo = + object || + Object.freeze({ + tty: sys.isTTY() ? true : false, + ci: + ['CI', 'BUILD_ID', 'BUILD_NUMBER', 'BITBUCKET_COMMIT', 'CODEBUILD_BUILD_ARN'].filter( + (v) => !!sys.getEnvironmentVar?.(v), + ).length > 0 || !!flags.ci, + }); + + return terminalInfo.tty && !terminalInfo.ci; +}; + +export const UUID_REGEX = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); + +// Plucked from https://github.com/ionic-team/capacitor/blob/b893a57aaaf3a16e13db9c33037a12f1a5ac92e0/cli/src/util/uuid.ts +export function uuidv4(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c == 'x' ? r : (r & 0x3) | 0x8; + + return v.toString(16); + }); +} + +/** + * Reads and parses a JSON file from the given `path` + * @param sys The system where the command is invoked + * @param path the path on the file system to read and parse + * @returns the parsed JSON + */ +export async function readJson(sys: d.CompilerSystem, path: string): Promise { + const file = await sys.readFile(path); + return !!file && JSON.parse(file); +} + +/** + * Does the command have the debug flag? + * @param flags The configuration flags passed into the Stencil command + * @returns true if --debug has been passed, otherwise false + */ +export function hasDebug(flags: ConfigFlags): boolean { + return !!flags.debug; +} + +/** + * Does the command have the verbose and debug flags? + * @param flags The configuration flags passed into the Stencil command + * @returns true if both --debug and --verbose have been passed, otherwise false + */ +export function hasVerbose(flags: ConfigFlags): boolean { + return !!flags.verbose && hasDebug(flags); +} diff --git a/packages/cli/src/telemetry/shouldTrack.ts b/packages/cli/src/telemetry/shouldTrack.ts new file mode 100644 index 00000000000..f4b0074d518 --- /dev/null +++ b/packages/cli/src/telemetry/shouldTrack.ts @@ -0,0 +1,14 @@ +import * as d from '../../declarations'; +import { isInteractive } from './helpers'; +import { checkTelemetry } from './telemetry'; + +/** + * Used to determine if tracking should occur. + * @param config The config passed into the Stencil command + * @param sys The system where the command is invoked + * @param ci whether or not the process is running in a Continuous Integration (CI) environment + * @returns true if telemetry should be sent, false otherwise + */ +export async function shouldTrack(config: d.ValidatedConfig, sys: d.CompilerSystem, ci?: boolean) { + return !ci && isInteractive(sys, config.flags) && (await checkTelemetry(sys)); +} diff --git a/packages/cli/src/telemetry/telemetry.ts b/packages/cli/src/telemetry/telemetry.ts new file mode 100644 index 00000000000..d50f9b082b1 --- /dev/null +++ b/packages/cli/src/telemetry/telemetry.ts @@ -0,0 +1,493 @@ +import { isOutputTargetHydrate, WWW } from '@utils'; + +import type * as d from '../../declarations'; +import { readConfig, updateConfig, writeConfig } from '../ionic-config'; +import { CoreCompiler } from '../load-compiler'; +import { hasDebug, hasVerbose, readJson, tryFn, uuidv4 } from './helpers'; +import { shouldTrack } from './shouldTrack'; + +/** + * Used to within taskBuild to provide the component_count property. + * + * @param sys The system where the command is invoked + * @param config The config passed into the Stencil command + * @param coreCompiler The compiler used to do builds + * @param result The results of a compiler build. + */ +export async function telemetryBuildFinishedAction( + sys: d.CompilerSystem, + config: d.ValidatedConfig, + coreCompiler: CoreCompiler, + result: d.CompilerBuildResults, +) { + const tracking = await shouldTrack(config, sys, !!config.flags.ci); + + if (!tracking) { + return; + } + + const component_count = result.componentGraph ? Object.keys(result.componentGraph).length : undefined; + + const data = await prepareData(coreCompiler, config, sys, result.duration, component_count); + + await sendMetric(sys, config, 'stencil_cli_command', data); + + config.logger.debug(`${config.logger.blue('Telemetry')}: ${config.logger.gray(JSON.stringify(data))}`); +} + +/** + * A function to wrap a compiler task function around. Will send telemetry if, and only if, the machine allows. + * + * @param sys The system where the command is invoked + * @param config The config passed into the Stencil command + * @param coreCompiler The compiler used to do builds + * @param action A Promise-based function to call in order to get the duration of any given command. + * @returns void + */ +export async function telemetryAction( + sys: d.CompilerSystem, + config: d.ValidatedConfig, + coreCompiler: CoreCompiler, + action?: d.TelemetryCallback, +) { + const tracking = await shouldTrack(config, sys, !!config.flags.ci); + + let duration = undefined; + let error: any; + + if (action) { + const start = new Date(); + + try { + await action(); + } catch (e) { + error = e; + } + + const end = new Date(); + duration = end.getTime() - start.getTime(); + } + + // We'll get componentCount details inside the taskBuild, so let's not send two messages. + if (!tracking || (config.flags.task == 'build' && !config.flags.args.includes('--watch'))) { + return; + } + + const data = await prepareData(coreCompiler, config, sys, duration); + + await sendMetric(sys, config, 'stencil_cli_command', data); + config.logger.debug(`${config.logger.blue('Telemetry')}: ${config.logger.gray(JSON.stringify(data))}`); + + if (error) { + throw error; + } +} + +/** + * Helper function to determine if a Stencil configuration builds an application. + * + * This function is a rough approximation whether an application is generated as a part of a Stencil build, based on + * contents of the project's `stencil.config.ts` file. + * + * @param config the configuration used by the Stencil project + * @returns true if we believe the project generates an application, false otherwise + */ +export function hasAppTarget(config: d.ValidatedConfig): boolean { + return config.outputTargets.some( + (target) => target.type === WWW && (!!target.serviceWorker || (!!target.baseUrl && target.baseUrl !== '/')), + ); +} + +export function isUsingYarn(sys: d.CompilerSystem) { + return sys.getEnvironmentVar?.('npm_execpath')?.includes('yarn') || false; +} + +/** + * Build a list of the different types of output targets used in a Stencil configuration. + * + * Duplicate entries will not be returned from the list + * + * @param config the configuration used by the Stencil project + * @returns a unique list of output target types found in the Stencil configuration + */ +export function getActiveTargets(config: d.ValidatedConfig): string[] { + const result = config.outputTargets.map((t) => t.type); + return Array.from(new Set(result)); +} + +/** + * Prepare data for telemetry + * + * @param coreCompiler the core compiler + * @param config the current Stencil config + * @param sys the compiler system instance in use + * @param duration_ms the duration of the action being tracked + * @param component_count the number of components being built (optional) + * @returns a Promise wrapping data for the telemetry endpoint + */ +export const prepareData = async ( + coreCompiler: CoreCompiler, + config: d.ValidatedConfig, + sys: d.CompilerSystem, + duration_ms: number | undefined, + component_count: number | undefined = undefined, +): Promise => { + const { typescript, rollup } = coreCompiler.versions || { typescript: 'unknown', rollup: 'unknown' }; + const { packages, packagesNoVersions } = await getInstalledPackages(sys, config); + const targets = getActiveTargets(config); + const yarn = isUsingYarn(sys); + const stencil = coreCompiler.version || 'unknown'; + const system = `${sys.name} ${sys.version}`; + const os_name = sys.details?.platform; + const os_version = sys.details?.release; + const cpu_model = sys.details?.cpuModel; + const build = coreCompiler.buildId || 'unknown'; + const has_app_pwa_config = hasAppTarget(config); + const anonymizedConfig = anonymizeConfigForTelemetry(config); + + return { + arguments: config.flags.args, + build, + component_count, + config: anonymizedConfig, + cpu_model, + duration_ms, + has_app_pwa_config, + os_name, + os_version, + packages, + packages_no_versions: packagesNoVersions, + rollup, + stencil, + system, + system_major: getMajorVersion(system), + targets, + task: config.flags.task, + typescript, + yarn, + }; +}; + +// Setting a key type to `never` excludes it from a mapped type, so we +// can get only keys which map to a string value by excluding all keys `K` +// where `d.Config[K]` does not extend `string`. +type ConfigStringKeys = keyof { + [K in keyof d.Config as Required[K] extends string ? K : never]: d.Config[K]; +}; + +// props in output targets for which we retain their original values when +// preparing a config for telemetry +// +// we omit the values of all other fields on output targets. +const OUTPUT_TARGET_KEYS_TO_KEEP: ReadonlyArray = ['type']; + +// top-level config props that we anonymize for telemetry +const CONFIG_PROPS_TO_ANONYMIZE: ReadonlyArray = [ + 'rootDir', + 'fsNamespace', + 'packageJsonFilePath', + 'namespace', + 'srcDir', + 'srcIndexHtml', + 'buildLogFilePath', + 'cacheDir', + 'configPath', + 'tsconfig', +]; + +// Props we delete entirely from the config for telemetry +// +// TODO(STENCIL-469): Investigate improving anonymization for tsCompilerOptions and devServer +const CONFIG_PROPS_TO_DELETE: ReadonlyArray = [ + 'commonjs', + 'devServer', + 'env', + 'logger', + 'rollupConfig', + 'sys', + 'testing', + 'tsCompilerOptions', +]; + +/** + * Anonymize the config for telemetry, replacing potentially revealing config props + * with a placeholder string if they are present (this lets us still track how frequently + * these config options are being used) + * + * @param config the config to anonymize + * @returns an anonymized copy of the same config + */ +export const anonymizeConfigForTelemetry = (config: d.ValidatedConfig): d.Config => { + const anonymizedConfig: d.Config = { ...config }; + + for (const prop of CONFIG_PROPS_TO_ANONYMIZE) { + if (anonymizedConfig[prop] !== undefined) { + anonymizedConfig[prop] = 'omitted'; + } + } + + anonymizedConfig.outputTargets = config.outputTargets.map((target) => { + // Anonymize the outputTargets on our configuration, taking advantage of the + // optional 2nd argument to `JSON.stringify`. If anything is not a string + // we retain it so that any nested properties are handled, else we check + // whether it's in our 'keep' list to decide whether to keep it or replace it + // with `"omitted"`. + const anonymizedOT = JSON.parse( + JSON.stringify(target, (key, value) => { + if (!(typeof value === 'string')) { + return value; + } + if (OUTPUT_TARGET_KEYS_TO_KEEP.includes(key)) { + return value; + } + return 'omitted'; + }), + ); + + // this prop has to be handled separately because it is an array + // so the replace function above will be called with all of its + // members, giving us `["omitted", "omitted", ...]`. + // + // Instead, we check for its presence and manually copy over. + if (isOutputTargetHydrate(target) && target.external) { + anonymizedOT['external'] = target.external.concat(); + } + return anonymizedOT; + }); + + // TODO(STENCIL-469): Investigate improving anonymization for tsCompilerOptions and devServer + for (const prop of CONFIG_PROPS_TO_DELETE) { + delete anonymizedConfig[prop]; + } + + return anonymizedConfig; +}; + +/** + * Reads package-lock.json, yarn.lock, and package.json files in order to cross-reference + * the dependencies and devDependencies properties. Pulls up the current installed version + * of each package under the @stencil, @ionic, and @capacitor scopes. + * + * @param sys the system instance where telemetry is invoked + * @param config the Stencil configuration associated with the current task that triggered telemetry + * @returns an object listing all dev and production dependencies under the aforementioned scopes + */ +async function getInstalledPackages( + sys: d.CompilerSystem, + config: d.ValidatedConfig, +): Promise<{ packages: string[]; packagesNoVersions: string[] }> { + let packages: string[] = []; + let packagesNoVersions: string[] = []; + const yarn = isUsingYarn(sys); + + try { + // Read package.json and package-lock.json + const appRootDir = sys.getCurrentDirectory(); + + const packageJson: d.PackageJsonData | null = await tryFn( + readJson, + sys, + sys.resolvePath(appRootDir + '/package.json'), + ); + + // They don't have a package.json for some reason? Eject button. + if (!packageJson) { + return { packages, packagesNoVersions }; + } + + const rawPackages: [string, string][] = Object.entries({ + ...packageJson.devDependencies, + ...packageJson.dependencies, + }); + + // Collect packages only in the stencil, ionic, or capacitor org's: + // https://www.npmjs.com/org/stencil + const ionicPackages = rawPackages.filter( + ([k]) => k.startsWith('@stencil/') || k.startsWith('@ionic/') || k.startsWith('@capacitor/'), + ); + + try { + packages = yarn ? await yarnPackages(sys, ionicPackages) : await npmPackages(sys, ionicPackages); + } catch (e) { + packages = ionicPackages.map(([k, v]) => `${k}@${v.replace('^', '')}`); + } + + packagesNoVersions = ionicPackages.map(([k]) => `${k}`); + + return { packages, packagesNoVersions }; + } catch (err) { + hasDebug(config.flags) && console.error(err); + return { packages, packagesNoVersions }; + } +} + +/** + * Visits the npm lock file to find the exact versions that are installed + * @param sys The system where the command is invoked + * @param ionicPackages a list of the found packages matching `@stencil`, `@capacitor`, or `@ionic` from the package.json file. + * @returns an array of strings of all the packages and their versions. + */ +async function npmPackages(sys: d.CompilerSystem, ionicPackages: [string, string][]): Promise { + const appRootDir = sys.getCurrentDirectory(); + const packageLockJson: any = await tryFn(readJson, sys, sys.resolvePath(appRootDir + '/package-lock.json')); + + return ionicPackages.map(([k, v]) => { + let version = packageLockJson?.dependencies[k]?.version ?? packageLockJson?.devDependencies[k]?.version ?? v; + version = version.includes('file:') ? sanitizeDeclaredVersion(v) : version; + return `${k}@${version}`; + }); +} + +/** + * Visits the yarn lock file to find the exact versions that are installed + * @param sys The system where the command is invoked + * @param ionicPackages a list of the found packages matching `@stencil`, `@capacitor`, or `@ionic` from the package.json file. + * @returns an array of strings of all the packages and their versions. + */ +async function yarnPackages(sys: d.CompilerSystem, ionicPackages: [string, string][]): Promise { + const appRootDir = sys.getCurrentDirectory(); + const yarnLock = sys.readFileSync(sys.resolvePath(appRootDir + '/yarn.lock')); + const yarnLockYml = sys.parseYarnLockFile?.(yarnLock); + + return ionicPackages.map(([k, v]) => { + const identifiedVersion = `${k}@${v}`; + let version = yarnLockYml?.object[identifiedVersion]?.version; + version = version && version.includes('undefined') ? sanitizeDeclaredVersion(identifiedVersion) : version; + return `${k}@${version}`; + }); +} + +/** + * This function is used for fallback purposes, where an npm or yarn lock file doesn't exist in the consumers directory. + * This will strip away '*', '^' and '~' from the declared package versions in a package.json. + * @param version the raw semver pattern identifier version string + * @returns a cleaned up representation without any qualifiers + */ +function sanitizeDeclaredVersion(version: string): string { + return version.replace(/[*^~]/g, ''); +} + +/** + * If telemetry is enabled, send a metric to an external data store + * + * @param sys the system instance where telemetry is invoked + * @param config the Stencil configuration associated with the current task that triggered telemetry + * @param name the name of a trackable metric. Note this name is not necessarily a scalar value to track, like + * "Stencil Version". For example, "stencil_cli_command" is a name that is used to track all CLI command information. + * @param value the data to send to the external data store under the provided name argument + */ +export async function sendMetric( + sys: d.CompilerSystem, + config: d.ValidatedConfig, + name: string, + value: d.TrackableData, +): Promise { + const session_id = await getTelemetryToken(sys); + + const message: d.Metric = { + name, + timestamp: new Date().toISOString(), + source: 'stencil_cli', + value, + session_id, + }; + + await sendTelemetry(sys, config, message); +} + +/** + * Used to read the config file's tokens.telemetry property. + * + * @param sys The system where the command is invoked + * @returns string + */ +async function getTelemetryToken(sys: d.CompilerSystem) { + const config = await readConfig(sys); + if (config['tokens.telemetry'] === undefined) { + config['tokens.telemetry'] = uuidv4(); + await writeConfig(sys, config); + } + return config['tokens.telemetry']; +} + +/** + * Issues a request to the telemetry server. + * @param sys The system where the command is invoked + * @param config The config passed into the Stencil command + * @param data Data to be tracked + */ +async function sendTelemetry(sys: d.CompilerSystem, config: d.ValidatedConfig, data: d.Metric): Promise { + try { + const now = new Date().toISOString(); + + const body = { + metrics: [data], + sent_at: now, + }; + + if (!sys.fetch) { + throw new Error('No fetch implementation available'); + } + + // This request is only made if telemetry is on. + const response = await sys.fetch('https://api.ionicjs.com/events/metrics', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + hasVerbose(config.flags) && + console.debug('\nSent %O metric to events service (status: %O)', data.name, response.status, '\n'); + + if (response.status !== 204) { + hasVerbose(config.flags) && + console.debug('\nBad response from events service. Request body: %O', response.body.toString(), '\n'); + } + } catch (e) { + hasVerbose(config.flags) && console.debug('Telemetry request failed:', e); + } +} + +/** + * Checks if telemetry is enabled on this machine + * @param sys The system where the command is invoked + * @returns true if telemetry is enabled, false otherwise + */ +export async function checkTelemetry(sys: d.CompilerSystem): Promise { + const config = await readConfig(sys); + if (config['telemetry.stencil'] === undefined) { + config['telemetry.stencil'] = true; + await writeConfig(sys, config); + } + return config['telemetry.stencil']; +} + +/** + * Writes to the config file, enabling telemetry for this machine. + * @param sys The system where the command is invoked + * @returns true if writing the file was successful, false otherwise + */ +export async function enableTelemetry(sys: d.CompilerSystem): Promise { + return await updateConfig(sys, { 'telemetry.stencil': true }); +} + +/** + * Writes to the config file, disabling telemetry for this machine. + * @param sys The system where the command is invoked + * @returns true if writing the file was successful, false otherwise + */ +export async function disableTelemetry(sys: d.CompilerSystem): Promise { + return await updateConfig(sys, { 'telemetry.stencil': false }); +} + +/** + * Takes in a semver string in order to return the major version. + * @param version The fully qualified semver version + * @returns a string of the major version + */ +function getMajorVersion(version: string): string { + const parts = version.split('.'); + return parts[0]; +} diff --git a/packages/cli/src/telemetry/test/helpers.spec.ts b/packages/cli/src/telemetry/test/helpers.spec.ts new file mode 100644 index 00000000000..bdcc0cfad9c --- /dev/null +++ b/packages/cli/src/telemetry/test/helpers.spec.ts @@ -0,0 +1,106 @@ +import { createSystem } from '../../../compiler/sys/stencil-sys'; +import { ConfigFlags, createConfigFlags } from '../../config-flags'; +import { hasDebug, hasVerbose, isInteractive, tryFn, uuidv4 } from '../helpers'; + +describe('hasDebug', () => { + it('returns true when the "debug" flag is true', () => { + const flags = createConfigFlags({ + debug: true, + }); + + expect(hasDebug(flags)).toBe(true); + }); + + it('returns false when the "debug" flag is false', () => { + const flags = createConfigFlags({ + debug: false, + }); + + expect(hasDebug(flags)).toBe(false); + }); + + it('returns false when a flag is not passed', () => { + const flags = createConfigFlags({}); + + expect(hasDebug(flags)).toBe(false); + }); +}); + +describe('hasVerbose', () => { + it.each>([ + { debug: true, verbose: false }, + { debug: false, verbose: true }, + { debug: false, verbose: false }, + ])('returns false when debug=$debug and verbose=$verbose', (flagOverrides) => { + const flags = createConfigFlags(flagOverrides); + + expect(hasVerbose(flags)).toBe(false); + }); + + it('returns true when debug=true and verbose=true', () => { + const flags = createConfigFlags({ + debug: true, + verbose: true, + }); + + expect(hasVerbose(flags)).toBe(true); + }); +}); + +describe('uuidv4', () => { + it('outputs a UUID', () => { + const pattern = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); + const uuid = uuidv4(); + expect(!!uuid.match(pattern)).toBe(true); + }); +}); + +describe('isInteractive', () => { + const sys = createSystem(); + + it('returns false by default', () => { + const result = isInteractive(sys, createConfigFlags({ ci: false }), { ci: false, tty: false }); + expect(result).toBe(false); + }); + + it('returns false when tty is false', () => { + const result = isInteractive(sys, createConfigFlags({ ci: true }), { ci: true, tty: false }); + expect(result).toBe(false); + }); + + it('returns false when ci is true', () => { + const result = isInteractive(sys, createConfigFlags({ ci: true }), { ci: true, tty: true }); + expect(result).toBe(false); + }); + + it('returns true when tty is true and ci is false', () => { + const result = isInteractive(sys, createConfigFlags({ ci: false }), { ci: false, tty: true }); + expect(result).toBe(true); + }); +}); + +describe('tryFn', () => { + it('handles failures correctly', async () => { + const result = await tryFn(async () => { + throw new Error('Uh oh!'); + }); + + expect(result).toBe(null); + }); + + it('handles success correctly', async () => { + const result = await tryFn(async () => { + return true; + }); + + expect(result).toBe(true); + }); + + it('handles returning false correctly', async () => { + const result = await tryFn(async () => { + return false; + }); + + expect(result).toBe(false); + }); +}); diff --git a/packages/cli/src/telemetry/test/telemetry.spec.ts b/packages/cli/src/telemetry/test/telemetry.spec.ts new file mode 100644 index 00000000000..bc51528ea26 --- /dev/null +++ b/packages/cli/src/telemetry/test/telemetry.spec.ts @@ -0,0 +1,285 @@ +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' }, + ]); + }); +}); diff --git a/packages/cli/src/test/ionic-config.spec.ts b/packages/cli/src/test/ionic-config.spec.ts new file mode 100644 index 00000000000..4cd688a1c69 --- /dev/null +++ b/packages/cli/src/test/ionic-config.spec.ts @@ -0,0 +1,107 @@ +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); + }); +}); diff --git a/packages/cli/src/test/parse-flags.spec.ts b/packages/cli/src/test/parse-flags.spec.ts new file mode 100644 index 00000000000..b6228514cd2 --- /dev/null +++ b/packages/cli/src/test/parse-flags.spec.ts @@ -0,0 +1,471 @@ +import { toDashCase } from '@utils'; + +import { LogLevel } from '../../declarations'; +import { + BOOLEAN_CLI_FLAGS, + BOOLEAN_STRING_CLI_FLAGS, + BooleanStringCLIFlag, + ConfigFlags, + NUMBER_CLI_FLAGS, + STRING_ARRAY_CLI_FLAGS, + STRING_CLI_FLAGS, + StringArrayCLIFlag, +} from '../config-flags'; +import { Empty, parseEqualsArg, parseFlags } from '../parse-flags'; + +describe('parseFlags', () => { + it('should get known and unknown args', () => { + const args = ['serve', '--address', '127.0.0.1', '--potatoArgument', '--flimflammery', 'test.spec.ts']; + + const flags = parseFlags(args); + expect(flags.task).toBe('serve'); + expect(flags.args[0]).toBe('--address'); + expect(flags.args[1]).toBe('127.0.0.1'); + expect(flags.args[2]).toBe('--potatoArgument'); + expect(flags.args[3]).toBe('--flimflammery'); + expect(flags.args[4]).toBe('test.spec.ts'); + expect(flags.knownArgs).toEqual(['--address', '127.0.0.1']); + expect(flags.unknownArgs[0]).toBe('--potatoArgument'); + expect(flags.unknownArgs[1]).toBe('--flimflammery'); + expect(flags.unknownArgs[2]).toBe('test.spec.ts'); + }); + + it('should parse cli for dev server', () => { + // user command line args + // $ npm run serve --port 4444 + + // args.slice(2) + // [ 'serve', '--address', '127.0.0.1', '--port', '4444' ] + + const args = ['serve', '--address', '127.0.0.1', '--port', '4444']; + + const flags = parseFlags(args); + expect(flags.task).toBe('serve'); + expect(flags.address).toBe('127.0.0.1'); + expect(flags.port).toBe(4444); + expect(flags.knownArgs).toEqual(['--address', '127.0.0.1', '--port', '4444']); + }); + + it('should parse task', () => { + const flags = parseFlags(['build']); + expect(flags.task).toBe('build'); + }); + + it('should parse no task', () => { + const flags = parseFlags(['--flag']); + expect(flags.task).toBe(null); + }); + + /** + * these comprehensive tests of all the supported boolean args serve as + * regression tests against duplicating any of the arguments in the arrays. + * Because of the way that the arg parsing algorithm works having a dupe + * will result in a value like `[true, true]` being set on ConfigFlags, which + * will cause these tests to start failing. + */ + describe.each(BOOLEAN_CLI_FLAGS)('should parse boolean flag %s', (cliArg) => { + it('should parse arg', () => { + const flags = parseFlags([`--${cliArg}`]); + expect(flags.knownArgs).toEqual([`--${cliArg}`]); + expect(flags[cliArg]).toBe(true); + }); + + it(`should parse --no${cliArg}`, () => { + const negativeFlag = '--no' + cliArg.charAt(0).toUpperCase() + cliArg.slice(1); + const flags = parseFlags([negativeFlag]); + expect(flags.knownArgs).toEqual([negativeFlag]); + expect(flags[cliArg]).toBe(false); + }); + + it(`should override --${cliArg} with --no${cliArg}`, () => { + const negativeFlag = '--no' + cliArg.charAt(0).toUpperCase() + cliArg.slice(1); + const flags = parseFlags([`--${cliArg}`, negativeFlag]); + expect(flags.knownArgs).toEqual([`--${cliArg}`, negativeFlag]); + expect(flags[cliArg]).toBe(false); + }); + + it('should not set value if not present', () => { + const flags = parseFlags([]); + expect(flags.knownArgs).toEqual([]); + expect(flags[cliArg]).toBe(undefined); + }); + + it.each([true, false])(`should set the value with --${cliArg}=%p`, (value) => { + const flags = parseFlags([`--${cliArg}=${value}`]); + expect(flags.knownArgs).toEqual([`--${cliArg}`, String(value)]); + expect(flags[cliArg]).toBe(value); + }); + }); + + describe.each(STRING_CLI_FLAGS)('should parse string flag %s', (cliArg) => { + it(`should parse "--${cliArg} value"`, () => { + const flags = parseFlags([`--${cliArg}`, 'test-value']); + expect(flags.knownArgs).toEqual([`--${cliArg}`, 'test-value']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toBe('test-value'); + }); + + it(`should parse "--${cliArg}=value"`, () => { + const flags = parseFlags([`--${cliArg}=path/to/file.js`]); + expect(flags.knownArgs).toEqual([`--${cliArg}`, 'path/to/file.js']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toBe('path/to/file.js'); + }); + + it(`should parse "--${toDashCase(cliArg)} value"`, () => { + const flags = parseFlags([`--${toDashCase(cliArg)}`, 'path/to/file.js']); + expect(flags.knownArgs).toEqual([`--${toDashCase(cliArg)}`, 'path/to/file.js']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toBe('path/to/file.js'); + }); + + it(`should parse "--${toDashCase(cliArg)}=value"`, () => { + const flags = parseFlags([`--${toDashCase(cliArg)}=path/to/file.js`]); + expect(flags.knownArgs).toEqual([`--${toDashCase(cliArg)}`, 'path/to/file.js']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toBe('path/to/file.js'); + }); + }); + + it.each(NUMBER_CLI_FLAGS)('should parse number flag %s', (cliArg) => { + const flags = parseFlags([`--${cliArg}`, '42']); + expect(flags.knownArgs).toEqual([`--${cliArg}`, '42']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toBe(42); + }); + + it('should override --config with second --config', () => { + const args = ['--config', '/config-1.js', '--config', '/config-2.js']; + const flags = parseFlags(args); + expect(flags.config).toBe('/config-2.js'); + }); + + describe.each(BOOLEAN_STRING_CLI_FLAGS)('boolean-string flag - %s', (cliArg: BooleanStringCLIFlag) => { + it('parses a boolean-string flag as a boolean with no arg', () => { + const args = [`--${cliArg}`]; + const flags = parseFlags(args); + expect(flags[cliArg]).toBe(true); + expect(flags.knownArgs).toEqual([`--${cliArg}`]); + }); + + it(`parses a boolean-string flag as a falsy boolean with "no" arg - --no-${cliArg}`, () => { + const args = [`--no-${cliArg}`]; + const flags = parseFlags(args); + expect(flags[cliArg]).toBe(false); + expect(flags.knownArgs).toEqual([`--no-${cliArg}`]); + }); + + it(`parses a boolean-string flag as a falsy boolean with "no" arg - --no${ + cliArg.charAt(0).toUpperCase() + cliArg.slice(1) + }`, () => { + const negativeFlag = '--no' + cliArg.charAt(0).toUpperCase() + cliArg.slice(1); + const flags = parseFlags([negativeFlag]); + expect(flags[cliArg]).toBe(false); + expect(flags.knownArgs).toEqual([negativeFlag]); + }); + + it('parses a boolean-string flag as a string with a string arg', () => { + const args = [`--${cliArg}`, 'shell']; + const flags = parseFlags(args); + expect(flags[cliArg]).toBe('shell'); + expect(flags.knownArgs).toEqual([`--${cliArg}`, 'shell']); + }); + + it('parses a boolean-string flag as a string with a string arg using equality', () => { + const args = [`--${cliArg}=shell`]; + const flags = parseFlags(args); + expect(flags[cliArg]).toBe('shell'); + expect(flags.knownArgs).toEqual([`--${cliArg}`, 'shell']); + }); + }); + + describe.each(['info', 'warn', 'error', 'debug'])('logLevel %s', (level) => { + it("should parse '--logLevel %s'", () => { + const args = ['--logLevel', level]; + const flags = parseFlags(args); + expect(flags.logLevel).toBe(level); + }); + + it('should parse --logLevel=%s', () => { + const args = [`--logLevel=${level}`]; + const flags = parseFlags(args); + expect(flags.logLevel).toBe(level); + }); + + it("should parse '--log-level %s'", () => { + const flags = parseFlags(['--log-level', level]); + expect(flags.logLevel).toBe(level); + }); + + it('should parse --log-level=%s', () => { + const flags = parseFlags([`--log-level=${level}`]); + expect(flags.logLevel).toBe(level); + }); + }); + + /** + * maxWorkers is (as of this writing) our only StringNumberCLIArg, meaning it + * may be a string (like "50%") or a number (like 4). For this reason we have + * some tests just for it. + */ + describe('maxWorkers', () => { + it.each([ + ['--maxWorkers', '4'], + ['--maxWorkers=4'], + ['--max-workers', '4'], + ['--maxWorkers', '4e+0'], + ['--maxWorkers', '40e-1'], + ])('should parse %p, %p', (...args) => { + const flags = parseFlags(args); + expect(flags.maxWorkers).toBe(4); + }); + + it('should parse --maxWorkers 4', () => { + const flags = parseFlags(['--maxWorkers', '4']); + expect(flags.maxWorkers).toBe(4); + }); + + it('should parse --maxWorkers=4', () => { + const flags = parseFlags(['--maxWorkers=4']); + expect(flags.maxWorkers).toBe(4); + }); + + it('should parse --max-workers 4', () => { + const flags = parseFlags(['--max-workers', '4']); + expect(flags.maxWorkers).toBe(4); + }); + + it('should parse --maxWorkers=50%', function () { + // see https://jestjs.io/docs/27.x/cli#--maxworkersnumstring + const flags = parseFlags(['--maxWorkers=50%']); + expect(flags.maxWorkers).toBe('50%'); + }); + + it('should parse --max-workers=1', () => { + const flags = parseFlags(['--max-workers=1']); + expect(flags.maxWorkers).toBe(1); + }); + + it('should not parse --max-workers', () => { + const flags = parseFlags([]); + expect(flags.maxWorkers).toBe(undefined); + }); + }); + + describe('aliases', () => { + describe('-p (alias for port)', () => { + it('should parse -p=4444', () => { + const flags = parseFlags(['-p=4444']); + expect(flags.port).toBe(4444); + }); + it('should parse -p 4444', () => { + const flags = parseFlags(['-p', '4444']); + expect(flags.port).toBe(4444); + }); + }); + + it('should parse -h (alias for help)', () => { + const flags = parseFlags(['-h']); + expect(flags.help).toBe(true); + }); + + it('should parse -v (alias for version)', () => { + const flags = parseFlags(['-v']); + expect(flags.version).toBe(true); + }); + + describe('-c alias for config', () => { + it('should parse -c /my-config.js', () => { + const flags = parseFlags(['-c', '/my-config.js']); + expect(flags.config).toBe('/my-config.js'); + expect(flags.knownArgs).toEqual(['--config', '/my-config.js']); + }); + + it('should parse -c=/my-config.js', () => { + const flags = parseFlags(['-c=/my-config.js']); + expect(flags.config).toBe('/my-config.js'); + expect(flags.knownArgs).toEqual(['--config', '/my-config.js']); + }); + }); + + describe('Jest aliases', () => { + it.each([ + ['w', 'maxWorkers', '4'], + ['t', 'testNamePattern', 'testname'], + ])('should support the string Jest alias %p for %p', (alias, fullArgument, value) => { + const flags = parseFlags([`-${alias}`, value]); + expect(flags.knownArgs).toEqual([`--${fullArgument}`, value]); + expect(flags.unknownArgs).toHaveLength(0); + }); + + it.each([ + ['w', 'maxWorkers', '4'], + ['t', 'testNamePattern', 'testname'], + ])('should support the string Jest alias %p for %p in an AliasEqualsArg', (alias, fullArgument, value) => { + const flags = parseFlags([`-${alias}=${value}`]); + expect(flags.knownArgs).toEqual([`--${fullArgument}`, value]); + expect(flags.unknownArgs).toHaveLength(0); + }); + + it.each<[string, keyof ConfigFlags]>([ + ['b', 'bail'], + ['e', 'expand'], + ['o', 'onlyChanged'], + ['f', 'onlyFailures'], + ['i', 'runInBand'], + ['u', 'updateSnapshot'], + ])('should support the boolean Jest alias %p for %p', (alias, fullArgument) => { + const flags = parseFlags([`-${alias}`]); + expect(flags.knownArgs).toEqual([`--${fullArgument}`]); + expect(flags[fullArgument]).toBe(true); + expect(flags.unknownArgs).toHaveLength(0); + }); + }); + }); + + it('should parse many', () => { + const args = ['-v', '--help', '-c=./myconfig.json']; + const flags = parseFlags(args); + expect(flags.version).toBe(true); + expect(flags.help).toBe(true); + expect(flags.config).toBe('./myconfig.json'); + }); + + describe('parseEqualsArg', () => { + it.each([ + ['--fooBar=baz', '--fooBar', 'baz'], + ['--foo-bar=4', '--foo-bar', '4'], + ['--fooBar=twenty=3*4', '--fooBar', 'twenty=3*4'], + ['--fooBar', '--fooBar', Empty], + ['--foo-bar', '--foo-bar', Empty], + ['--foo-bar=""', '--foo-bar', '""'], + ])('should parse %s correctly', (testArg, expectedArg, expectedValue) => { + const [arg, value] = parseEqualsArg(testArg); + expect(arg).toBe(expectedArg); + expect(value).toEqual(expectedValue); + }); + }); + + describe.each(STRING_ARRAY_CLI_FLAGS)('should parse string flag %s', (cliArg: StringArrayCLIFlag) => { + it(`should parse single value: "--${cliArg} test-value"`, () => { + const flags = parseFlags([`--${cliArg}`, 'test-value']); + expect(flags.knownArgs).toEqual([`--${cliArg}`, 'test-value']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toEqual(['test-value']); + }); + + it(`should parse multiple values: "--${cliArg} test-value"`, () => { + const flags = parseFlags([`--${cliArg}`, 'test-value', `--${cliArg}`, 'second-test-value']); + expect(flags.knownArgs).toEqual([`--${cliArg}`, 'test-value', `--${cliArg}`, 'second-test-value']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toEqual(['test-value', 'second-test-value']); + }); + + it(`should parse "--${cliArg}=value"`, () => { + const flags = parseFlags([`--${cliArg}=path/to/file.js`]); + expect(flags.knownArgs).toEqual([`--${cliArg}`, 'path/to/file.js']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toEqual(['path/to/file.js']); + }); + + it(`should parse multiple values: "--${cliArg}=test-value"`, () => { + const flags = parseFlags([`--${cliArg}=test-value`, `--${cliArg}=second-test-value`]); + expect(flags.knownArgs).toEqual([`--${cliArg}`, 'test-value', `--${cliArg}`, 'second-test-value']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toEqual(['test-value', 'second-test-value']); + }); + + it(`should parse "--${toDashCase(cliArg)} value"`, () => { + const flags = parseFlags([`--${toDashCase(cliArg)}`, 'path/to/file.js']); + expect(flags.knownArgs).toEqual([`--${toDashCase(cliArg)}`, 'path/to/file.js']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toEqual(['path/to/file.js']); + }); + + it(`should parse multiple values: "--${toDashCase(cliArg)} test-value"`, () => { + const flags = parseFlags([ + `--${toDashCase(cliArg)}`, + 'test-value', + `--${toDashCase(cliArg)}`, + 'second-test-value', + ]); + expect(flags.knownArgs).toEqual([ + `--${toDashCase(cliArg)}`, + 'test-value', + `--${toDashCase(cliArg)}`, + 'second-test-value', + ]); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toEqual(['test-value', 'second-test-value']); + }); + + it(`should parse "--${toDashCase(cliArg)}=value"`, () => { + const flags = parseFlags([`--${toDashCase(cliArg)}=path/to/file.js`]); + expect(flags.knownArgs).toEqual([`--${toDashCase(cliArg)}`, 'path/to/file.js']); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toEqual(['path/to/file.js']); + }); + + it(`should parse multiple values: "--${toDashCase(cliArg)}=test-value"`, () => { + const flags = parseFlags([`--${toDashCase(cliArg)}=test-value`, `--${toDashCase(cliArg)}=second-test-value`]); + expect(flags.knownArgs).toEqual([ + `--${toDashCase(cliArg)}`, + 'test-value', + `--${toDashCase(cliArg)}`, + 'second-test-value', + ]); + expect(flags.unknownArgs).toEqual([]); + expect(flags[cliArg]).toEqual(['test-value', 'second-test-value']); + }); + }); + + describe('error reporting', () => { + it('should throw if you pass no argument to a string flag', () => { + expect(() => { + parseFlags(['--cacheDirectory', '--someOtherFlag']); + }).toThrow('when parsing CLI flag "--cacheDirectory": expected a string argument but received nothing'); + }); + + it('should throw if you pass no argument to a string array flag', () => { + expect(() => { + parseFlags(['--reporters', '--someOtherFlag']); + }).toThrow('when parsing CLI flag "--reporters": expected a string argument but received nothing'); + }); + + it('should throw if you pass no argument to a number flag', () => { + expect(() => { + parseFlags(['--port', '--someOtherFlag']); + }).toThrow('when parsing CLI flag "--port": expected a number argument but received nothing'); + }); + + it('should throw if you pass a non-number argument to a number flag', () => { + expect(() => { + parseFlags(['--port', 'stringy']); + }).toThrow('when parsing CLI flag "--port": expected a number but received "stringy"'); + }); + + it('should throw if you pass a bad number argument to a number flag', () => { + expect(() => { + parseFlags(['--port=NaN']); + }).toThrow('when parsing CLI flag "--port": expected a number but received "NaN"'); + }); + + it('should throw if you pass no argument to a string/number flag', () => { + expect(() => { + parseFlags(['--maxWorkers']); + }).toThrow('when parsing CLI flag "--maxWorkers": expected a string or a number but received nothing'); + }); + + it('should throw if you pass an invalid log level for --logLevel', () => { + expect(() => { + parseFlags(['--logLevel', 'potato']); + }).toThrow('when parsing CLI flag "--logLevel": expected to receive a valid log level but received "potato"'); + }); + + it('should throw if you pass no argument to --logLevel', () => { + expect(() => { + parseFlags(['--logLevel']); + }).toThrow('when parsing CLI flag "--logLevel": expected to receive a valid log level but received nothing'); + }); + }); +}); diff --git a/packages/cli/src/test/run.spec.ts b/packages/cli/src/test/run.spec.ts new file mode 100644 index 00000000000..1b48e1993d0 --- /dev/null +++ b/packages/cli/src/test/run.spec.ts @@ -0,0 +1,309 @@ +import * as coreCompiler from '@stencil/core/compiler'; +import { mockCompilerSystem, mockConfig, mockLogger as createMockLogger } from '@stencil/core/testing'; + +import type * as d from '../../declarations'; +import { createTestingSystem } from '../../testing/testing-sys'; +import { createConfigFlags } from '../config-flags'; +import * as ParseFlags from '../parse-flags'; +import { run, runTask } from '../run'; +import * as BuildTask from '../task-build'; +import * as DocsTask from '../task-docs'; +import * as GenerateTask from '../task-generate'; +import * as HelpTask from '../task-help'; +import * as PrerenderTask from '../task-prerender'; +import * as ServeTask from '../task-serve'; +import * as TelemetryTask from '../task-telemetry'; +import * as TestTask from '../task-test'; + +describe('run', () => { + describe('run()', () => { + let cliInitOptions: d.CliInitOptions; + let mockLogger: d.Logger; + let mockSystem: d.CompilerSystem; + + let parseFlagsSpy: jest.SpyInstance< + ReturnType, + Parameters + >; + + beforeEach(() => { + mockLogger = createMockLogger(); + mockSystem = createTestingSystem(); + + cliInitOptions = { + args: [], + logger: mockLogger, + sys: mockSystem, + }; + + parseFlagsSpy = jest.spyOn(ParseFlags, 'parseFlags'); + parseFlagsSpy.mockReturnValue( + createConfigFlags({ + // use the 'help' task as a reasonable default for all calls to this function. + // code paths that require a different task can always override this value as needed. + task: 'help', + }), + ); + }); + + afterEach(() => { + parseFlagsSpy.mockRestore(); + }); + + describe('help task', () => { + let taskHelpSpy: jest.SpyInstance, Parameters>; + + beforeEach(() => { + taskHelpSpy = jest.spyOn(HelpTask, 'taskHelp'); + taskHelpSpy.mockReturnValue(Promise.resolve()); + }); + + afterEach(() => { + taskHelpSpy.mockRestore(); + }); + + it("calls the help task when the 'task' field is set to 'help'", async () => { + await run(cliInitOptions); + + expect(taskHelpSpy).toHaveBeenCalledTimes(1); + expect(taskHelpSpy).toHaveBeenCalledWith( + { + task: 'help', + args: [], + knownArgs: [], + unknownArgs: [], + }, + mockLogger, + mockSystem, + ); + + taskHelpSpy.mockRestore(); + }); + + it("calls the help task when the 'task' field is set to null", async () => { + parseFlagsSpy.mockReturnValue( + createConfigFlags({ + task: null, + }), + ); + + await run(cliInitOptions); + + expect(taskHelpSpy).toHaveBeenCalledTimes(1); + expect(taskHelpSpy).toHaveBeenCalledWith( + { + task: 'help', + args: [], + knownArgs: [], + unknownArgs: [], + }, + mockLogger, + mockSystem, + ); + + taskHelpSpy.mockRestore(); + }); + + it("calls the help task when the 'help' field is set on flags", async () => { + parseFlagsSpy.mockReturnValue( + createConfigFlags({ + help: true, + }), + ); + + await run(cliInitOptions); + + expect(taskHelpSpy).toHaveBeenCalledTimes(1); + expect(taskHelpSpy).toHaveBeenCalledWith( + { + task: 'help', + args: [], + unknownArgs: [], + knownArgs: [], + }, + mockLogger, + mockSystem, + ); + + taskHelpSpy.mockRestore(); + }); + }); + }); + + describe('runTask()', () => { + let sys: d.CompilerSystem; + let unvalidatedConfig: d.UnvalidatedConfig; + + let taskBuildSpy: jest.SpyInstance, Parameters>; + let taskDocsSpy: jest.SpyInstance, Parameters>; + let taskGenerateSpy: jest.SpyInstance< + ReturnType, + Parameters + >; + let taskHelpSpy: jest.SpyInstance, Parameters>; + let taskPrerenderSpy: jest.SpyInstance< + ReturnType, + Parameters + >; + let taskServeSpy: jest.SpyInstance, Parameters>; + let taskTelemetrySpy: jest.SpyInstance< + ReturnType, + Parameters + >; + let taskTestSpy: jest.SpyInstance, Parameters>; + + beforeEach(() => { + sys = mockCompilerSystem(); + sys.exit = jest.fn(); + + unvalidatedConfig = mockConfig({ outputTargets: [], sys, fsNamespace: 'testing' }); + + taskBuildSpy = jest.spyOn(BuildTask, 'taskBuild'); + taskBuildSpy.mockResolvedValue(); + + taskDocsSpy = jest.spyOn(DocsTask, 'taskDocs'); + taskDocsSpy.mockResolvedValue(); + + taskGenerateSpy = jest.spyOn(GenerateTask, 'taskGenerate'); + taskGenerateSpy.mockResolvedValue(); + + taskHelpSpy = jest.spyOn(HelpTask, 'taskHelp'); + taskHelpSpy.mockResolvedValue(); + + taskPrerenderSpy = jest.spyOn(PrerenderTask, 'taskPrerender'); + taskPrerenderSpy.mockResolvedValue(); + + taskServeSpy = jest.spyOn(ServeTask, 'taskServe'); + taskServeSpy.mockResolvedValue(); + + taskTelemetrySpy = jest.spyOn(TelemetryTask, 'taskTelemetry'); + taskTelemetrySpy.mockResolvedValue(); + + taskTestSpy = jest.spyOn(TestTask, 'taskTest'); + taskTestSpy.mockResolvedValue(); + }); + + afterEach(() => { + taskBuildSpy.mockRestore(); + taskDocsSpy.mockRestore(); + taskGenerateSpy.mockRestore(); + taskHelpSpy.mockRestore(); + taskPrerenderSpy.mockRestore(); + taskServeSpy.mockRestore(); + taskTelemetrySpy.mockRestore(); + taskTestSpy.mockRestore(); + }); + + describe('default configuration', () => { + describe('sys property', () => { + it('uses the sys argument if one is provided', async () => { + // remove the `CompilerSystem` on the config, just to be sure we don't accidentally use it + unvalidatedConfig.sys = undefined; + + await runTask(coreCompiler, unvalidatedConfig, 'build', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, { sys }); + + // first validate there was one call, and that call had two arguments + expect(taskBuildSpy).toHaveBeenCalledTimes(1); + expect(taskBuildSpy).toHaveBeenCalledWith(coreCompiler, validated.config); + + const compilerSystemUsed: d.CompilerSystem = taskBuildSpy.mock.calls[0][1].sys; + expect(compilerSystemUsed).toBe(sys); + }); + }); + }); + + it('calls the build task', async () => { + await runTask(coreCompiler, unvalidatedConfig, 'build', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); + + expect(taskBuildSpy).toHaveBeenCalledTimes(1); + expect(taskBuildSpy).toHaveBeenCalledWith(coreCompiler, validated.config); + }); + + it('calls the docs task', async () => { + await runTask(coreCompiler, unvalidatedConfig, 'docs', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); + + expect(taskDocsSpy).toHaveBeenCalledTimes(1); + expect(taskDocsSpy).toHaveBeenCalledWith(coreCompiler, validated.config); + }); + + describe('generate task', () => { + it("calls the generate task for the argument 'generate'", async () => { + await runTask(coreCompiler, unvalidatedConfig, 'generate', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); + + expect(taskGenerateSpy).toHaveBeenCalledTimes(1); + expect(taskGenerateSpy).toHaveBeenCalledWith(validated.config); + }); + + it("calls the generate task for the argument 'g'", async () => { + await runTask(coreCompiler, unvalidatedConfig, 'g', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); + + expect(taskGenerateSpy).toHaveBeenCalledTimes(1); + expect(taskGenerateSpy).toHaveBeenCalledWith(validated.config); + }); + }); + + it('calls the help task', async () => { + await runTask(coreCompiler, unvalidatedConfig, 'help', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); + + expect(taskHelpSpy).toHaveBeenCalledTimes(1); + expect(taskHelpSpy).toHaveBeenCalledWith(validated.config.flags, validated.config.logger, sys); + }); + + it('calls the prerender task', async () => { + await runTask(coreCompiler, unvalidatedConfig, 'prerender', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); + + expect(taskPrerenderSpy).toHaveBeenCalledTimes(1); + expect(taskPrerenderSpy).toHaveBeenCalledWith(coreCompiler, validated.config); + }); + + it('calls the serve task', async () => { + await runTask(coreCompiler, unvalidatedConfig, 'serve', sys); + + expect(taskServeSpy).toHaveBeenCalledTimes(1); + expect(taskServeSpy).toHaveBeenCalledWith(coreCompiler.validateConfig(unvalidatedConfig, {}).config); + }); + + describe('telemetry task', () => { + it('calls the telemetry task when a compiler system is present', async () => { + await runTask(coreCompiler, unvalidatedConfig, 'telemetry', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); + + expect(taskTelemetrySpy).toHaveBeenCalledTimes(1); + expect(taskTelemetrySpy).toHaveBeenCalledWith(validated.config.flags, sys, validated.config.logger); + }); + }); + + it('calls the test task', async () => { + await runTask(coreCompiler, unvalidatedConfig, 'test', sys); + + expect(taskTestSpy).toHaveBeenCalledTimes(1); + expect(taskTestSpy).toHaveBeenCalledWith(coreCompiler.validateConfig(unvalidatedConfig, {}).config); + }); + + it('defaults to the help task for an unaccounted for task name', async () => { + // info is a valid task name, but isn't used in the `switch` statement of `runTask` + await runTask(coreCompiler, unvalidatedConfig, 'info', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); + + expect(taskHelpSpy).toHaveBeenCalledTimes(1); + expect(taskHelpSpy).toHaveBeenCalledWith(validated.config.flags, validated.config.logger, sys); + }); + + it('defaults to the provided task if no flags exist on the provided config', async () => { + unvalidatedConfig = mockConfig({ flags: undefined, sys }); + + await runTask(coreCompiler, unvalidatedConfig, 'help', sys); + const validated = coreCompiler.validateConfig(unvalidatedConfig, {}); + + expect(taskHelpSpy).toHaveBeenCalledTimes(1); + expect(taskHelpSpy).toHaveBeenCalledWith(createConfigFlags({ task: 'help' }), validated.config.logger, sys); + }); + }); +}); diff --git a/packages/cli/src/test/task-generate.spec.ts b/packages/cli/src/test/task-generate.spec.ts new file mode 100644 index 00000000000..e5ba522de2e --- /dev/null +++ b/packages/cli/src/test/task-generate.spec.ts @@ -0,0 +1,188 @@ +import { mockCompilerSystem, mockValidatedConfig } from '@stencil/core/testing'; + +import type * as d from '../../declarations'; +import * as utils from '../../utils/validation'; +import { createConfigFlags } from '../config-flags'; +import { BoilerplateFile, getBoilerplateByExtension, taskGenerate } from '../task-generate'; + +const promptMock = jest.fn().mockResolvedValue('my-component'); + +jest.mock('prompts', () => ({ + prompt: promptMock, +})); + +let formatToPick = 'css'; + +const setup = async (plugins: any[] = []) => { + const sys = mockCompilerSystem(); + const config: d.ValidatedConfig = mockValidatedConfig({ + configPath: '/testing-path', + flags: createConfigFlags({ task: 'generate' }), + srcDir: '/src', + sys, + plugins, + }); + + // set up some mocks / spies + config.sys.exit = jest.fn(); + const errorSpy = jest.spyOn(config.logger, 'error'); + const validateTagSpy = jest.spyOn(utils, 'validateComponentTag').mockReturnValue(undefined); + + // mock prompt usage: tagName and filesToGenerate are the keys used for + // different calls, so we can cheat here and just do a single + // mockResolvedValue + let format = formatToPick; + promptMock.mockImplementation((params) => { + if (params.name === 'sassFormat') { + format = 'sass'; + return { sassFormat: 'sass' }; + } + return { + tagName: 'my-component', + filesToGenerate: [format, 'spec.tsx', 'e2e.ts'], + }; + }); + + return { config, errorSpy, validateTagSpy }; +}; + +/** + * Little test helper function which just temporarily silences + * console.log calls, so we can avoid spewing a bunch of stuff. + * @param config the user-supplied config to forward to `taskGenerate` + */ +async function silentGenerate(config: d.ValidatedConfig): Promise { + const tmp = console.log; + console.log = jest.fn(); + await taskGenerate(config); + console.log = tmp; +} + +describe('generate task', () => { + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + jest.resetModules(); + formatToPick = 'css'; + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('should exit with an error if no `configPath` is supplied', async () => { + const { config, errorSpy } = await setup(); + config.configPath = undefined; + await taskGenerate(config); + expect(config.sys.exit).toHaveBeenCalledWith(1); + expect(errorSpy).toHaveBeenCalledWith( + 'Please run this command in your root directory (i. e. the one containing stencil.config.ts).', + ); + }); + + it('should exit with an error if no `srcDir` is supplied', async () => { + const { config, errorSpy } = await setup(); + config.srcDir = undefined; + await taskGenerate(config); + expect(config.sys.exit).toHaveBeenCalledWith(1); + expect(errorSpy).toHaveBeenCalledWith("Stencil's srcDir was not specified."); + }); + + it('should exit with an error if the component name does not validate', async () => { + const { config, errorSpy, validateTagSpy } = await setup(); + validateTagSpy.mockReturnValue('error error error'); + await taskGenerate(config); + expect(config.sys.exit).toHaveBeenCalledWith(1); + expect(errorSpy).toHaveBeenCalledWith('error error error'); + }); + + it.each([true, false])('should create a directory for the generated components', async (includeTests) => { + const { config } = await setup(); + if (!includeTests) { + promptMock.mockResolvedValue({ + tagName: 'my-component', + // simulate the user picking only the css option + filesToGenerate: ['css'], + }); + } + + const createDirSpy = jest.spyOn(config.sys, 'createDir'); + await silentGenerate(config); + expect(createDirSpy).toHaveBeenCalledWith( + includeTests ? `${config.srcDir}/components/my-component/test` : `${config.srcDir}/components/my-component`, + { recursive: true }, + ); + }); + + it('should generate the files the user picked', async () => { + const { config } = await setup(); + const writeFileSpy = jest.spyOn(config.sys, 'writeFile'); + await silentGenerate(config); + const userChoices: ReadonlyArray = [ + { extension: 'tsx', path: '/src/components/my-component/my-component.tsx' }, + { extension: 'css', path: '/src/components/my-component/my-component.css' }, + { extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' }, + { extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' }, + ]; + + userChoices.forEach((file) => { + expect(writeFileSpy).toHaveBeenCalledWith( + file.path, + getBoilerplateByExtension('my-component', file.extension, true, 'css'), + ); + }); + }); + + it('should error without writing anything if a to-be-generated file is already present', async () => { + const { config, errorSpy } = await setup(); + jest.spyOn(config.sys, 'readFile').mockResolvedValue('some file contents'); + await silentGenerate(config); + expect(errorSpy).toHaveBeenCalledWith( + 'Generating code would overwrite the following files:', + '\t/src/components/my-component/my-component.tsx', + '\t/src/components/my-component/my-component.css', + '\t/src/components/my-component/test/my-component.spec.tsx', + '\t/src/components/my-component/test/my-component.e2e.ts', + ); + expect(config.sys.exit).toHaveBeenCalledWith(1); + }); + + it('should generate files for sass projects', async () => { + const { config } = await setup([{ name: 'sass' }]); + const writeFileSpy = jest.spyOn(config.sys, 'writeFile'); + await silentGenerate(config); + const userChoices: ReadonlyArray = [ + { extension: 'tsx', path: '/src/components/my-component/my-component.tsx' }, + { extension: 'sass', path: '/src/components/my-component/my-component.sass' }, + { extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' }, + { extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' }, + ]; + + userChoices.forEach((file) => { + expect(writeFileSpy).toHaveBeenCalledWith( + file.path, + getBoilerplateByExtension('my-component', file.extension, true, 'sass'), + ); + }); + }); + + it('should generate files for less projects', async () => { + formatToPick = 'less'; + const { config } = await setup([{ name: 'less' }]); + const writeFileSpy = jest.spyOn(config.sys, 'writeFile'); + await silentGenerate(config); + const userChoices: ReadonlyArray = [ + { extension: 'tsx', path: '/src/components/my-component/my-component.tsx' }, + { extension: 'less', path: '/src/components/my-component/my-component.less' }, + { extension: 'spec.tsx', path: '/src/components/my-component/test/my-component.spec.tsx' }, + { extension: 'e2e.ts', path: '/src/components/my-component/test/my-component.e2e.ts' }, + ]; + + userChoices.forEach((file) => { + expect(writeFileSpy).toHaveBeenCalledWith( + file.path, + getBoilerplateByExtension('my-component', file.extension, true, 'less'), + ); + }); + }); +}); diff --git a/packages/cli/vite.config.ts b/packages/cli/vite.config.ts new file mode 100644 index 00000000000..7574f0e6037 --- /dev/null +++ b/packages/cli/vite.config.ts @@ -0,0 +1,56 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +/** + * Vite config for @stencil/cli + * + * Source is in packages/cli/src/ + */ +export default defineConfig({ + build: { + ssr: true, + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: '@stencil/cli', + formats: ['es'], + fileName: () => 'index.js', + }, + outDir: 'dist', + emptyOutDir: true, + sourcemap: !!process.env.DEBUG, + target: 'node18', + rollupOptions: { + external: [ + /^node:/, + '@stencil/core', + '@stencil/core/compiler', + '@stencil/core/testing', // Being removed in v5 + '@stencil/core/dev-server', // Being replaced by Vite in v5 + '@stencil/mock-doc', + 'typescript', + 'prompts', + ], + output: { + preserveModules: false, + }, + }, + }, + resolve: { + alias: { + // CLI uses utils/declarations from core + '@utils': resolve(__dirname, '../core/src/utils'), + '@app-data': resolve(__dirname, '../core/src/app-data'), + '@app-globals': resolve(__dirname, '../core/src/app-globals'), + '@platform': resolve(__dirname, '../core/src/client'), + '@runtime': resolve(__dirname, '../core/src/runtime'), + '@stencil/core/declarations': resolve(__dirname, '../core/src/declarations'), + // Relative imports from old src/ structure + '../declarations': resolve(__dirname, '../core/src/declarations'), + '../../declarations': resolve(__dirname, '../core/src/declarations'), + '../compiler': resolve(__dirname, '../core/src/compiler'), + '../../compiler': resolve(__dirname, '../core/src/compiler'), + '../version': resolve(__dirname, '../core/src/version'), + '../../version': resolve(__dirname, '../core/src/version'), + }, + }, +}); diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000000..3478fb44863 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,28 @@ +{ + "name": "@stencil/core", + "version": "5.0.0-alpha.0", + "description": "Compiler for web components and progressive web apps", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./internal": "./dist/internal/index.js", + "./internal/client": "./dist/internal/client/index.js", + "./internal/server": "./dist/internal/server/index.js", + "./internal/app-data": "./dist/internal/app-data/index.js", + "./internal/app-globals": "./dist/internal/app-globals/index.js" + }, + "files": [ + "dist/" + ], + "dependencies": { + "@stencil/mock-doc": "workspace:*", + "typescript": "~5.8.3", + "terser": "5.37.0", + "parse5": "7.2.1" + } +} diff --git a/packages/core/src/app-data/index.ts b/packages/core/src/app-data/index.ts new file mode 100644 index 00000000000..682e97095ba --- /dev/null +++ b/packages/core/src/app-data/index.ts @@ -0,0 +1,109 @@ +import type { BuildConditionals } from '@stencil/core/internal'; + +/** + * A collection of default build flags for a Stencil project. + * + * This collection can be found throughout the Stencil codebase, often imported from the `@app-data` module like so: + * ```ts + * import { BUILD } from '@app-data'; + * ``` + * and is used to determine if a portion of the output of a Stencil _project_'s compilation step can be eliminated. + * + * e.g. When `BUILD.allRenderFn` evaluates to `false`, the compiler will eliminate conditional statements like: + * ```ts + * if (BUILD.allRenderFn) { + * // some code that will be eliminated if BUILD.allRenderFn is false + * } + * ``` + * + * `@app-data`, the module that `BUILD` is imported from, is an alias for the `@stencil/core/internal/app-data`, and is + * partially referenced by {@link STENCIL_APP_DATA_ID}. The `src/compiler/bundle/app-data-plugin.ts` references + * `STENCIL_APP_DATA_ID` uses it to replace these defaults with {@link BuildConditionals} that are derived from a + * Stencil project's contents (i.e. metadata from the components). This replacement happens at a Stencil project's + * compile time. Such code can be found at `src/compiler/app-core/app-data.ts`. + */ +export const BUILD: BuildConditionals = { + allRenderFn: false, + element: true, + event: true, + hasRenderFn: true, + hostListener: true, + hostListenerTargetWindow: true, + hostListenerTargetDocument: true, + hostListenerTargetBody: true, + hostListenerTargetParent: false, + hostListenerTarget: true, + member: true, + method: true, + mode: true, + observeAttribute: true, + prop: true, + propMutable: true, + reflect: true, + scoped: true, + shadowDom: true, + slot: true, + cssAnnotations: true, + state: true, + style: true, + formAssociated: false, + svg: true, + updatable: true, + vdomAttribute: true, + vdomXlink: true, + vdomClass: true, + vdomFunctional: true, + vdomKey: true, + vdomListener: true, + vdomRef: true, + vdomPropOrAttr: true, + vdomRender: true, + vdomStyle: true, + vdomText: true, + propChangeCallback: true, + taskQueue: true, + hotModuleReplacement: false, + isDebug: false, + isDev: false, + isTesting: false, + hydrateServerSide: false, + hydrateClientSide: false, + lifecycleDOMEvents: false, + lazyLoad: false, + profile: false, + slotRelocation: true, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + appendChildSlotFix: false, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + cloneNodeFix: false, + hydratedAttribute: false, + hydratedClass: true, + // TODO(STENCIL-1305): remove this option + scriptDataOpts: false, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + scopedSlotTextContentFix: false, + // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field + shadowDomShim: false, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + slotChildNodesFix: false, + invisiblePrehydration: true, + propBoolean: true, + propNumber: true, + propString: true, + constructableCSS: true, + devTools: false, + shadowDelegatesFocus: true, + shadowSlotAssignmentManual: false, + initializeNextTick: false, + asyncLoading: true, + asyncQueue: false, + // TODO: deprecated in favour of `setTagTransformer` and `transformTag`. Remove in 5.0 + transformTagName: false, + attachStyles: true, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + experimentalSlotFixes: false, +}; + +export const Env = {}; + +export const NAMESPACE = /* default */ 'app' as string; diff --git a/packages/core/src/app-globals/index.ts b/packages/core/src/app-globals/index.ts new file mode 100644 index 00000000000..7defb1dab58 --- /dev/null +++ b/packages/core/src/app-globals/index.ts @@ -0,0 +1,5 @@ +export const globalScripts = /* default */ () => { + /**/ +}; + +export const globalStyles = /* default */ ''; diff --git a/packages/core/src/cli/config-flags.ts b/packages/core/src/cli/config-flags.ts new file mode 100644 index 00000000000..01ba12bddeb --- /dev/null +++ b/packages/core/src/cli/config-flags.ts @@ -0,0 +1,353 @@ +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', + 'e2e', + 'es5', + 'esm', + 'help', + 'log', + 'open', + 'prerender', + 'prerenderExternal', + 'prod', + 'profile', + 'serviceWorker', + 'screenshot', + 'serve', + 'skipNodeCheck', + 'spec', + 'ssr', + 'updateScreenshot', + 'verbose', + 'version', + 'watch', + + // 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', + // 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', + 'screenshotConnector', + + // 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; + +export const STRING_ARRAY_CLI_FLAGS = [ + 'collectCoverageOnlyFrom', + 'coveragePathIgnorePatterns', + 'coverageReporters', + 'moduleDirectories', + 'moduleFileExtensions', + 'modulePathIgnorePatterns', + 'modulePaths', + 'projects', + 'reporters', + 'roots', + 'selectProjects', + 'setupFiles', + 'setupFilesAfterEnv', + 'snapshotSerializers', + 'testMatch', + 'testPathIgnorePatterns', + 'testPathPattern', + 'testRegex', + 'transformIgnorePatterns', + 'unmockedModulePathPatterns', + 'watchPathIgnorePatterns', +] as const; + +/** + * All the CLI arguments which may have string or number values + * + * `maxWorkers` is an argument which is used both by Stencil _and_ by Jest, + * which means that we need to support parsing both string and number values. + */ +export const STRING_NUMBER_CLI_FLAGS = ['maxWorkers'] as const; + +/** + * All the CLI arguments which may have boolean or string values. + */ +export const BOOLEAN_STRING_CLI_FLAGS = [ + /** + * `headless` is an argument passed through to Puppeteer (which is passed to Chrome) for end-to-end testing. + * + * {@see https://developer.chrome.com/blog/chrome-headless-shell/} + */ + 'headless', + /** + * `stats` is an argument that can optionally accept a file path where stats should be written. + * When used as a boolean (--stats), it defaults to 'stencil-stats.json'. + * When used with a path (--stats dist/stats.json), it writes to that path. + */ + 'stats', +] as const; + +/** + * All the LogLevel-type options supported by the Stencil CLI + * + * This is a bit silly since there's only one such argument atm, + * but this approach lets us make sure that we're handling all + * our arguments in a type-safe way. + */ +export const LOG_LEVEL_CLI_FLAGS = ['logLevel'] as const; + +/** + * A type which gives the members of a `ReadonlyArray` as + * an enum-like type which can be used for e.g. keys in a `Record` + * (as in the `AliasMap` type below) + */ +type ArrayValuesAsUnion> = T[number]; + +export type BooleanCLIFlag = ArrayValuesAsUnion; +export type StringCLIFlag = ArrayValuesAsUnion; +export type StringArrayCLIFlag = ArrayValuesAsUnion; +export type NumberCLIFlag = ArrayValuesAsUnion; +export type StringNumberCLIFlag = ArrayValuesAsUnion; +export type BooleanStringCLIFlag = ArrayValuesAsUnion; +export type LogCLIFlag = ArrayValuesAsUnion; + +export type KnownCLIFlag = + | BooleanCLIFlag + | StringCLIFlag + | StringArrayCLIFlag + | NumberCLIFlag + | StringNumberCLIFlag + | BooleanStringCLIFlag + | LogCLIFlag; + +type AliasMap = Partial>; + +/** + * For a small subset of CLI options we support a short alias e.g. `'h'` for `'help'` + */ +export const CLI_FLAG_ALIASES: AliasMap = { + c: 'config', + h: 'help', + p: 'port', + v: 'version', + + // JEST SPECIFIC CLI FLAGS + // these are defined in + // https://github.com/facebook/jest/blob/4156f86/packages/jest-cli/src/args.ts + b: 'bail', + e: 'expand', + f: 'onlyFailures', + i: 'runInBand', + o: 'onlyChanged', + t: 'testNamePattern', + u: 'updateSnapshot', + w: 'maxWorkers', +}; + +/** + * A regular expression which can be used to match a CLI flag for one of our + * short aliases. + */ +export const CLI_FLAG_REGEX = new RegExp(`^-[chpvbewofitu]{1}$`); + +/** + * Given two types `K` and `T` where `K` extends `ReadonlyArray`, + * construct a type which maps the strings in `K` as keys to values of type `T`. + * + * Because we use types derived this way to construct an interface (`ConfigFlags`) + * for which we want optional keys, we make all the properties optional (w/ `'?'`) + * and possibly null. + */ +type ObjectFromKeys, T> = { + [key in K[number]]?: T | null; +}; + +/** + * Type containing the possible Boolean configuration flags, to be included + * in ConfigFlags, below + */ +type BooleanConfigFlags = ObjectFromKeys; + +/** + * Type containing the possible String configuration flags, to be included + * in ConfigFlags, below + */ +type StringConfigFlags = ObjectFromKeys; + +/** + * Type containing the possible String Array configuration flags. This is + * one of the 'constituent types' for `ConfigFlags`. + */ +type StringArrayConfigFlags = ObjectFromKeys; + +/** + * Type containing the possible numeric configuration flags, to be included + * in ConfigFlags, below + */ +type NumberConfigFlags = ObjectFromKeys; + +/** + * Type containing the configuration flags which may be set to either string + * or number values. + */ +type StringNumberConfigFlags = ObjectFromKeys; + +/** + * Type containing the configuration flags which may be set to either string + * or boolean values. + */ +type BooleanStringConfigFlags = ObjectFromKeys; + +/** + * Type containing the possible LogLevel configuration flags, to be included + * in ConfigFlags, below + */ +type LogLevelFlags = ObjectFromKeys; + +/** + * The configuration flags which can be set by the user on the command line. + * This interface captures both known arguments (which are enumerated and then + * parsed according to their types) and unknown arguments which the user may + * pass at the CLI. + * + * Note that this interface is constructed by extending `BooleanConfigFlags`, + * `StringConfigFlags`, etc. These types are in turn constructed from types + * extending `ReadonlyArray` which we declare in another module. This + * allows us to record our known CLI arguments in one place, using a + * `ReadonlyArray` to get both a type-level representation of what CLI + * options we support and a runtime list of strings which can be used to match + * on actual flags passed by the user. + */ +export interface ConfigFlags + extends BooleanConfigFlags, + StringConfigFlags, + StringArrayConfigFlags, + NumberConfigFlags, + StringNumberConfigFlags, + BooleanStringConfigFlags, + LogLevelFlags { + task: TaskCommand | null; + args: string[]; + knownArgs: string[]; + unknownArgs: string[]; +} + +/** + * Helper function for initializing a `ConfigFlags` object. Provide any overrides + * for default values and off you go! + * + * @param init an object with any overrides for default values + * @returns a complete CLI flag object + */ +export const createConfigFlags = (init: Partial = {}): ConfigFlags => { + const flags: ConfigFlags = { + task: null, + args: [], + knownArgs: [], + unknownArgs: [], + ...init, + }; + + return flags; +}; diff --git a/packages/core/src/client/client-build.ts b/packages/core/src/client/client-build.ts new file mode 100644 index 00000000000..ebf6b46481c --- /dev/null +++ b/packages/core/src/client/client-build.ts @@ -0,0 +1,10 @@ +import { BUILD } from '@app-data'; + +import type * as d from '../declarations'; + +export const Build: d.UserBuildConditionals = { + isDev: BUILD.isDev ? true : false, + isBrowser: true, + isServer: false, + isTesting: BUILD.isTesting ? true : false, +}; diff --git a/packages/core/src/client/client-host-ref.ts b/packages/core/src/client/client-host-ref.ts new file mode 100644 index 00000000000..5b3e013c8d6 --- /dev/null +++ b/packages/core/src/client/client-host-ref.ts @@ -0,0 +1,80 @@ +import { BUILD } from '@app-data'; +import { CMP_FLAGS } from '@utils/constants'; +import { reWireGetterSetter } from '@utils/es2022-rewire-class-members'; + +import type * as d from '../declarations'; + +/** + * Given a {@link d.RuntimeRef} retrieve the corresponding {@link d.HostRef} + * + * @param ref the runtime ref of interest + * @returns the Host reference (if found) or undefined + */ +export const getHostRef = (ref: d.RuntimeRef): d.HostRef | undefined => { + if (ref.__stencil__getHostRef) { + return ref.__stencil__getHostRef(); + } + + return undefined; +}; + +/** + * Register a lazy instance with the {@link hostRefs} object so it's + * corresponding {@link d.HostRef} can be retrieved later. + * + * @param lazyInstance the lazy instance of interest + * @param hostRef that instances `HostRef` object + */ +export const registerInstance = (lazyInstance: any, hostRef: d.HostRef) => { + if (!hostRef) return; + lazyInstance.__stencil__getHostRef = () => hostRef; + hostRef.$lazyInstance$ = lazyInstance; + + if (hostRef.$cmpMeta$.$flags$ & CMP_FLAGS.hasModernPropertyDecls && (BUILD.state || BUILD.prop)) { + reWireGetterSetter(lazyInstance, hostRef); + } +}; + +/** + * Register a host element for a Stencil component, setting up various metadata + * and callbacks based on {@link BUILD} flags as well as the component's runtime + * metadata. + * + * @param hostElement the host element to register + * @param cmpMeta runtime metadata for that component + * @returns a reference to the host ref WeakMap + */ +export const registerHost = (hostElement: d.HostElement, cmpMeta: d.ComponentRuntimeMeta) => { + const hostRef: d.HostRef = { + $flags$: 0, + $hostElement$: hostElement, + $cmpMeta$: cmpMeta, + $instanceValues$: new Map(), + $serializerValues$: new Map(), + }; + if (BUILD.isDev) { + hostRef.$renderCount$ = 0; + } + if (BUILD.method && BUILD.lazyLoad) { + hostRef.$onInstancePromise$ = new Promise((r) => (hostRef.$onInstanceResolve$ = r)); + } + if (BUILD.asyncLoading) { + hostRef.$onReadyPromise$ = new Promise((r) => (hostRef.$onReadyResolve$ = r)); + hostElement['s-p'] = []; + hostElement['s-rc'] = []; + } + if (BUILD.lazyLoad) { + hostRef.$fetchedCbList$ = []; + } + + const ref = hostRef; + hostElement.__stencil__getHostRef = () => ref; + + if (!BUILD.lazyLoad && cmpMeta.$flags$ & CMP_FLAGS.hasModernPropertyDecls && (BUILD.state || BUILD.prop)) { + reWireGetterSetter(hostElement, hostRef); + } + + return ref; +}; + +export const isMemberInElement = (elm: any, memberName: string) => memberName in elm; diff --git a/packages/core/src/client/client-load-module.ts b/packages/core/src/client/client-load-module.ts new file mode 100644 index 00000000000..63651bda846 --- /dev/null +++ b/packages/core/src/client/client-load-module.ts @@ -0,0 +1,66 @@ +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$); + }, + ); +}; diff --git a/packages/core/src/client/client-log.ts b/packages/core/src/client/client-log.ts new file mode 100644 index 00000000000..9d2dc76a3ef --- /dev/null +++ b/packages/core/src/client/client-log.ts @@ -0,0 +1,22 @@ +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); diff --git a/packages/core/src/client/client-patch-browser.ts b/packages/core/src/client/client-patch-browser.ts new file mode 100644 index 00000000000..e5e813faace --- /dev/null +++ b/packages/core/src/client/client-patch-browser.ts @@ -0,0 +1,54 @@ +import { BUILD, NAMESPACE } from '@app-data'; +import { consoleDevInfo, H, promiseResolve, win } from '@platform'; + +import type * as d from '../declarations'; + +export const patchBrowser = (): Promise => { + // NOTE!! This fn cannot use async/await! + if (BUILD.isDev && !BUILD.isTesting) { + consoleDevInfo('Running in development mode.'); + } + + if (BUILD.cloneNodeFix) { + // opted-in to polyfill cloneNode() for slot polyfilled components + patchCloneNodeFix((H as any).prototype); + } + + const scriptElm = BUILD.scriptDataOpts + ? win.document && + Array.from(win.document.querySelectorAll('script')).find( + (s) => + new RegExp(`\/${NAMESPACE}(\\.esm)?\\.js($|\\?|#)`).test(s.src) || + s.getAttribute('data-stencil-namespace') === NAMESPACE, + ) + : null; + const importMeta = import.meta.url; + const opts = BUILD.scriptDataOpts ? ((scriptElm as any) || {})['data-opts'] || {} : {}; + + if (importMeta !== '') { + opts.resourcesUrl = new URL('.', importMeta).href; + } + + return promiseResolve(opts); +}; + +const patchCloneNodeFix = (HTMLElementPrototype: any) => { + const nativeCloneNodeFn = HTMLElementPrototype.cloneNode; + + HTMLElementPrototype.cloneNode = function (this: Node, deep: boolean) { + if (this.nodeName === 'TEMPLATE') { + return nativeCloneNodeFn.call(this, deep); + } + const clonedNode = nativeCloneNodeFn.call(this, false); + const srcChildNodes = this.childNodes; + if (deep) { + for (let i = 0; i < srcChildNodes.length; i++) { + // Node.ATTRIBUTE_NODE === 2, and checking because IE11 + if (srcChildNodes[i].nodeType !== 2) { + clonedNode.appendChild(srcChildNodes[i].cloneNode(true)); + } + } + } + return clonedNode; + }; +}; diff --git a/packages/core/src/client/client-style.ts b/packages/core/src/client/client-style.ts new file mode 100644 index 00000000000..75065613a9f --- /dev/null +++ b/packages/core/src/client/client-style.ts @@ -0,0 +1,6 @@ +import type * as d from '../declarations'; + +export const styles: d.StyleMap = /*@__PURE__*/ new Map(); +export const modeResolutionChain: d.ResolutionHandler[] = []; +export const setScopedSSR = (_opts: d.HydrateFactoryOptions) => {}; +export const needsScopedSSR = () => false; diff --git a/packages/core/src/client/client-task-queue.ts b/packages/core/src/client/client-task-queue.ts new file mode 100644 index 00000000000..d90641bcbbe --- /dev/null +++ b/packages/core/src/client/client-task-queue.ts @@ -0,0 +1,103 @@ +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); diff --git a/packages/core/src/client/client-window.ts b/packages/core/src/client/client-window.ts new file mode 100644 index 00000000000..2a5258e8b92 --- /dev/null +++ b/packages/core/src/client/client-window.ts @@ -0,0 +1,72 @@ +import { BUILD } from '@app-data'; + +import type * as d from '../declarations'; + +interface StencilWindow extends Omit { + document?: Document; +} + +export const win = (typeof window !== 'undefined' ? window : ({} as StencilWindow)) as StencilWindow; + +export const H = ((win as any).HTMLElement || (class {} as any)) as HTMLElement; + +export const plt: d.PlatformRuntime = { + $flags$: 0, + $resourcesUrl$: '', + jmp: (h) => h(), + raf: (h) => requestAnimationFrame(h), + ael: (el, eventName, listener, opts) => el.addEventListener(eventName, listener, opts), + rel: (el, eventName, listener, opts) => el.removeEventListener(eventName, listener, opts), + ce: (eventName, opts) => new CustomEvent(eventName, opts), +}; + +export const setPlatformHelpers = (helpers: { + jmp?: (c: any) => any; + raf?: (c: any) => number; + ael?: (el: any, eventName: string, listener: any, options: any) => void; + rel?: (el: any, eventName: string, listener: any, options: any) => void; + ce?: (eventName: string, opts?: any) => any; +}) => { + Object.assign(plt, helpers); +}; + +export const supportsShadow = BUILD.shadowDom; + +export const supportsListenerOptions = /*@__PURE__*/ (() => { + let supportsListenerOptions = false; + try { + win.document?.addEventListener( + 'e', + null, + Object.defineProperty({}, 'passive', { + get() { + supportsListenerOptions = true; + }, + }), + ); + } catch (e) {} + return supportsListenerOptions; +})(); + +export const promiseResolve = (v?: any) => Promise.resolve(v); + +export const supportsConstructableStylesheets = BUILD.constructableCSS + ? /*@__PURE__*/ (() => { + try { + if (!win.document.adoptedStyleSheets) { + return false; + } + new CSSStyleSheet(); + return typeof new CSSStyleSheet().replaceSync === 'function'; + } catch (e) {} + return false; + })() + : false; + +// https://github.com/salesforce/lwc/blob/5af18fdd904bc6cfcf7b76f3c539490ff11515b2/packages/%40lwc/engine-dom/src/renderer.ts#L41-L43 +export const supportsMutableAdoptedStyleSheets = supportsConstructableStylesheets + ? /*@__PURE__*/ (() => + !!win.document && Object.getOwnPropertyDescriptor(win.document.adoptedStyleSheets, 'length')!.writable)() + : false; + +export { H as HTMLElement }; diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts new file mode 100644 index 00000000000..372348bde66 --- /dev/null +++ b/packages/core/src/client/index.ts @@ -0,0 +1,9 @@ +export * from './client-build'; +export * from './client-host-ref'; +export * from './client-load-module'; +export * from './client-log'; +export * from './client-style'; +export * from './client-task-queue'; +export * from './client-window'; +export { BUILD, Env, NAMESPACE } from '@app-data'; +export * from '@runtime'; diff --git a/packages/core/src/client/polyfills/core-js.js b/packages/core/src/client/polyfills/core-js.js new file mode 100755 index 00000000000..7bbc909ed48 --- /dev/null +++ b/packages/core/src/client/polyfills/core-js.js @@ -0,0 +1,11 @@ +/** + * core-js 3.6.5 + * https://github.com/zloirock/core-js + * License: http://rock.mit-license.org + * © 2019 Denis Pushkarev (zloirock.ru) + */ +!function(t){"use strict";!function(t){var n={};function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}e.m=t,e.c=n,e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:r})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,n){if(1&n&&(t=e(t)),8&n)return t;if(4&n&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(e.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&n&&"string"!=typeof t)for(var o in t)e.d(r,o,function(n){return t[n]}.bind(null,o));return r},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},e.p="",e(e.s=0)}([function(t,n,e){e(1),e(55),e(62),e(68),e(70),e(71),e(72),e(73),e(75),e(76),e(78),e(87),e(88),e(89),e(98),e(99),e(101),e(102),e(103),e(105),e(106),e(107),e(108),e(110),e(111),e(112),e(113),e(114),e(115),e(116),e(117),e(118),e(127),e(130),e(131),e(133),e(135),e(136),e(137),e(138),e(139),e(141),e(143),e(146),e(148),e(150),e(151),e(153),e(154),e(155),e(156),e(157),e(159),e(160),e(162),e(163),e(164),e(165),e(166),e(167),e(168),e(169),e(170),e(172),e(173),e(183),e(184),e(185),e(189),e(191),e(192),e(193),e(194),e(195),e(196),e(198),e(201),e(202),e(203),e(204),e(208),e(209),e(212),e(213),e(214),e(215),e(216),e(217),e(218),e(219),e(221),e(222),e(223),e(226),e(227),e(228),e(229),e(230),e(231),e(232),e(233),e(234),e(235),e(236),e(237),e(238),e(240),e(241),e(243),e(248),t.exports=e(246)},function(t,n,e){var r=e(2),o=e(6),i=e(45),a=e(14),u=e(46),c=e(39),f=e(47),s=e(48),l=e(52),p=e(49),h=e(53),v=p("isConcatSpreadable"),g=h>=51||!o((function(){var t=[];return t[v]=!1,t.concat()[0]!==t})),d=l("concat"),y=function(t){if(!a(t))return!1;var n=t[v];return void 0!==n?!!n:i(t)};r({target:"Array",proto:!0,forced:!g||!d},{concat:function(t){var n,e,r,o,i,a=u(this),l=s(a,0),p=0;for(n=-1,r=arguments.length;n9007199254740991)throw TypeError("Maximum allowed index exceeded");for(e=0;e=9007199254740991)throw TypeError("Maximum allowed index exceeded");f(l,p++,i)}return l.length=p,l}})},function(t,n,e){var r=e(3),o=e(4).f,i=e(18),a=e(21),u=e(22),c=e(32),f=e(44);t.exports=function(t,n){var e,s,l,p,h,v=t.target,g=t.global,d=t.stat;if(e=g?r:d?r[v]||u(v,{}):(r[v]||{}).prototype)for(s in n){if(p=n[s],l=t.noTargetGet?(h=o(e,s))&&h.value:e[s],!f(g?s:v+(d?".":"#")+s,t.forced)&&void 0!==l){if(typeof p==typeof l)continue;c(p,l)}(t.sham||l&&l.sham)&&i(p,"sham",!0),a(e,s,p,t)}}},function(t,n){var e=function(t){return t&&t.Math==Math&&t};t.exports=e("object"==typeof globalThis&&globalThis)||e("object"==typeof window&&window)||e("object"==typeof self&&self)||e("object"==typeof global&&global)||Function("return this")()},function(t,n,e){var r=e(5),o=e(7),i=e(8),a=e(9),u=e(13),c=e(15),f=e(16),s=Object.getOwnPropertyDescriptor;n.f=r?s:function(t,n){if(t=a(t),n=u(n,!0),f)try{return s(t,n)}catch(t){}if(c(t,n))return i(!o.f.call(t,n),t[n])}},function(t,n,e){var r=e(6);t.exports=!r((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},function(t,n){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,n,e){var r={}.propertyIsEnumerable,o=Object.getOwnPropertyDescriptor,i=o&&!r.call({1:2},1);n.f=i?function(t){var n=o(this,t);return!!n&&n.enumerable}:r},function(t,n){t.exports=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}}},function(t,n,e){var r=e(10),o=e(12);t.exports=function(t){return r(o(t))}},function(t,n,e){var r=e(6),o=e(11),i="".split;t.exports=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==o(t)?i.call(t,""):Object(t)}:Object},function(t,n){var e={}.toString;t.exports=function(t){return e.call(t).slice(8,-1)}},function(t,n){t.exports=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t}},function(t,n,e){var r=e(14);t.exports=function(t,n){if(!r(t))return t;var e,o;if(n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;if("function"==typeof(e=t.valueOf)&&!r(o=e.call(t)))return o;if(!n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,n){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,n){var e={}.hasOwnProperty;t.exports=function(t,n){return e.call(t,n)}},function(t,n,e){var r=e(5),o=e(6),i=e(17);t.exports=!r&&!o((function(){return 7!=Object.defineProperty(i("div"),"a",{get:function(){return 7}}).a}))},function(t,n,e){var r=e(3),o=e(14),i=r.document,a=o(i)&&o(i.createElement);t.exports=function(t){return a?i.createElement(t):{}}},function(t,n,e){var r=e(5),o=e(19),i=e(8);t.exports=r?function(t,n,e){return o.f(t,n,i(1,e))}:function(t,n,e){return t[n]=e,t}},function(t,n,e){var r=e(5),o=e(16),i=e(20),a=e(13),u=Object.defineProperty;n.f=r?u:function(t,n,e){if(i(t),n=a(n,!0),i(e),o)try{return u(t,n,e)}catch(t){}if("get"in e||"set"in e)throw TypeError("Accessors not supported");return"value"in e&&(t[n]=e.value),t}},function(t,n,e){var r=e(14);t.exports=function(t){if(!r(t))throw TypeError(String(t)+" is not an object");return t}},function(t,n,e){var r=e(3),o=e(18),i=e(15),a=e(22),u=e(23),c=e(25),f=c.get,s=c.enforce,l=String(String).split("String");(t.exports=function(t,n,e,u){var c=!!u&&!!u.unsafe,f=!!u&&!!u.enumerable,p=!!u&&!!u.noTargetGet;"function"==typeof e&&("string"!=typeof n||i(e,"name")||o(e,"name",n),s(e).source=l.join("string"==typeof n?n:"")),t!==r?(c?!p&&t[n]&&(f=!0):delete t[n],f?t[n]=e:o(t,n,e)):f?t[n]=e:a(n,e)})(Function.prototype,"toString",(function(){return"function"==typeof this&&f(this).source||u(this)}))},function(t,n,e){var r=e(3),o=e(18);t.exports=function(t,n){try{o(r,t,n)}catch(e){r[t]=n}return n}},function(t,n,e){var r=e(24),o=Function.toString;"function"!=typeof r.inspectSource&&(r.inspectSource=function(t){return o.call(t)}),t.exports=r.inspectSource},function(t,n,e){var r=e(3),o=e(22),i=r["__core-js_shared__"]||o("__core-js_shared__",{});t.exports=i},function(t,n,e){var r,o,i,a=e(26),u=e(3),c=e(14),f=e(18),s=e(15),l=e(27),p=e(31),h=u.WeakMap;if(a){var v=new h,g=v.get,d=v.has,y=v.set;r=function(t,n){return y.call(v,t,n),n},o=function(t){return g.call(v,t)||{}},i=function(t){return d.call(v,t)}}else{var x=l("state");p[x]=!0,r=function(t,n){return f(t,x,n),n},o=function(t){return s(t,x)?t[x]:{}},i=function(t){return s(t,x)}}t.exports={set:r,get:o,has:i,enforce:function(t){return i(t)?o(t):r(t,{})},getterFor:function(t){return function(n){var e;if(!c(n)||(e=o(n)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return e}}}},function(t,n,e){var r=e(3),o=e(23),i=r.WeakMap;t.exports="function"==typeof i&&/native code/.test(o(i))},function(t,n,e){var r=e(28),o=e(30),i=r("keys");t.exports=function(t){return i[t]||(i[t]=o(t))}},function(t,n,e){var r=e(29),o=e(24);(t.exports=function(t,n){return o[t]||(o[t]=void 0!==n?n:{})})("versions",[]).push({version:"3.6.5",mode:r?"pure":"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})},function(t,n){t.exports=!1},function(t,n){var e=0,r=Math.random();t.exports=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++e+r).toString(36)}},function(t,n){t.exports={}},function(t,n,e){var r=e(15),o=e(33),i=e(4),a=e(19);t.exports=function(t,n){for(var e=o(n),u=a.f,c=i.f,f=0;fc;)r(u,e=n[c++])&&(~i(f,e)||f.push(e));return f}},function(t,n,e){var r=e(9),o=e(39),i=e(41),a=function(t){return function(n,e,a){var u,c=r(n),f=o(c.length),s=i(a,f);if(t&&e!=e){for(;f>s;)if((u=c[s++])!=u)return!0}else for(;f>s;s++)if((t||s in c)&&c[s]===e)return t||s||0;return!t&&-1}};t.exports={includes:a(!0),indexOf:a(!1)}},function(t,n,e){var r=e(40),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},function(t,n){var e=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:e)(t)}},function(t,n,e){var r=e(40),o=Math.max,i=Math.min;t.exports=function(t,n){var e=r(t);return e<0?o(e+n,0):i(e,n)}},function(t,n){t.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},function(t,n){n.f=Object.getOwnPropertySymbols},function(t,n,e){var r=e(6),o=/#|\.prototype\./,i=function(t,n){var e=u[a(t)];return e==f||e!=c&&("function"==typeof n?r(n):!!n)},a=i.normalize=function(t){return String(t).replace(o,".").toLowerCase()},u=i.data={},c=i.NATIVE="N",f=i.POLYFILL="P";t.exports=i},function(t,n,e){var r=e(11);t.exports=Array.isArray||function(t){return"Array"==r(t)}},function(t,n,e){var r=e(12);t.exports=function(t){return Object(r(t))}},function(t,n,e){var r=e(13),o=e(19),i=e(8);t.exports=function(t,n,e){var a=r(n);a in t?o.f(t,a,i(0,e)):t[a]=e}},function(t,n,e){var r=e(14),o=e(45),i=e(49)("species");t.exports=function(t,n){var e;return o(t)&&("function"!=typeof(e=t.constructor)||e!==Array&&!o(e.prototype)?r(e)&&null===(e=e[i])&&(e=void 0):e=void 0),new(void 0===e?Array:e)(0===n?0:n)}},function(t,n,e){var r=e(3),o=e(28),i=e(15),a=e(30),u=e(50),c=e(51),f=o("wks"),s=r.Symbol,l=c?s:s&&s.withoutSetter||a;t.exports=function(t){return i(f,t)||(u&&i(s,t)?f[t]=s[t]:f[t]=l("Symbol."+t)),f[t]}},function(t,n,e){var r=e(6);t.exports=!!Object.getOwnPropertySymbols&&!r((function(){return!String(Symbol())}))},function(t,n,e){var r=e(50);t.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},function(t,n,e){var r=e(6),o=e(49),i=e(53),a=o("species");t.exports=function(t){return i>=51||!r((function(){var n=[];return(n.constructor={})[a]=function(){return{foo:1}},1!==n[t](Boolean).foo}))}},function(t,n,e){var r,o,i=e(3),a=e(54),u=i.process,c=u&&u.versions,f=c&&c.v8;f?o=(r=f.split("."))[0]+r[1]:a&&(!(r=a.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=a.match(/Chrome\/(\d+)/))&&(o=r[1]),t.exports=o&&+o},function(t,n,e){var r=e(34);t.exports=r("navigator","userAgent")||""},function(t,n,e){var r=e(2),o=e(56),i=e(57);r({target:"Array",proto:!0},{copyWithin:o}),i("copyWithin")},function(t,n,e){var r=e(46),o=e(41),i=e(39),a=Math.min;t.exports=[].copyWithin||function(t,n){var e=r(this),u=i(e.length),c=o(t,u),f=o(n,u),s=arguments.length>2?arguments[2]:void 0,l=a((void 0===s?u:o(s,u))-f,u-c),p=1;for(f0;)f in e?e[c]=e[f]:delete e[c],c+=p,f+=p;return e}},function(t,n,e){var r=e(49),o=e(58),i=e(19),a=r("unscopables"),u=Array.prototype;null==u[a]&&i.f(u,a,{configurable:!0,value:o(null)}),t.exports=function(t){u[a][t]=!0}},function(t,n,e){var r,o=e(20),i=e(59),a=e(42),u=e(31),c=e(61),f=e(17),s=e(27),l=s("IE_PROTO"),p=function(){},h=function(t){return" +`)} + + +

AFTER:

+

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

+
+  ${escapeHtml(`type="module" src="/build/${
+    config.fsNamespace
+  }.esm.js"${escapeHtml(`>`)}
+  ${escapeHtml(`nomodule ${escapeHtml(
+    `src="/build/${config.fsNamespace}.js">`,
+  )}
+    
+ `; + return `${generatePreamble(config)} +(function() { + function checkSupport() { + if (!document.body) { + setTimeout(checkSupport); + return; + } + function supportsDynamicImports() { + try { + new Function('import("")'); + return true; + } catch (e) {} + return false; + } + var supportsEsModules = !!('noModule' in document.createElement('script')); + + if (!supportsEsModules) { + document.body.innerHTML = '${inlineHTML(htmlLegacy)}'; + + document.getElementById('current-browser-output').textContent = window.navigator.userAgent; + document.getElementById('es-modules-test').textContent = supportsEsModules; + document.getElementById('es-dynamic-modules-test').textContent = supportsDynamicImports(); + document.getElementById('shadow-dom-test').textContent = !!(document.head.attachShadow); + document.getElementById('custom-elements-test').textContent = !!(window.customElements); + document.getElementById('css-variables-test').textContent = !!(window.CSS && window.CSS.supports && window.CSS.supports('color', 'var(--c)')); + document.getElementById('fetch-test').textContent = !!(window.fetch); + } else { + document.body.innerHTML = '${inlineHTML(htmlUpdate)}'; + } + } + + setTimeout(checkSupport); +})();`; +}; + +const inlineHTML = (html: string) => { + return html.replace(/\n/g, '\\n').replace(/\'/g, `\\'`).trim(); +}; diff --git a/packages/core/src/compiler/app-core/app-polyfills.ts b/packages/core/src/compiler/app-core/app-polyfills.ts new file mode 100644 index 00000000000..efa49f5d07e --- /dev/null +++ b/packages/core/src/compiler/app-core/app-polyfills.ts @@ -0,0 +1,36 @@ +import { join } from '@utils'; + +import type * as d from '../../declarations'; + +export const getClientPolyfill = async ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + polyfillFile: string, +) => { + const polyfillFilePath = join( + config.sys.getCompilerExecutingPath(), + '..', + '..', + 'internal', + 'client', + 'polyfills', + polyfillFile, + ); + return compilerCtx.fs.readFile(polyfillFilePath); +}; + +export const getAppBrowserCorePolyfills = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx) => { + // read all the polyfill content, in this particular order + const polyfills = INLINE_POLYFILLS.slice(); + + const results = await Promise.all( + polyfills.map((polyfillFile) => getClientPolyfill(config, compilerCtx, polyfillFile)), + ); + + // concat the polyfills + return results.join('\n').trim(); +}; + +// order of the polyfills matters!! test test test +// actual source of the polyfills are found in /src/client/polyfills/ +const INLINE_POLYFILLS = ['core-js.js', 'dom.js', 'es5-html-element.js', 'system.js']; diff --git a/packages/core/src/compiler/app-core/bundle-app-core.ts b/packages/core/src/compiler/app-core/bundle-app-core.ts new file mode 100644 index 00000000000..5939499c6a5 --- /dev/null +++ b/packages/core/src/compiler/app-core/bundle-app-core.ts @@ -0,0 +1,57 @@ +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, + }; + } + }); +}; diff --git a/packages/core/src/compiler/build/build-ctx.ts b/packages/core/src/compiler/build/build-ctx.ts new file mode 100644 index 00000000000..dcd5c28e453 --- /dev/null +++ b/packages/core/src/compiler/build/build-ctx.ts @@ -0,0 +1,239 @@ +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: {}, +}; diff --git a/packages/core/src/compiler/build/build-finish.ts b/packages/core/src/compiler/build/build-finish.ts new file mode 100644 index 00000000000..58a44123df3 --- /dev/null +++ b/packages/core/src/compiler/build/build-finish.ts @@ -0,0 +1,195 @@ +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); + } + }); +}; diff --git a/packages/core/src/compiler/build/build-hmr.ts b/packages/core/src/compiler/build/build-hmr.ts new file mode 100644 index 00000000000..50f0d6002b5 --- /dev/null +++ b/packages/core/src/compiler/build/build-hmr.ts @@ -0,0 +1,248 @@ +import { isGlob, isOutputTargetWww, normalizePath, sortBy } from '@utils'; +import { minimatch } from 'minimatch'; +import { basename } from 'path'; + +import type * as d from '../../declarations'; +import { getScopeId } from '../style/scope-css'; + +export const generateHmr = (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { + if (config.devServer?.reloadStrategy == null) { + return null; + } + + const hmr: d.HotModuleReplacement = { + reloadStrategy: config.devServer.reloadStrategy, + versionId: Date.now().toString().substring(6) + '' + Math.round(Math.random() * 89999 + 10000), + }; + + if (buildCtx.scriptsAdded.length > 0) { + hmr.scriptsAdded = buildCtx.scriptsAdded.slice(); + } + + if (buildCtx.scriptsDeleted.length > 0) { + hmr.scriptsDeleted = buildCtx.scriptsDeleted.slice(); + } + + const excludeHmr = excludeHmrFiles(config, config.devServer.excludeHmr, buildCtx.filesChanged); + if (excludeHmr.length > 0) { + hmr.excludeHmr = excludeHmr.slice(); + } + + if (buildCtx.hasHtmlChanges) { + hmr.indexHtmlUpdated = true; + } + + if (buildCtx.hasServiceWorkerChanges) { + hmr.serviceWorkerUpdated = true; + } + + const outputTargetsWww = config.outputTargets.filter(isOutputTargetWww); + + const componentsUpdated = getComponentsUpdated(compilerCtx, buildCtx); + if (componentsUpdated) { + hmr.componentsUpdated = componentsUpdated; + } + + if (Object.keys(buildCtx.stylesUpdated).length > 0) { + hmr.inlineStylesUpdated = sortBy( + buildCtx.stylesUpdated.map((s) => { + return { + styleId: getScopeId(s.styleTag, s.styleMode), + styleTag: s.styleTag, + styleText: s.styleText, + } as d.HmrStyleUpdate; + }), + (s) => s.styleId, + ); + } + + const externalStylesUpdated = getExternalStylesUpdated(buildCtx, outputTargetsWww); + if (externalStylesUpdated) { + hmr.externalStylesUpdated = externalStylesUpdated; + } + + const externalImagesUpdated = getImagesUpdated(buildCtx, outputTargetsWww); + if (externalImagesUpdated) { + hmr.imagesUpdated = externalImagesUpdated; + } + + return hmr; +}; + +const getComponentsUpdated = (compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { + // find all of the components that would be affected from the file changes + if (!buildCtx.filesChanged) { + return null; + } + + const filesToLookForImporters = buildCtx.filesChanged.filter((f) => { + return f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.js') || f.endsWith('.jsx'); + }); + + if (filesToLookForImporters.length === 0) { + return null; + } + + const changedScriptFiles: string[] = []; + const checkedFiles = new Set(); + const allModuleFiles = buildCtx.moduleFiles.filter((m) => m.localImports && m.localImports.length > 0); + + while (filesToLookForImporters.length > 0) { + const scriptFile = filesToLookForImporters.shift(); + addTsFileImporters(allModuleFiles, filesToLookForImporters, checkedFiles, changedScriptFiles, scriptFile); + } + + const tags = changedScriptFiles.reduce((tags, changedTsFile) => { + const moduleFile = compilerCtx.moduleMap.get(changedTsFile); + if (moduleFile != null) { + moduleFile.cmps.forEach((cmp) => { + if (typeof cmp.tagName === 'string') { + if (!tags.includes(cmp.tagName)) { + tags.push(cmp.tagName); + } + } + }); + } + return tags; + }, [] as string[]); + + if (tags.length === 0) { + return null; + } + + return tags.sort(); +}; + +const addTsFileImporters = ( + allModuleFiles: d.Module[], + filesToLookForImporters: string[], + checkedFiles: Set, + changedScriptFiles: string[], + scriptFile: string, +) => { + if (!changedScriptFiles.includes(scriptFile)) { + // add it to our list of files to transpile + changedScriptFiles.push(scriptFile); + } + + if (checkedFiles.has(scriptFile)) { + // already checked this file + return; + } + checkedFiles.add(scriptFile); + + // get all the ts files that import this ts file + const tsFilesThatImportsThisTsFile = allModuleFiles.reduce((arr, moduleFile) => { + moduleFile.localImports.forEach((localImport) => { + let checkFile = localImport; + + if (checkFile === scriptFile) { + arr.push(moduleFile.sourceFilePath); + return; + } + + checkFile = localImport + '.tsx'; + if (checkFile === scriptFile) { + arr.push(moduleFile.sourceFilePath); + return; + } + + checkFile = localImport + '.ts'; + if (checkFile === scriptFile) { + arr.push(moduleFile.sourceFilePath); + return; + } + + checkFile = localImport + '.js'; + if (checkFile === scriptFile) { + arr.push(moduleFile.sourceFilePath); + return; + } + }); + return arr; + }, [] as string[]); + + // add all the files that import this ts file to the list of ts files we need to look through + tsFilesThatImportsThisTsFile.forEach((tsFileThatImportsThisTsFile) => { + // if we add to this array, then the while look will keep working until it's empty + filesToLookForImporters.push(tsFileThatImportsThisTsFile); + }); +}; + +const getExternalStylesUpdated = (buildCtx: d.BuildCtx, outputTargetsWww: d.OutputTargetWww[]) => { + if (!buildCtx.isRebuild || outputTargetsWww.length === 0) { + return null; + } + + const cssFiles = buildCtx.filesWritten.filter((f) => f.endsWith('.css')); + if (cssFiles.length === 0) { + return null; + } + + return cssFiles.map((cssFile) => basename(cssFile)).sort(); +}; + +const getImagesUpdated = (buildCtx: d.BuildCtx, outputTargetsWww: d.OutputTargetWww[]) => { + if (outputTargetsWww.length === 0) { + return null; + } + + const imageFiles = buildCtx.filesChanged.reduce((arr, filePath) => { + if (IMAGE_EXT.some((ext) => filePath.toLowerCase().endsWith(ext))) { + const fileName = basename(filePath); + if (!arr.includes(fileName)) { + arr.push(fileName); + } + } + return arr; + }, []); + + if (imageFiles.length === 0) { + return null; + } + + return imageFiles.sort(); +}; + +/** + * Determine a list of files (if any) which should be excluded from HMR updates. + * + * @param config a user-supplied config + * @param excludeHmr a list of glob patterns that should be used to determine + * whether to exclude a file or not (a file will be excluded if it matches one + * @param filesChanged an array of files which are changed in the HMR update + * currently under consideration + * @returns a sorted list of files to exclude + */ +const excludeHmrFiles = (config: d.Config, excludeHmr: string[], filesChanged: string[]): string[] => { + const excludeFiles: string[] = []; + + if (!excludeHmr || excludeHmr.length === 0) { + return excludeFiles; + } + + excludeHmr.forEach((excludeHmr) => { + return filesChanged + .map((fileChanged) => { + let shouldExclude = false; + + if (isGlob(excludeHmr)) { + shouldExclude = minimatch(fileChanged, excludeHmr); + } else { + shouldExclude = normalizePath(excludeHmr) === normalizePath(fileChanged); + } + + if (shouldExclude) { + config.logger.debug(`excludeHmr: ${fileChanged}`); + excludeFiles.push(basename(fileChanged)); + } + + return shouldExclude; + }) + .some((r) => r); + }); + + return excludeFiles.sort(); +}; + +const IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg']; diff --git a/packages/core/src/compiler/build/build-results.ts b/packages/core/src/compiler/build/build-results.ts new file mode 100644 index 00000000000..cc60d280171 --- /dev/null +++ b/packages/core/src/compiler/build/build-results.ts @@ -0,0 +1,43 @@ +import { fromEntries, hasError, isString, normalizeDiagnostics } from '@utils'; + +import type * as d from '../../declarations'; +import { getBuildTimestamp } from './build-ctx'; +import { generateHmr } from './build-hmr'; + +export const generateBuildResults = (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { + const componentGraph = buildCtx.componentGraph ? fromEntries(buildCtx.componentGraph.entries()) : undefined; + + const buildResults: d.CompilerBuildResults = { + buildId: buildCtx.buildId, + diagnostics: normalizeDiagnostics(compilerCtx, buildCtx.diagnostics), + dirsAdded: buildCtx.dirsAdded.slice().sort(), + dirsDeleted: buildCtx.dirsDeleted.slice().sort(), + duration: Date.now() - buildCtx.startTime, + filesAdded: buildCtx.filesAdded.slice().sort(), + filesChanged: buildCtx.filesChanged.slice().sort(), + filesDeleted: buildCtx.filesDeleted.slice().sort(), + filesUpdated: buildCtx.filesUpdated.slice().sort(), + hasError: hasError(buildCtx.diagnostics), + hasSuccessfulBuild: compilerCtx.hasSuccessfulBuild, + isRebuild: buildCtx.isRebuild, + namespace: config.namespace, + outputs: compilerCtx.fs.getBuildOutputs(), + rootDir: config.rootDir, + srcDir: config.srcDir, + timestamp: getBuildTimestamp(), + componentGraph, + }; + + const hmr = generateHmr(config, compilerCtx, buildCtx); + if (hmr != null) { + buildResults.hmr = hmr; + } + + if (isString(buildCtx.hydrateAppFilePath)) { + buildResults.hydrateAppFilePath = buildCtx.hydrateAppFilePath; + } + + compilerCtx.lastBuildResults = Object.assign({}, buildResults as any); + + return buildResults; +}; diff --git a/packages/core/src/compiler/build/build-stats.ts b/packages/core/src/compiler/build/build-stats.ts new file mode 100644 index 00000000000..20865fd3d94 --- /dev/null +++ b/packages/core/src/compiler/build/build-stats.ts @@ -0,0 +1,187 @@ +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)); +} diff --git a/packages/core/src/compiler/build/build.ts b/packages/core/src/compiler/build/build.ts new file mode 100644 index 00000000000..fad449f947d --- /dev/null +++ b/packages/core/src/compiler/build/build.ts @@ -0,0 +1,80 @@ +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 componentDtsChanged = await validateTypesAfterGeneration( + config, + compilerCtx, + buildCtx, + tsBuilder, + emittedDts, + ); + if (buildCtx.hasError) return buildAbort(buildCtx); + + if (config.watch && componentDtsChanged) { + // silent abort for watch mode only + return null; + } + + // preprocess and generate styles before any outputTarget starts + buildCtx.stylesPromise = generateGlobalStyles(config, compilerCtx, buildCtx); + if (buildCtx.hasError) return buildAbort(buildCtx); + + // create outputs + await generateOutputTargets(config, compilerCtx, buildCtx); + if (buildCtx.hasError) return buildAbort(buildCtx); + + // write outputs + await buildCtx.stylesPromise; + await writeBuild(config, compilerCtx, buildCtx); + } catch (e: any) { + // ¯\_(ツ)_/¯ + catchError(buildCtx.diagnostics, e); + } + + // TODO + // clear changed files + compilerCtx.changedFiles.clear(); + + // return what we've learned today + return buildFinish(buildCtx); +}; diff --git a/packages/core/src/compiler/build/compiler-ctx.ts b/packages/core/src/compiler/build/compiler-ctx.ts new file mode 100644 index 00000000000..c5d9206b513 --- /dev/null +++ b/packages/core/src/compiler/build/compiler-ctx.ts @@ -0,0 +1,161 @@ +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, + 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; +}; diff --git a/packages/core/src/compiler/build/full-build.ts b/packages/core/src/compiler/build/full-build.ts new file mode 100644 index 00000000000..7a2ec27ebd2 --- /dev/null +++ b/packages/core/src/compiler/build/full-build.ts @@ -0,0 +1,50 @@ +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); + } + }; + + createTsBuildProgram(config, onBuild).then((program) => { + tsWatchProgram = program; + }); + }); +}; diff --git a/packages/core/src/compiler/build/test/build-stats.spec.ts b/packages/core/src/compiler/build/test/build-stats.spec.ts new file mode 100644 index 00000000000..8047ac93282 --- /dev/null +++ b/packages/core/src/compiler/build/test/build-stats.spec.ts @@ -0,0 +1,71 @@ +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], + }); + }); +}); diff --git a/packages/core/src/compiler/build/test/write-export-maps.spec.ts b/packages/core/src/compiler/build/test/write-export-maps.spec.ts new file mode 100644 index 00000000000..17c49e91435 --- /dev/null +++ b/packages/core/src/compiler/build/test/write-export-maps.spec.ts @@ -0,0 +1,143 @@ +import { mockBuildCtx, mockValidatedConfig } from '@stencil/core/testing'; +import childProcess from 'child_process'; + +import * as d from '../../../declarations'; +import { stubComponentCompilerMeta } from '../../types/tests/ComponentCompilerMeta.stub'; +import { writeExportMaps } from '../write-export-maps'; + +describe('writeExportMaps', () => { + let config: d.ValidatedConfig; + let buildCtx: d.BuildCtx; + let execSyncSpy: jest.SpyInstance; + + beforeEach(() => { + config = mockValidatedConfig(); + buildCtx = mockBuildCtx(config); + + execSyncSpy = jest.spyOn(childProcess, 'execSync').mockImplementation(() => ''); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should not generate any exports if there are no output targets', () => { + writeExportMaps(config, buildCtx); + + expect(execSyncSpy).toHaveBeenCalledTimes(0); + }); + + it('should generate the default exports for the lazy build if present', () => { + config.outputTargets = [ + { + type: 'dist', + dir: '/dist', + typesDir: '/dist/types', + }, + ]; + + writeExportMaps(config, buildCtx); + + expect(execSyncSpy).toHaveBeenCalledTimes(3); + expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][import]"="./dist/index.js"`); + expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][require]"="./dist/index.cjs.js"`); + expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][types]"="./dist/types/index.d.ts"`); + }); + + it('should generate the default exports for the custom elements build if present', () => { + config.outputTargets = [ + { + type: 'dist-custom-elements', + dir: '/dist/components', + generateTypeDeclarations: true, + }, + ]; + + writeExportMaps(config, buildCtx); + + expect(execSyncSpy).toHaveBeenCalledTimes(2); + expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][import]"="./dist/components/index.js"`); + expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[.][types]"="./dist/components/index.d.ts"`); + }); + + it('should generate the lazy loader exports if the output target is present', () => { + config.rootDir = '/'; + config.outputTargets.push({ + type: 'dist-lazy-loader', + dir: '/dist/lazy-loader', + empty: true, + esmDir: '/dist/esm', + cjsDir: '/dist/cjs', + componentDts: '/dist/components.d.ts', + }); + + writeExportMaps(config, buildCtx); + + expect(execSyncSpy).toHaveBeenCalledTimes(3); + expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[./loader][import]"="./dist/lazy-loader/index.js"`); + expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[./loader][require]"="./dist/lazy-loader/index.cjs"`); + expect(execSyncSpy).toHaveBeenCalledWith(`npm pkg set "exports[./loader][types]"="./dist/lazy-loader/index.d.ts"`); + }); + + it('should generate the custom elements exports if the output target is present', () => { + config.rootDir = '/'; + config.outputTargets.push({ + type: 'dist-custom-elements', + dir: '/dist/components', + generateTypeDeclarations: true, + }); + + buildCtx.components = [ + stubComponentCompilerMeta({ + tagName: 'my-component', + componentClassName: 'MyComponent', + }), + ]; + + writeExportMaps(config, buildCtx); + + expect(execSyncSpy).toHaveBeenCalledTimes(4); + expect(execSyncSpy).toHaveBeenCalledWith( + `npm pkg set "exports[./my-component][import]"="./dist/components/my-component.js"`, + ); + expect(execSyncSpy).toHaveBeenCalledWith( + `npm pkg set "exports[./my-component][types]"="./dist/components/my-component.d.ts"`, + ); + }); + + it('should generate the custom elements exports for multiple components', () => { + config.rootDir = '/'; + config.outputTargets.push({ + type: 'dist-custom-elements', + dir: '/dist/components', + generateTypeDeclarations: true, + }); + + buildCtx.components = [ + stubComponentCompilerMeta({ + tagName: 'my-component', + componentClassName: 'MyComponent', + }), + stubComponentCompilerMeta({ + tagName: 'my-other-component', + componentClassName: 'MyOtherComponent', + }), + ]; + + writeExportMaps(config, buildCtx); + + expect(execSyncSpy).toHaveBeenCalledTimes(6); + expect(execSyncSpy).toHaveBeenCalledWith( + `npm pkg set "exports[./my-component][import]"="./dist/components/my-component.js"`, + ); + expect(execSyncSpy).toHaveBeenCalledWith( + `npm pkg set "exports[./my-component][types]"="./dist/components/my-component.d.ts"`, + ); + expect(execSyncSpy).toHaveBeenCalledWith( + `npm pkg set "exports[./my-other-component][import]"="./dist/components/my-other-component.js"`, + ); + expect(execSyncSpy).toHaveBeenCalledWith( + `npm pkg set "exports[./my-other-component][types]"="./dist/components/my-other-component.d.ts"`, + ); + }); +}); diff --git a/packages/core/src/compiler/build/validate-files.ts b/packages/core/src/compiler/build/validate-files.ts new file mode 100644 index 00000000000..fc729155354 --- /dev/null +++ b/packages/core/src/compiler/build/validate-files.ts @@ -0,0 +1,26 @@ +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), + ]); +}; diff --git a/packages/core/src/compiler/build/watch-build.ts b/packages/core/src/compiler/build/watch-build.ts new file mode 100644 index 00000000000..3e5d9ee3b27 --- /dev/null +++ b/packages/core/src/compiler/build/watch-build.ts @@ -0,0 +1,409 @@ +import { isString, resolve } from '@utils'; +import { dirname } from 'path'; +import type ts from 'typescript'; + +import type * as d from '../../declarations'; +import { compilerRequest } from '../bundle/dev-module'; +import { + filesChanged, + hasHtmlChanges, + hasScriptChanges, + hasStyleChanges, + isWatchIgnorePath, + scriptsAdded, + scriptsDeleted, +} from '../fs-watch/fs-watch-rebuild'; +import { hasServiceWorkerChanges } from '../service-worker/generate-sw'; +import { createTsWatchProgram } from '../transpile/create-watch-program'; +import { build } from './build'; +import { BuildContext } from './build-ctx'; + +/** + * This method contains context and functionality for a TS watch build. This is called via + * the compiler when running a build in watch mode (i.e. `stencil build --watch`). + * + * In essence, this method tracks all files that change while the program is running to trigger + * a rebuild of a Stencil project using a {@link ts.EmitAndSemanticDiagnosticsBuilderProgram}. + * + * @param config The validated config for the Stencil project + * @param compilerCtx The compiler context for the project + * @returns An object containing helper methods for the dev-server's watch program + */ +export const createWatchBuild = async ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, +): Promise => { + let isRebuild = false; + let tsWatchProgram: { + program: ts.WatchOfConfigFile; + rebuild: () => void; + }; + let closeResolver: Function; + const watchWaiter = new Promise((resolve) => (closeResolver = resolve)); + + const dirsAdded = new Set(); + const dirsDeleted = new Set(); + const filesAdded = new Set(); + const filesUpdated = new Set(); + const filesDeleted = new Set(); + + /** + * A callback function that is invoked to trigger a rebuild of a Stencil project. This will + * update the build context with the associated file changes (these are used downstream to trigger + * HMR) and then calls the `build()` function to execute the Stencil build. + * + * @param tsBuilder A {@link ts.BuilderProgram} to be passed to the `build()` function. + */ + const onBuild = async (tsBuilder: ts.BuilderProgram) => { + const buildCtx = new BuildContext(config, compilerCtx); + buildCtx.isRebuild = isRebuild; + buildCtx.requiresFullBuild = !isRebuild; + buildCtx.dirsAdded = Array.from(dirsAdded.keys()).sort(); + buildCtx.dirsDeleted = Array.from(dirsDeleted.keys()).sort(); + buildCtx.filesAdded = Array.from(filesAdded.keys()).sort(); + buildCtx.filesUpdated = Array.from(filesUpdated.keys()).sort(); + buildCtx.filesDeleted = Array.from(filesDeleted.keys()).sort(); + buildCtx.filesChanged = filesChanged(buildCtx); + buildCtx.scriptsAdded = scriptsAdded(buildCtx); + buildCtx.scriptsDeleted = scriptsDeleted(buildCtx); + buildCtx.hasScriptChanges = hasScriptChanges(buildCtx); + buildCtx.hasStyleChanges = hasStyleChanges(buildCtx); + buildCtx.hasHtmlChanges = hasHtmlChanges(config, buildCtx); + buildCtx.hasServiceWorkerChanges = hasServiceWorkerChanges(config, buildCtx); + + if (config.flags.debug) { + config.logger.debug(`WATCH_BUILD::watchBuild::onBuild filesAdded: ${formatFilesForDebug(buildCtx.filesAdded)}`); + config.logger.debug( + `WATCH_BUILD::watchBuild::onBuild filesDeleted: ${formatFilesForDebug(buildCtx.filesDeleted)}`, + ); + config.logger.debug( + `WATCH_BUILD::watchBuild::onBuild filesUpdated: ${formatFilesForDebug(buildCtx.filesUpdated)}`, + ); + config.logger.debug( + `WATCH_BUILD::watchBuild::onBuild filesWritten: ${formatFilesForDebug(buildCtx.filesWritten)}`, + ); + } + + // Make sure all files in the module map are still in the fs + // Otherwise, we can run into build errors because the compiler can think + // there are two component files with the same tag name + Array.from(compilerCtx.moduleMap.keys()).forEach((key) => { + if (filesUpdated.has(key) || filesDeleted.has(key)) { + // Check if the file exists in the fs + const fileExists = compilerCtx.fs.accessSync(key); + if (!fileExists) { + compilerCtx.moduleMap.delete(key); + } + } + }); + + // Make sure all added/updated files are watched + // We need to check both added/updates since the TS watch program behaves kinda weird + // and doesn't always handle file renames the same way + new Set([...filesUpdated, ...filesAdded]).forEach((filePath) => { + compilerCtx.addWatchFile(filePath); + }); + + dirsAdded.clear(); + dirsDeleted.clear(); + filesAdded.clear(); + filesUpdated.clear(); + filesDeleted.clear(); + + emitFsChange(compilerCtx, buildCtx); + + buildCtx.start(); + + // Rebuild the project + const result = await build(config, compilerCtx, buildCtx, tsBuilder); + + if (result && !result.hasError) { + isRebuild = true; + } + }; + + /** + * Utility method for formatting a debug message that must either list a number of files, or the word 'none' if the + * provided list is empty + * + * @param files a list of files, the list may be empty + * @returns the provided list if it is not empty. otherwise, return the word 'none' + */ + const formatFilesForDebug = (files: ReadonlyArray): string => { + /** + * In the created message, it's important that there's no whitespace prior to the file name. + * Stencil's logger will split messages by whitespace according to the width of the terminal window. + * Since file names can be fully qualified paths (and therefore quite long), putting whitespace between a '-' and + * the path can lead to formatted messages where the '-' is on its own line + */ + return files.length > 0 ? files.map((filename: string) => `-${filename}`).join('\n') : 'none'; + }; + + /** + * Utility method to start/construct the watch program. This will mark + * all relevant files to be watched and then call a method to build the TS + * program responsible for building the project. + * + * @returns A promise of the result of creating the watch program. + */ + const start = async () => { + /** + * Stencil watches the following directories for changes: + */ + await Promise.all([ + /** + * the `srcDir` directory, e.g. component files + */ + watchFiles(compilerCtx, config.srcDir), + /** + * the root directory, e.g. `stencil.config.ts` + */ + watchFiles(compilerCtx, config.rootDir, { + recursive: false, + }), + /** + * the external directories, defined in `watchExternalDirs`, e.g. `node_modules` + */ + ...(config.watchExternalDirs || []).map((dir) => watchFiles(compilerCtx, dir)), + ]); + + tsWatchProgram = await createTsWatchProgram(config, onBuild); + return watchWaiter; + }; + + /** + * A map of absolute directory paths and their associated {@link d.CompilerFileWatcher} (which contains + * the ability to teardown the watcher for the specific directory) + */ + const watchingDirs = new Map(); + /** + * A map of absolute file paths and their associated {@link d.CompilerFileWatcher} (which contains + * the ability to teardown the watcher for the specific file) + */ + const watchingFiles = new Map(); + + /** + * Callback method that will execute whenever TS alerts us that a file change + * has occurred. This will update the appropriate set with the file path based on the + * type of change, and then will kick off a rebuild of the project. + * + * @param filePath The absolute path to the file in the Stencil project + * @param eventKind The type of file change that occurred (update, add, delete) + */ + const onFsChange: d.CompilerFileWatcherCallback = (filePath, eventKind) => { + if (tsWatchProgram && !isWatchIgnorePath(config, filePath)) { + updateCompilerCtxCache(config, compilerCtx, filePath, eventKind); + + switch (eventKind) { + case 'dirAdd': + dirsAdded.add(filePath); + break; + case 'dirDelete': + dirsDeleted.add(filePath); + break; + case 'fileAdd': + filesAdded.add(filePath); + break; + case 'fileUpdate': + filesUpdated.add(filePath); + break; + case 'fileDelete': + filesDeleted.add(filePath); + break; + } + + config.logger.debug( + `WATCH_BUILD::fs_event_change - type=${eventKind}, path=${filePath}, time=${new Date().getTime()}`, + ); + + // Trigger a rebuild of the project + tsWatchProgram.rebuild(); + } + }; + + /** + * Callback method that will execute when TS alerts us that a directory modification has occurred. + * This will just call the `onFsChange()` callback method with the same arguments. + * + * @param filePath The absolute path to the file in the Stencil project + * @param eventKind The type of file change that occurred (update, add, delete) + */ + const onDirChange: d.CompilerFileWatcherCallback = (filePath, eventKind) => { + if (eventKind != null) { + onFsChange(filePath, eventKind); + } + }; + + /** + * Utility method to teardown the TS watch program and close/clear all watched files. + * + * @returns An object with the `exitCode` status of the teardown. + */ + const close = async () => { + watchingDirs.forEach((w) => w.close()); + watchingFiles.forEach((w) => w.close()); + watchingDirs.clear(); + watchingFiles.clear(); + + if (tsWatchProgram) { + tsWatchProgram.program.close(); + tsWatchProgram = null; + } + + const watcherCloseResults: d.WatcherCloseResults = { + exitCode: 0, + }; + closeResolver(watcherCloseResults); + return watcherCloseResults; + }; + + const request = async (data: d.CompilerRequest) => compilerRequest(config, compilerCtx, data); + + // Add a definition to the `compilerCtx` for `addWatchFile` + // This method will add the specified file path to the watched files collection and instruct + // the `CompilerSystem` what to do when a file change occurs (the `onFsChange()` callback) + compilerCtx.addWatchFile = (filePath) => { + if (isString(filePath) && !watchingFiles.has(filePath) && !isWatchIgnorePath(config, filePath)) { + watchingFiles.set(filePath, config.sys.watchFile(filePath, onFsChange)); + } + }; + + // Add a definition to the `compilerCtx` for `addWatchDir` + // This method will add the specified file path to the watched directories collection and instruct + // the `CompilerSystem` what to do when a directory change occurs (the `onDirChange()` callback) + compilerCtx.addWatchDir = (dirPath, recursive) => { + if (isString(dirPath) && !watchingDirs.has(dirPath) && !isWatchIgnorePath(config, dirPath)) { + watchingDirs.set(dirPath, config.sys.watchDirectory(dirPath, onDirChange, recursive)); + } + }; + + // When the compiler system destroys, we need to also destroy this watch program + config.sys.addDestroy(close); + + return { + start, + close, + on: compilerCtx.events.on, + request, + }; +}; + +/** + * A list of directories that are excluded from being watched for changes. + */ +const EXCLUDE_DIRS = ['.cache', '.git', '.github', '.stencil', '.vscode', 'node_modules']; + +/** + * A list of file extensions that are excluded from being watched for changes. + */ +const EXCLUDE_EXTENSIONS = [ + '.md', + '.markdown', + '.txt', + '.spec.ts', + '.spec.tsx', + '.e2e.ts', + '.e2e.tsx', + '.gitignore', + '.editorconfig', +]; + +/** + * Marks all root files of a Stencil project to be watched for changes. Whenever + * one of these files is determined as changed (according to TS), a rebuild of the project will execute. + * + * @param compilerCtx The compiler context for the Stencil project + * @param dir The directory to watch for changes + * @param options The options to watch files in the directory + * @param options.recursive Whether to watch files recursively + * @param options.excludeDirNames A list of directories to exclude from being watched + * @param options.excludeExtensions A list of file extensions to exclude from being watched for changes + */ +const watchFiles = async ( + compilerCtx: d.CompilerCtx, + dir: string, + options?: { + recursive?: boolean; + excludeDirNames?: string[]; + excludeExtensions?: string[]; + }, +) => { + const recursive = options?.recursive ?? true; + const excludeDirNames = options?.excludeDirNames ?? EXCLUDE_DIRS; + const excludeExtensions = options?.excludeExtensions ?? EXCLUDE_EXTENSIONS; + + /** + * non-src files that cause a rebuild + * mainly for root level config files, and getting an event when they change + */ + const rootFiles = await compilerCtx.fs.readdir(dir, { + recursive, + excludeDirNames, + excludeExtensions, + }); + + /** + * If the directory is watched recursively, we need to watch the directory itself. + */ + if (recursive) { + compilerCtx.addWatchDir(dir, true); + } + + /** + * Iterate over each file in the collection (filter out directories) and add + * a watcher for each + */ + rootFiles.filter(({ isFile }) => isFile).forEach(({ absPath }) => compilerCtx.addWatchFile(absPath)); +}; + +const emitFsChange = (compilerCtx: d.CompilerCtx, buildCtx: BuildContext) => { + if ( + buildCtx.dirsAdded.length > 0 || + buildCtx.dirsDeleted.length > 0 || + buildCtx.filesUpdated.length > 0 || + buildCtx.filesAdded.length > 0 || + buildCtx.filesDeleted.length > 0 + ) { + compilerCtx.events.emit('fsChange', { + dirsAdded: buildCtx.dirsAdded.slice(), + dirsDeleted: buildCtx.dirsDeleted.slice(), + filesUpdated: buildCtx.filesUpdated.slice(), + filesAdded: buildCtx.filesAdded.slice(), + filesDeleted: buildCtx.filesDeleted.slice(), + }); + } +}; + +const updateCompilerCtxCache = ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + path: string, + kind: d.CompilerFileWatcherEvent, +) => { + compilerCtx.fs.clearFileCache(path); + compilerCtx.changedFiles.add(path); + + if (kind === 'fileDelete') { + compilerCtx.moduleMap.delete(path); + } else if (kind === 'dirDelete') { + const fsRootDir = resolve('/'); + compilerCtx.moduleMap.forEach((_, moduleFilePath) => { + let moduleAncestorDir = dirname(moduleFilePath); + + for (let i = 0; i < 50; i++) { + if (moduleAncestorDir === config.rootDir || moduleAncestorDir === fsRootDir) { + break; + } + + if (moduleAncestorDir === path) { + compilerCtx.fs.clearFileCache(moduleFilePath); + compilerCtx.moduleMap.delete(moduleFilePath); + compilerCtx.changedFiles.add(moduleFilePath); + break; + } + + moduleAncestorDir = dirname(moduleAncestorDir); + } + }); + } +}; diff --git a/packages/core/src/compiler/build/write-build.ts b/packages/core/src/compiler/build/write-build.ts new file mode 100644 index 00000000000..447e4940289 --- /dev/null +++ b/packages/core/src/compiler/build/write-build.ts @@ -0,0 +1,51 @@ +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}`); +}; diff --git a/packages/core/src/compiler/build/write-export-maps.ts b/packages/core/src/compiler/build/write-export-maps.ts new file mode 100644 index 00000000000..ab8588a3844 --- /dev/null +++ b/packages/core/src/compiler/build/write-export-maps.ts @@ -0,0 +1,81 @@ +import { + isEligiblePrimaryPackageOutputTarget, + isOutputTargetDistCustomElements, + isOutputTargetDistLazyLoader, +} from '@utils'; +import { relative } from '@utils'; +import { execSync } from 'child_process'; + +import * as d from '../../declarations'; +import { PRIMARY_PACKAGE_TARGET_CONFIGS } from '../types/validate-primary-package-output-target'; + +/** + * Create export map entry point definitions for the `package.json` file using the npm CLI. + * This will generate a root entry point for the package, as well as entry points for each component and + * the lazy loader (if applicable). + * + * @param config The validated Stencil config + * @param buildCtx The build context containing the components to generate export maps for + */ +export const writeExportMaps = (config: d.ValidatedConfig, buildCtx: d.BuildCtx) => { + const eligiblePrimaryTargets = config.outputTargets.filter(isEligiblePrimaryPackageOutputTarget); + if (eligiblePrimaryTargets.length > 0) { + const primaryTarget = + eligiblePrimaryTargets.find((o) => o.isPrimaryPackageOutputTarget) ?? eligiblePrimaryTargets[0]; + const outputTargetConfig = PRIMARY_PACKAGE_TARGET_CONFIGS[primaryTarget.type]; + + if (outputTargetConfig.getModulePath) { + const importPath = outputTargetConfig.getModulePath(config.rootDir, primaryTarget.dir!); + + if (importPath) { + execSync(`npm pkg set "exports[.][import]"="${importPath}"`); + } + } + + if (outputTargetConfig.getMainPath) { + const requirePath = outputTargetConfig.getMainPath(config.rootDir, primaryTarget.dir!); + + if (requirePath) { + execSync(`npm pkg set "exports[.][require]"="${requirePath}"`); + } + } + + if (outputTargetConfig.getTypesPath) { + const typesPath = outputTargetConfig.getTypesPath(config.rootDir, primaryTarget); + + if (typesPath) { + execSync(`npm pkg set "exports[.][types]"="${typesPath}"`); + } + } + } + + const distLazyLoader = config.outputTargets.find(isOutputTargetDistLazyLoader); + if (distLazyLoader != null) { + // Calculate relative path from project root to lazy-loader output directory + let outDir = relative(config.rootDir, distLazyLoader.dir); + if (!outDir.startsWith('.')) { + outDir = './' + outDir; + } + + execSync(`npm pkg set "exports[./loader][import]"="${outDir}/index.js"`); + execSync(`npm pkg set "exports[./loader][require]"="${outDir}/index.cjs"`); + execSync(`npm pkg set "exports[./loader][types]"="${outDir}/index.d.ts"`); + } + + const distCustomElements = config.outputTargets.find(isOutputTargetDistCustomElements); + if (distCustomElements != null) { + // Calculate relative path from project root to custom elements output directory + let outDir = relative(config.rootDir, distCustomElements.dir!); + if (!outDir.startsWith('.')) { + outDir = './' + outDir; + } + + buildCtx.components.forEach((cmp) => { + execSync(`npm pkg set "exports[./${cmp.tagName}][import]"="${outDir}/${cmp.tagName}.js"`); + + if (distCustomElements.generateTypeDeclarations) { + execSync(`npm pkg set "exports[./${cmp.tagName}][types]"="${outDir}/${cmp.tagName}.d.ts"`); + } + }); + } +}; diff --git a/packages/core/src/compiler/bundle/app-data-plugin.ts b/packages/core/src/compiler/bundle/app-data-plugin.ts new file mode 100644 index 00000000000..e04f0e64ea1 --- /dev/null +++ b/packages/core/src/compiler/bundle/app-data-plugin.ts @@ -0,0 +1,260 @@ +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 { 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: 'client' | 'hydrate' | 'worker', +): 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); + 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 + */ +const appendGlobalStyles = async (buildCtx: d.BuildCtx, s: MagicString) => { + const globalStyles = + buildCtx.config.globalStyle && buildCtx.config.extras.addGlobalStyleToComponents !== false + ? await buildCtx.stylesPromise + : ''; + s.append(`export const globalStyles = ${JSON.stringify(globalStyles)};\n`); +}; + +/** + * Generates the `BUILD` constant that is used at compile-time in a Stencil project + * + * **This function mutates the provided {@link MagicString} argument** + * + * @param config the configuration associated with the Stencil project + * @param buildConditionals the build conditionals to serialize into a JS object + * @param s a `MagicString` to append the generated constant onto + */ +export const appendBuildConditionals = ( + config: d.ValidatedConfig, + buildConditionals: d.BuildConditionals, + s: MagicString, +): void => { + const buildData = Object.keys(buildConditionals) + .sort() + .map((key) => key + ': ' + JSON.stringify((buildConditionals as any)[key])) + .join(', '); + + s.append(`export const BUILD = /* ${config.fsNamespace} */ { ${buildData} };\n`); +}; + +const appendEnv = (config: d.ValidatedConfig, s: MagicString) => { + s.append(`export const Env = /* ${config.fsNamespace} */ ${JSON.stringify(config.env)};\n`); +}; + +const appendNamespace = (config: d.ValidatedConfig, s: MagicString) => { + s.append(`export const NAMESPACE = '${config.fsNamespace}';\n`); +}; + +interface GlobalScript { + defaultName: string; + path: string; +} diff --git a/packages/core/src/compiler/bundle/bundle-interface.ts b/packages/core/src/compiler/bundle/bundle-interface.ts new file mode 100644 index 00000000000..3708619fd57 --- /dev/null +++ b/packages/core/src/compiler/bundle/bundle-interface.ts @@ -0,0 +1,61 @@ +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: 'client' | 'hydrate' | 'worker'; + /** + * 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; +} diff --git a/packages/core/src/compiler/bundle/bundle-output.ts b/packages/core/src/compiler/bundle/bundle-output.ts new file mode 100644 index 00000000000..7686975f0c8 --- /dev/null +++ b/packages/core/src/compiler/bundle/bundle-output.ts @@ -0,0 +1,211 @@ +import rollupCommonjsPlugin from '@rollup/plugin-commonjs'; +import rollupJsonPlugin from '@rollup/plugin-json'; +import rollupNodeResolvePlugin from '@rollup/plugin-node-resolve'; +import rollupReplacePlugin from '@rollup/plugin-replace'; +import { createOnWarnFn, isString, loadRollupDiagnostics } from '@utils'; +import { type ObjectHook, PluginContext, rollup, RollupOptions, TreeshakingOptions } from 'rollup'; + +import type * as d from '../../declarations'; +import { lazyComponentPlugin } from '../output-targets/dist-lazy/lazy-component-plugin'; +import { appDataPlugin } from './app-data-plugin'; +import type { BundleOptions } from './bundle-interface'; +import { coreResolvePlugin } from './core-resolve-plugin'; +import { devNodeModuleResolveId } from './dev-node-module-resolve'; +import { extFormatPlugin } from './ext-format-plugin'; +import { extTransformsPlugin } from './ext-transforms-plugin'; +import { fileLoadPlugin } from './file-load-plugin'; +import { loaderPlugin } from './loader-plugin'; +import { pluginHelper } from './plugin-helper'; +import { serverPlugin } from './server-plugin'; +import { resolveIdWithTypeScript, typescriptPlugin } from './typescript-plugin'; +import { userIndexPlugin } from './user-index-plugin'; +import { workerPlugin } from './worker-plugin'; + +export const bundleOutput = async ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + buildCtx: d.BuildCtx, + bundleOpts: BundleOptions, +) => { + try { + const rollupOptions = getRollupOptions(config, compilerCtx, buildCtx, bundleOpts); + const rollupBuild = await rollup(rollupOptions); + + compilerCtx.rollupCache.set(bundleOpts.id, rollupBuild.cache); + return rollupBuild; + } catch (e: any) { + if (!buildCtx.hasError) { + // TODO(STENCIL-353): Implement a type guard that balances using our own copy of Rollup types (which are + // breakable) and type safety (so that the error variable may be something other than `any`) + loadRollupDiagnostics(config, compilerCtx, buildCtx, e); + } + } + return undefined; +}; + +/** + * Build the rollup options that will be used to transpile, minify, and otherwise transform a Stencil project + * @param config the Stencil configuration for the project + * @param compilerCtx the current compiler context + * @param buildCtx a context object containing information about the current build + * @param bundleOpts Rollup bundling options to apply to the base configuration setup by this function + * @returns the rollup options to be used + */ +export const getRollupOptions = ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + buildCtx: d.BuildCtx, + bundleOpts: BundleOptions, +): RollupOptions => { + const nodeResolvePlugin = rollupNodeResolvePlugin({ + mainFields: ['collection:main', 'jsnext:main', 'es2017', 'es2015', 'module', 'main'], + browser: bundleOpts.platform !== 'hydrate', + rootDir: config.rootDir, + exportConditions: ['default', 'module', 'import', 'require'], + extensions: ['.tsx', '.ts', '.mts', '.cts', '.js', '.mjs', '.cjs', '.json', '.d.ts', '.d.mts', '.d.cts'], + ...config.nodeResolve, + }); + + // @ts-expect-error - this is required now. + nodeResolvePlugin.resolve = async function () { + // Investigate if we can use this to leverage Stencil's in-memory fs + }; + + // @ts-expect-error - this is required now. + nodeResolvePlugin.warn = (log) => { + const onWarn = createOnWarnFn(buildCtx.diagnostics); + if (typeof log === 'string') { + onWarn({ message: log }); + } else if (typeof log === 'function') { + const result = log(); + if (typeof result === 'string') { + onWarn({ message: result }); + } else { + onWarn(result); + } + } else { + onWarn(log); + } + }; + + assertIsObjectHook(nodeResolvePlugin.resolveId); + // remove default 'post' order + nodeResolvePlugin.resolveId.order = null; + const orgNodeResolveId = nodeResolvePlugin.resolveId.handler; + + const orgNodeResolveId2 = (nodeResolvePlugin.resolveId.handler = async function (importee: string, importer: string) { + const [realImportee, query] = importee.split('?'); + const resolved = await orgNodeResolveId.call( + nodeResolvePlugin as unknown as PluginContext, + realImportee, + importer, + { + attributes: {}, + isEntry: true, + }, + ); + if (resolved) { + if (isString(resolved)) { + return query ? resolved + '?' + query : resolved; + } + return { + ...resolved, + id: query ? resolved.id + '?' + query : resolved.id, + }; + } + return resolved; + }); + if (config.devServer?.experimentalDevModules) { + nodeResolvePlugin.resolveId = async function (importee: string, importer: string) { + const resolvedId = await orgNodeResolveId2.call( + nodeResolvePlugin as unknown as PluginContext, + importee, + importer, + ); + return devNodeModuleResolveId(config, compilerCtx.fs, resolvedId, importee); + }; + } + + const beforePlugins = config.rollupPlugins.before || []; + const afterPlugins = config.rollupPlugins.after || []; + + const rollupOptions: RollupOptions = { + input: bundleOpts.inputs, + output: { + inlineDynamicImports: bundleOpts.inlineDynamicImports ?? false, + }, + + plugins: [ + coreResolvePlugin( + config, + compilerCtx, + bundleOpts.platform, + !!bundleOpts.externalRuntime, + bundleOpts.conditionals?.lazyLoad ?? false, + ), + appDataPlugin(config, compilerCtx, buildCtx, bundleOpts.conditionals, bundleOpts.platform), + lazyComponentPlugin(buildCtx), + loaderPlugin(bundleOpts.loader), + userIndexPlugin(config, compilerCtx), + typescriptPlugin(compilerCtx, bundleOpts, config), + extFormatPlugin(config), + extTransformsPlugin(config, compilerCtx, buildCtx), + workerPlugin(config, compilerCtx, buildCtx, bundleOpts.platform, !!bundleOpts.inlineWorkers), + serverPlugin(config, bundleOpts.platform), + ...beforePlugins, + nodeResolvePlugin, + resolveIdWithTypeScript(config, compilerCtx), + rollupCommonjsPlugin({ + include: /node_modules/, + sourceMap: config.sourceMap, + transformMixedEsModules: false, + ...config.commonjs, + }), + ...afterPlugins, + pluginHelper(config, buildCtx, bundleOpts.platform), + rollupJsonPlugin({ + preferConst: true, + }), + rollupReplacePlugin({ + 'process.env.NODE_ENV': config.devMode ? '"development"' : '"production"', + preventAssignment: true, + }), + fileLoadPlugin(compilerCtx.fs), + ], + + treeshake: getTreeshakeOption(config, bundleOpts), + preserveEntrySignatures: bundleOpts.preserveEntrySignatures ?? 'strict', + + onwarn: createOnWarnFn(buildCtx.diagnostics), + + cache: compilerCtx.rollupCache.get(bundleOpts.id), + + external: config.rollupConfig.inputOptions.external, + + maxParallelFileOps: config.rollupConfig.inputOptions.maxParallelFileOps, + }; + + return rollupOptions; +}; + +const getTreeshakeOption = (config: d.ValidatedConfig, bundleOpts: BundleOptions): TreeshakingOptions | boolean => { + if (bundleOpts.platform === 'hydrate') { + return { + propertyReadSideEffects: false, + tryCatchDeoptimization: false, + }; + } + + const treeshake = + !config.devMode && config.rollupConfig.inputOptions.treeshake !== false + ? { + propertyReadSideEffects: false, + tryCatchDeoptimization: false, + } + : false; + return treeshake; +}; + +function assertIsObjectHook(hook: ObjectHook): asserts hook is { handler: T; order?: 'pre' | 'post' | null } { + if (typeof hook !== 'object') throw new Error(`expected the rollup plugin hook ${hook} to be an object`); +} diff --git a/packages/core/src/compiler/bundle/constants.ts b/packages/core/src/compiler/bundle/constants.ts new file mode 100644 index 00000000000..463deceada2 --- /dev/null +++ b/packages/core/src/compiler/bundle/constants.ts @@ -0,0 +1,3 @@ +export const DEV_MODULE_CACHE_BUSTER = 0; + +export const DEV_MODULE_DIR = `~dev-module`; diff --git a/packages/core/src/compiler/bundle/core-resolve-plugin.ts b/packages/core/src/compiler/bundle/core-resolve-plugin.ts new file mode 100644 index 00000000000..d8db94b18e2 --- /dev/null +++ b/packages/core/src/compiler/bundle/core-resolve-plugin.ts @@ -0,0 +1,167 @@ +import { isRemoteUrl, join, normalizeFsPath, normalizePath } from '@utils'; +import { dirname } from 'path'; +import type { Plugin } from 'rollup'; + +import type * as d from '../../declarations'; +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, +} from './entry-alias-ids'; + +export const coreResolvePlugin = ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + platform: 'client' | 'hydrate' | 'worker', + 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; + } + return null; + }, + + async load(filePath) { + if (filePath && !filePath.startsWith('\0')) { + filePath = normalizeFsPath(filePath); + + if (filePath === internalClient || filePath === internalHydrate) { + if (platform === 'worker') { + return ` +export const Build = { + isDev: ${config.devMode}, + isBrowser: true, + isServer: false, + isTesting: false, +};`; + } + let code = await compilerCtx.fs.readFile(filePath); + + if (typeof code !== 'string' && isRemoteUrl(compilerExe)) { + const url = getStencilModuleUrl(compilerExe, filePath); + code = await fetchModuleAsync(config.sys, compilerCtx.fs, packageVersions, url, filePath); + } + + if (typeof code === 'string') { + const hydratedFlag = config.hydratedFlag; + if (hydratedFlag) { + const hydratedFlagHead = getHydratedFlagHead(hydratedFlag); + if (HYDRATED_CSS !== hydratedFlagHead) { + code = code.replace(HYDRATED_CSS, hydratedFlagHead); + if (hydratedFlag.name !== 'hydrated') { + code = code.replace(`.classList.add("hydrated")`, `.classList.add("${hydratedFlag.name}")`); + code = code.replace(`.classList.add('hydrated')`, `.classList.add('${hydratedFlag.name}')`); + code = code.replace(`.setAttribute("hydrated",`, `.setAttribute("${hydratedFlag.name}",`); + code = code.replace(`.setAttribute('hydrated',`, `.setAttribute('${hydratedFlag.name}',`); + } + } + } else { + code = code.replace(HYDRATED_CSS, '{}'); + } + } + + return code; + } + } + return null; + }, + }; +}; + +export const getStencilInternalModule = (config: d.ValidatedConfig, compilerExe: string, internalModule: string) => { + if (isRemoteUrl(compilerExe)) { + return normalizePath( + config.sys.getLocalModulePath({ + rootDir: config.rootDir, + moduleId: '@stencil/core', + path: 'internal/' + internalModule, + }), + ); + } + + const compilerExeDir = dirname(compilerExe); + return normalizePath(join(compilerExeDir, '..', 'internal', internalModule)); +}; + +export const getHydratedFlagHead = (h: d.HydratedFlag) => { + // {visibility:hidden}.hydrated{visibility:inherit} + + let initial: string; + let hydrated: string; + + if (!String(h.initialValue) || h.initialValue === '' || h.initialValue == null) { + initial = ''; + } else { + initial = `{${h.property}:${h.initialValue}}`; + } + + const selector = h.selector === 'attribute' ? `[${h.name}]` : `.${h.name}`; + + if (!String(h.hydratedValue) || h.hydratedValue === '' || h.hydratedValue == null) { + hydrated = ''; + } else { + hydrated = `${selector}{${h.property}:${h.hydratedValue}}`; + } + + return initial + hydrated; +}; diff --git a/packages/core/src/compiler/bundle/dev-module.ts b/packages/core/src/compiler/bundle/dev-module.ts new file mode 100644 index 00000000000..ce630568724 --- /dev/null +++ b/packages/core/src/compiler/bundle/dev-module.ts @@ -0,0 +1,193 @@ +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; +} diff --git a/packages/core/src/compiler/bundle/dev-node-module-resolve.ts b/packages/core/src/compiler/bundle/dev-node-module-resolve.ts new file mode 100644 index 00000000000..58f0c94ff12 --- /dev/null +++ b/packages/core/src/compiler/bundle/dev-node-module-resolve.ts @@ -0,0 +1,89 @@ +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; +}; diff --git a/packages/core/src/compiler/bundle/entry-alias-ids.ts b/packages/core/src/compiler/bundle/entry-alias-ids.ts new file mode 100644 index 00000000000..62865b32fc4 --- /dev/null +++ b/packages/core/src/compiler/bundle/entry-alias-ids.ts @@ -0,0 +1,13 @@ +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 APP_DATA_CONDITIONAL = '?app-data=conditional'; +export const LAZY_BROWSER_ENTRY_ID = '@lazy-browser-entrypoint' + APP_DATA_CONDITIONAL; +export const LAZY_EXTERNAL_ENTRY_ID = '@lazy-external-entrypoint' + APP_DATA_CONDITIONAL; +export const USER_INDEX_ENTRY_ID = '@user-index-entrypoint'; diff --git a/packages/core/src/compiler/bundle/ext-format-plugin.ts b/packages/core/src/compiler/bundle/ext-format-plugin.ts new file mode 100644 index 00000000000..2642c9468fb --- /dev/null +++ b/packages/core/src/compiler/bundle/ext-format-plugin.ts @@ -0,0 +1,74 @@ +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};`; +}; diff --git a/packages/core/src/compiler/bundle/ext-transforms-plugin.ts b/packages/core/src/compiler/bundle/ext-transforms-plugin.ts new file mode 100644 index 00000000000..f48ff60133f --- /dev/null +++ b/packages/core/src/compiler/bundle/ext-transforms-plugin.ts @@ -0,0 +1,215 @@ +import { hasError, isOutputTargetDistCollection, join, mergeIntoWith, normalizeFsPath, relative } from '@utils'; +import type { Plugin } from 'rollup'; + +import type * as d from '../../declarations'; +import { runPluginTransformsEsmImports } from '../plugin/plugin'; +import { getScopeId } from '../style/scope-css'; +import { parseImportPath } from '../transformers/stencil-import-path'; + +/** + * This keeps a map of all the component styles we've seen already so we can create + * a correct state of all styles when we're doing a rebuild. This map helps by + * storing the state of all styles as follows, e.g.: + * + * ``` + * { + * 'cmp-a-$': { + * '/path/to/project/cmp-a.scss': 'button{color:red}', + * '/path/to/project/cmp-a.md.scss': 'button{color:blue}' + * } + * ``` + * + * Whenever one of the files change, we can propagate a correct concatenated + * version of all styles to the browser by setting `buildCtx.stylesUpdated`. + */ +type ComponentStyleMap = Map; +const allCmpStyles = new Map(); + +/** + * A Rollup plugin which bundles up some transformation of CSS imports as well + * as writing some files to disk for the `DIST_COLLECTION` output target. + * + * @param config a user-supplied configuration + * @param compilerCtx the current compiler context + * @param buildCtx the current build context + * @returns a Rollup plugin which carries out the necessary work + */ +export const extTransformsPlugin = ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + buildCtx: d.BuildCtx, +): Plugin => { + return { + name: 'extTransformsPlugin', + + /** + * A custom function targeting the `transform` build hook in Rollup. See here for details: + * https://rollupjs.org/guide/en/#transform + * + * Here we are ignoring the first argument (which contains the module's source code) and + * only looking at the `id` argument. We use that `id` to get information about the module + * in question from disk ourselves so that we can then do some transformations on it. + * + * @param _ an unused parameter (normally the code for a given module) + * @param id the id of a module + * @returns metadata for Rollup or null if no transformation should be done + */ + async transform(_, id) { + if (/\0/.test(id)) { + return null; + } + + /** + * Make sure compiler context has a registered worker. The interface suggests that it + * potentially can be undefined, therefore check for it here. + */ + if (!compilerCtx.worker) { + return null; + } + + // The `id` here was possibly previously updated using + // `serializeImportPath` to annotate the filepath with various metadata + // serialized to query-params. If that was done for this particular `id` + // then the `data` prop will not be null. + const { data } = parseImportPath(id); + + if (data != null) { + let cmpStyles: ComponentStyleMap | undefined = undefined; + let cmp: d.ComponentCompilerMeta | undefined = undefined; + const filePath = normalizeFsPath(id); + const code = await compilerCtx.fs.readFile(filePath); + if (typeof code !== 'string') { + return null; + } + + /** + * add file to watch list if it is outside of the `srcDir` config path + */ + if (config.watch && (id.startsWith('/') || id.startsWith('.')) && !id.startsWith(config.srcDir)) { + compilerCtx.addWatchFile(id.split('?')[0]); + } + + const pluginTransforms = await runPluginTransformsEsmImports(config, compilerCtx, buildCtx, code, filePath); + + if (data.tag) { + cmp = buildCtx.components.find((c) => c.tagName === data.tag); + const moduleFile = cmp && !cmp.isCollectionDependency && compilerCtx.moduleMap.get(cmp.sourceFilePath); + + if (moduleFile) { + const collectionDirs = config.outputTargets.filter(isOutputTargetDistCollection); + const relPath = relative(config.srcDir, pluginTransforms.id); + + // If we found a `moduleFile` in the module map above then we + // should write the transformed CSS file (found in the return value + // of `runPluginTransformsEsmImports`, above) to disk. + await Promise.all( + collectionDirs.map(async (outputTarget) => { + const collectionPath = join(outputTarget.collectionDir, relPath); + await compilerCtx.fs.writeFile(collectionPath, pluginTransforms.code); + }), + ); + } + + /** + * initiate map for component styles + */ + const scopeId = getScopeId(data.tag, data.mode); + if (!allCmpStyles.has(scopeId)) { + allCmpStyles.set(scopeId, new Map()); + } + cmpStyles = allCmpStyles.get(scopeId); + } + + const cssTransformResults = await compilerCtx.worker.transformCssToEsm({ + file: pluginTransforms.id, + input: pluginTransforms.code, + tag: data.tag, + tags: buildCtx.components.map((c) => c.tagName), + addTagTransformers: !!buildCtx.config.extras.additionalTagTransformers, + encapsulation: data.encapsulation, + mode: data.mode, + sourceMap: config.sourceMap, + minify: config.minifyCss, + autoprefixer: config.autoprefixCss, + docs: config.buildDocs, + }); + + /** + * persist component styles for transformed stylesheet + */ + if (cmpStyles) { + cmpStyles.set(filePath, cssTransformResults.styleText); + } + + // Set style docs + if (cmp) { + cmp.styleDocs ||= []; + mergeIntoWith(cmp.styleDocs, cssTransformResults.styleDocs, (docs) => `${docs.name},${docs.mode}`); + } + + // Track dependencies + for (const dep of pluginTransforms.dependencies) { + this.addWatchFile(dep); + compilerCtx.addWatchFile(dep); + } + + buildCtx.diagnostics.push(...pluginTransforms.diagnostics); + buildCtx.diagnostics.push(...cssTransformResults.diagnostics); + const didError = hasError(cssTransformResults.diagnostics) || hasError(pluginTransforms.diagnostics); + if (didError) { + this.error('Plugin CSS transform error'); + } + + const hasUpdatedStyle = buildCtx.stylesUpdated.some((s) => { + return s.styleTag === data.tag && s.styleMode === data.mode && s.styleText === cssTransformResults.styleText; + }); + + /** + * if the style has updated, compose all styles for the component + */ + if (!hasUpdatedStyle && data.tag && data.mode) { + const externalStyles = cmp?.styles?.[0]?.externalStyles; + + /** + * if component has external styles, use a list to keep the order to which + * styles are applied. + */ + const styleText = cmpStyles + ? externalStyles + ? /** + * attempt to find the original `filePath` key through `originalComponentPath` + * and `absolutePath` as path can differ based on how Stencil is installed + * e.g. through `npm link` or `npm install` + */ + externalStyles + .map((es) => cmpStyles.get(es.originalComponentPath) || cmpStyles.get(es.absolutePath)) + .join('\n') + : /** + * if `externalStyles` is not defined, then created the style text in the + * order of which the styles were compiled. + */ + [...cmpStyles.values()].join('\n') + : /** + * if `cmpStyles` is not defined, then use the style text from the transform + * as it is not connected to a component. + */ + cssTransformResults.styleText; + + buildCtx.stylesUpdated.push({ + styleTag: data.tag, + styleMode: data.mode, + styleText, + }); + } + + return { + code: cssTransformResults.output, + map: cssTransformResults.map, + moduleSideEffects: false, + }; + } + + return null; + }, + }; +}; diff --git a/packages/core/src/compiler/bundle/file-load-plugin.ts b/packages/core/src/compiler/bundle/file-load-plugin.ts new file mode 100644 index 00000000000..df79ee44f1c --- /dev/null +++ b/packages/core/src/compiler/bundle/file-load-plugin.ts @@ -0,0 +1,18 @@ +import { isDtsFile, normalizeFsPath } from '@utils'; +import type { Plugin } from 'rollup'; + +import { InMemoryFileSystem } from '../sys/in-memory-fs'; + +export const fileLoadPlugin = (fs: InMemoryFileSystem): Plugin => { + return { + name: 'fileLoadPlugin', + + load(id) { + const fsFilePath = normalizeFsPath(id); + if (isDtsFile(fsFilePath)) { + return ''; + } + return fs.readFile(fsFilePath); + }, + }; +}; diff --git a/packages/core/src/compiler/bundle/loader-plugin.ts b/packages/core/src/compiler/bundle/loader-plugin.ts new file mode 100644 index 00000000000..091cc702ebe --- /dev/null +++ b/packages/core/src/compiler/bundle/loader-plugin.ts @@ -0,0 +1,43 @@ +import type { LoadResult, Plugin, ResolveIdResult } from 'rollup'; + +/** + * Rollup plugin that aids in resolving the entry points (1 or more files) for a Stencil project. For example, a project + * using the `dist-custom-elements` output target may have a single 'entry point' for each file containing a component. + * Each of those files will be independently resolved and loaded by this plugin for further processing by Rollup later + * in the bundling process. + * + * @param entries the Stencil project files to process. It should be noted that the keys in this object may not + * necessarily be an absolute or relative path to a file, but may be a Rollup Virtual Module (which begin with \0). + * @returns the rollup plugin that loads and process a Stencil project's entry points + */ +export const loaderPlugin = (entries: { [id: string]: string } = {}): Plugin => { + return { + name: 'stencilLoaderPlugin', + /** + * A rollup build hook for resolving the imports of individual Stencil project files. This hook only resolves + * modules that are contained in the plugin's `entries` argument. [Source](https://rollupjs.org/guide/en/#resolveid) + * @param id the importee to resolve + * @returns a string that resolves an import to some id, null otherwise + */ + resolveId(id: string): ResolveIdResult { + if (id in entries) { + return { + id, + }; + } + return null; + }, + /** + * A rollup build hook for loading individual Stencil project files [Source](https://rollupjs.org/guide/en/#load) + * @param id the path of the module to load. It should be noted that the keys in this object may not necessarily + * be an absolute or relative path to a file, but may be a Rollup Virtual Module. + * @returns the module matched, null otherwise + */ + load(id: string): LoadResult { + if (id in entries) { + return entries[id]; + } + return null; + }, + }; +}; diff --git a/packages/core/src/compiler/bundle/plugin-helper.ts b/packages/core/src/compiler/bundle/plugin-helper.ts new file mode 100644 index 00000000000..31792eaa927 --- /dev/null +++ b/packages/core/src/compiler/bundle/plugin-helper.ts @@ -0,0 +1,77 @@ +import { buildError, relative } from '@utils'; + +import type * as d from '../../declarations'; + +export const pluginHelper = (config: d.ValidatedConfig, builtCtx: d.BuildCtx, platform: string) => { + return { + name: 'pluginHelper', + resolveId(importee: string, importer: string): null { + if (/\0/.test(importee)) { + // ignore IDs with null character, these belong to other plugins + return null; + } + + if (importee.endsWith('/')) { + importee = importee.slice(0, -1); + } + + if (builtIns.has(importee)) { + let fromMsg = ''; + if (importer) { + fromMsg = ` from ${relative(config.rootDir, importer)}`; + } + const diagnostic = buildError(builtCtx.diagnostics); + diagnostic.header = `Node Polyfills Required`; + diagnostic.messageText = `For the import "${importee}" to be bundled${fromMsg}, ensure the "rollup-plugin-node-polyfills" plugin is installed and added to the stencil config plugins (${platform}). Please see the bundling docs for more information. + Further information: https://stenciljs.com/docs/module-bundling`; + } + return null; + }, + }; +}; + +const builtIns = new Set([ + 'child_process', + 'cluster', + 'dgram', + 'dns', + 'module', + 'net', + 'readline', + 'repl', + 'tls', + + 'assert', + 'console', + 'constants', + 'domain', + 'events', + 'path', + 'punycode', + 'querystring', + '_stream_duplex', + '_stream_passthrough', + '_stream_readable', + '_stream_writable', + '_stream_transform', + 'string_decoder', + 'sys', + 'tty', + + 'crypto', + 'fs', + + 'Buffer', + 'buffer', + 'global', + 'http', + 'https', + 'os', + 'process', + 'stream', + 'timers', + 'url', + 'util', + 'vm', + 'zlib', +]); diff --git a/packages/core/src/compiler/bundle/server-plugin.ts b/packages/core/src/compiler/bundle/server-plugin.ts new file mode 100644 index 00000000000..58533d3043e --- /dev/null +++ b/packages/core/src/compiler/bundle/server-plugin.ts @@ -0,0 +1,66 @@ +import { isOutputTargetHydrate, isString, normalizeFsPath } from '@utils'; +import { isAbsolute } from 'path'; +import type { Plugin } from 'rollup'; + +import type * as d from '../../declarations'; + +export const serverPlugin = (config: d.ValidatedConfig, platform: string): Plugin => { + const isHydrateBundle = platform === 'hydrate'; + const serverVarid = `@removed-server-code`; + + const isServerOnlyModule = (id: string) => { + if (isString(id)) { + id = normalizeFsPath(id); + return id.includes('.server/') || id.endsWith('.server'); + } + return false; + }; + + const externals = isHydrateBundle + ? config.outputTargets.filter(isOutputTargetHydrate).flatMap((o) => o.external) + : []; + + return { + name: 'serverPlugin', + + resolveId(id, importer) { + if (id === serverVarid) { + return id; + } + if (isHydrateBundle) { + if (externals.includes(id)) { + // don't attempt to bundle node builtins for the hydrate bundle + return { + id, + external: true, + }; + } + if (isServerOnlyModule(importer) && !id.startsWith('.') && !isAbsolute(id)) { + // do not bundle if the importer is a server-only module + // and the module it is importing is a node module + return { + id, + external: true, + }; + } + } else { + if (isServerOnlyModule(id)) { + // any path that has .server in it shouldn't actually + // be bundled in the web build, only the hydrate build + return serverVarid; + } + } + return null; + }, + + load(id) { + if (id === serverVarid) { + return { + code: 'export default {};', + syntheticNamedExports: true, + }; + } + return null; + }, + }; +}; diff --git a/packages/core/src/compiler/bundle/test/app-data-plugin.spec.ts b/packages/core/src/compiler/bundle/test/app-data-plugin.spec.ts new file mode 100644 index 00000000000..2ebeb7d695f --- /dev/null +++ b/packages/core/src/compiler/bundle/test/app-data-plugin.spec.ts @@ -0,0 +1,46 @@ +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); + }); +}); diff --git a/packages/core/src/compiler/bundle/test/core-resolve-plugin.spec.ts b/packages/core/src/compiler/bundle/test/core-resolve-plugin.spec.ts new file mode 100644 index 00000000000..bada6c31750 --- /dev/null +++ b/packages/core/src/compiler/bundle/test/core-resolve-plugin.spec.ts @@ -0,0 +1,70 @@ +import { mockValidatedConfig } from '@stencil/core/testing'; + +import { createSystem } from '../../../compiler/sys/stencil-sys'; +import type * as d from '../../../declarations'; +import { getHydratedFlagHead, getStencilInternalModule } from '../core-resolve-plugin'; + +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}`); + }); +}); diff --git a/packages/core/src/compiler/bundle/test/ext-transforms-plugin.spec.ts b/packages/core/src/compiler/bundle/test/ext-transforms-plugin.spec.ts new file mode 100644 index 00000000000..984ee2d7a80 --- /dev/null +++ b/packages/core/src/compiler/bundle/test/ext-transforms-plugin.spec.ts @@ -0,0 +1,105 @@ +import { mockBuildCtx, mockCompilerCtx, mockModule, mockValidatedConfig } from '@stencil/core/testing'; +import { normalizePath } from '@utils'; + +import * as importPathLib from '../../transformers/stencil-import-path'; +import { stubComponentCompilerMeta } from '../../types/tests/ComponentCompilerMeta.stub'; +import { BundleOptions } from '../bundle-interface'; +import { extTransformsPlugin } from '../ext-transforms-plugin'; + +describe('extTransformsPlugin', () => { + function setup(bundleOptsOverrides: Partial = {}) { + const config = mockValidatedConfig({ + plugins: [], + outputTargets: [ + { + type: 'dist-collection', + dir: 'dist/', + collectionDir: 'dist/collectionDir', + }, + ], + srcDir: '/some/stubbed/path', + }); + const compilerCtx = mockCompilerCtx(config); + const buildCtx = mockBuildCtx(config, compilerCtx); + + const compilerComponentMeta = stubComponentCompilerMeta({ + tagName: 'my-component', + componentClassName: 'MyComponent', + }); + + buildCtx.components = [compilerComponentMeta]; + + compilerCtx.moduleMap.set( + compilerComponentMeta.sourceFilePath, + mockModule({ + cmps: [compilerComponentMeta], + }), + ); + + const bundleOpts: BundleOptions = { + id: 'test-bundle', + platform: 'client', + inputs: {}, + ...bundleOptsOverrides, + }; + + const cssText = ':host { text: pink; }'; + + // mock out the read for our CSS + jest.spyOn(compilerCtx.fs, 'readFile').mockResolvedValue(cssText); + + // mock out compilerCtx.worker.transformCssToEsm because 1) we want to + // test what arguments are passed to it and 2) calling it un-mocked causes + // the infamous autoprefixer-spew-issue :( + const transformCssToEsmSpy = jest.spyOn(compilerCtx.worker, 'transformCssToEsm').mockResolvedValue({ + styleText: cssText, + output: cssText, + map: null, + diagnostics: [], + imports: [], + defaultVarName: 'foo', + styleDocs: [], + }); + + const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile'); + return { + plugin: extTransformsPlugin(config, compilerCtx, buildCtx), + config, + compilerCtx, + buildCtx, + bundleOpts, + writeFileSpy, + transformCssToEsmSpy, + cssText, + }; + } + + describe('transform function', () => { + it('should set name', () => { + expect(setup().plugin.name).toBe('extTransformsPlugin'); + }); + + it('should return early if no data can be gleaned from the id', async () => { + const { plugin } = setup(); + // @ts-ignore we're testing something which shouldn't normally happen, + // but might if an argument of the wrong type were passed as `id` + const parseSpy = jest.spyOn(importPathLib, 'parseImportPath').mockReturnValue({ data: null }); + // @ts-ignore the Rollup plugins expect to be called in a Rollup context + expect(await plugin.transform('asdf', 'foo.css')).toBe(null); + parseSpy.mockRestore(); + }); + + it('should write CSS files if associated with a tag', async () => { + const { plugin, writeFileSpy } = setup(); + + // @ts-ignore the Rollup plugins expect to be called in a Rollup context + await plugin.transform('asdf', '/some/stubbed/path/foo.css?tag=my-component'); + + const [path, css] = writeFileSpy.mock.calls[0]; + + expect(normalizePath(path)).toBe('./dist/collectionDir/foo.css'); + + expect(css).toBe(':host { text: pink; }'); + }); + }); +}); diff --git a/packages/core/src/compiler/bundle/typescript-plugin.ts b/packages/core/src/compiler/bundle/typescript-plugin.ts new file mode 100644 index 00000000000..c3fa2ae4a96 --- /dev/null +++ b/packages/core/src/compiler/bundle/typescript-plugin.ts @@ -0,0 +1,99 @@ +import { isDtsFile, isString, normalizeFsPath } from '@utils'; +import { basename, isAbsolute } from 'path'; +import type { LoadResult, Plugin, TransformResult } from 'rollup'; +import ts from 'typescript'; + +import type * as d from '../../declarations'; +import { tsResolveModuleName } from '../sys/typescript/typescript-resolve-module'; +import { getModule } from '../transpile/transpiled-module'; +import type { BundleOptions } from './bundle-interface'; + +/** + * Rollup plugin that aids in resolving the TypeScript files and performing the transpilation step. + * @param compilerCtx the current compiler context + * @param bundleOpts Rollup bundling options to apply during TypeScript compilation + * @param config the Stencil configuration for the project + * @returns the rollup plugin for handling TypeScript files. + */ +export const typescriptPlugin = ( + compilerCtx: d.CompilerCtx, + bundleOpts: BundleOptions, + config: d.ValidatedConfig, +): Plugin => { + return { + name: `${bundleOpts.id}TypescriptPlugin`, + + /** + * A rollup build hook for loading TypeScript files and their associated source maps (if they exist). + * [Source](https://rollupjs.org/guide/en/#load) + * @param id the path of the file to load + * @returns the module matched (with its sourcemap if it exists), null otherwise + */ + load(id: string): LoadResult { + if (isAbsolute(id)) { + const fsFilePath = normalizeFsPath(id); + const module = getModule(compilerCtx, fsFilePath); + + if (module) { + if (!module.sourceMapFileText) { + return { code: module.staticSourceFileText, map: null }; + } + + const sourceMap: d.SourceMap = JSON.parse(module.sourceMapFileText); + sourceMap.sources = sourceMap.sources.map((src) => basename(src)); + return { code: module.staticSourceFileText, map: sourceMap }; + } + } + return null; + }, + /** + * Performs TypeScript compilation/transpilation, including applying any transformations against the Abstract Syntax + * Tree (AST) specific to stencil + * @param _code the code to modify, unused + * @param id module's identifier + * @returns the transpiled code, with its associated sourcemap. null otherwise + */ + transform(_code: string, id: string): TransformResult { + if (isAbsolute(id)) { + const fsFilePath = normalizeFsPath(id); + const mod = getModule(compilerCtx, fsFilePath); + if (mod?.cmps) { + const tsResult = ts.transpileModule(mod.staticSourceFileText, { + compilerOptions: config.tsCompilerOptions, + fileName: mod.sourceFilePath, + transformers: { + before: bundleOpts.customBeforeTransformers ?? [], + }, + }); + const sourceMap: d.SourceMap = tsResult.sourceMapText ? JSON.parse(tsResult.sourceMapText) : null; + return { code: tsResult.outputText, map: sourceMap }; + } + } + return null; + }, + }; +}; + +export const resolveIdWithTypeScript = (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx): Plugin => { + return { + name: `resolveIdWithTypeScript`, + + async resolveId(importee, importer) { + if (/\0/.test(importee) || !isString(importer)) { + return null; + } + + const tsResolved = tsResolveModuleName(config, compilerCtx, importee, importer); + if (tsResolved && tsResolved.resolvedModule) { + // this is probably a .d.ts file for whatever reason in how TS resolves this + // use this resolved file as the "importer" + const tsResolvedPath = tsResolved.resolvedModule.resolvedFileName; + if (isString(tsResolvedPath) && !isDtsFile(tsResolvedPath)) { + return tsResolvedPath; + } + } + + return null; + }, + }; +}; diff --git a/packages/core/src/compiler/bundle/user-index-plugin.ts b/packages/core/src/compiler/bundle/user-index-plugin.ts new file mode 100644 index 00000000000..d5c8f52483d --- /dev/null +++ b/packages/core/src/compiler/bundle/user-index-plugin.ts @@ -0,0 +1,30 @@ +import { join } from '@utils'; +import type { Plugin } from 'rollup'; + +import type * as d from '../../declarations'; +import { USER_INDEX_ENTRY_ID } from './entry-alias-ids'; + +export const userIndexPlugin = (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx): Plugin => { + return { + name: 'userIndexPlugin', + + async resolveId(importee) { + if (importee === USER_INDEX_ENTRY_ID) { + const usersIndexJsPath = join(config.srcDir, 'index.ts'); + const hasUserIndex = await compilerCtx.fs.access(usersIndexJsPath); + if (hasUserIndex) { + return usersIndexJsPath; + } + return importee; + } + return null; + }, + + async load(id) { + if (id === USER_INDEX_ENTRY_ID) { + return `//! Autogenerated index`; + } + return null; + }, + }; +}; diff --git a/packages/core/src/compiler/bundle/worker-plugin.ts b/packages/core/src/compiler/bundle/worker-plugin.ts new file mode 100644 index 00000000000..ac1b9e44c60 --- /dev/null +++ b/packages/core/src/compiler/bundle/worker-plugin.ts @@ -0,0 +1,466 @@ +import { generatePreamble, hasError, normalizeFsPath } from '@utils'; +import type { Plugin, PluginContext, TransformResult } from 'rollup'; + +import type * as d from '../../declarations'; +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: string, + 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')} +`; +}; diff --git a/packages/core/src/compiler/cache.ts b/packages/core/src/compiler/cache.ts new file mode 100644 index 00000000000..a095d2dad21 --- /dev/null +++ b/packages/core/src/compiler/cache.ts @@ -0,0 +1,188 @@ +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. +`; diff --git a/packages/core/src/compiler/compiler.ts b/packages/core/src/compiler/compiler.ts new file mode 100644 index 00000000000..228c4c13bfc --- /dev/null +++ b/packages/core/src/compiler/compiler.ts @@ -0,0 +1,65 @@ +import { isFunction } from '@utils'; +import ts from 'typescript'; + +import type { Compiler, Config, Diagnostic, ValidatedConfig } from '../declarations'; +import { CompilerContext } from './build/compiler-ctx'; +import { createFullBuild } from './build/full-build'; +import { createWatchBuild } from './build/watch-build'; +import { Cache } from './cache'; +import { getConfig } from './sys/config'; +import { createInMemoryFs } from './sys/in-memory-fs'; +import { resolveModuleIdAsync } from './sys/resolve/resolve-module-async'; +import { patchTypescript } from './sys/typescript/typescript-sys'; +import { createSysWorker } from './sys/worker/sys-worker'; + +/** + * Generate a Stencil compiler instance + * @param userConfig a user-provided Stencil configuration to apply to the compiler instance + * @returns a new instance of a Stencil compiler + * @public + */ +export const createCompiler = async (userConfig: Config): Promise => { + // actual compiler code + const config: ValidatedConfig = getConfig(userConfig); + const diagnostics: Diagnostic[] = []; + const sys = config.sys; + const compilerCtx = new CompilerContext(); + + if (isFunction(config.sys.setupCompiler)) { + config.sys.setupCompiler({ ts }); + } + + compilerCtx.fs = createInMemoryFs(sys); + compilerCtx.cache = new Cache(config, createInMemoryFs(sys)); + await compilerCtx.cache.initCacheDir(); + + sys.resolveModuleId = (opts) => resolveModuleIdAsync(sys, compilerCtx.fs, opts); + compilerCtx.worker = createSysWorker(config); + + if (sys.events) { + // Pipe events from sys.events to compilerCtx + sys.events.on(compilerCtx.events.emit); + } + patchTypescript(config, compilerCtx.fs); + + const build = () => createFullBuild(config, compilerCtx); + + const createWatcher = () => createWatchBuild(config, compilerCtx); + + const destroy = async () => { + compilerCtx.reset(); + compilerCtx.events.unsubscribeAll(); + await sys.destroy(); + }; + + const compiler: Compiler = { + build, + createWatcher, + destroy, + sys, + }; + + config.logger.printDiagnostics(diagnostics); + + return compiler; +}; diff --git a/packages/core/src/compiler/config/config-utils.ts b/packages/core/src/compiler/config/config-utils.ts new file mode 100644 index 00000000000..6c1f78a9407 --- /dev/null +++ b/packages/core/src/compiler/config/config-utils.ts @@ -0,0 +1,77 @@ +import { isBoolean, join } from '@utils'; +import { isAbsolute } from 'path'; + +import type { ConfigFlags } from '../../cli/config-flags'; +import type * as d from '../../declarations'; + +export const getAbsolutePath = (config: d.ValidatedConfig, dir: string) => { + if (!isAbsolute(dir)) { + dir = join(config.rootDir, dir); + } + return dir; +}; + +/** + * This function does two things: + * + * 1. If you pass a `flagName`, it will hoist that `flagName` out of the + * `ConfigFlags` object and onto the 'root' level (if you will) of the + * `config` under the `configName` (`keyof d.Config`) that you pass. + * 2. If you _don't_ pass a `flagName` it will just set the value you supply + * on the config. + * + * @param config the config that we want to update + * @param configName the key we're setting on the config + * @param flagName either the name of a ConfigFlag prop we want to hoist up or null + * @param defaultValue the default value we should set! + */ +export const setBooleanConfig = ( + config: d.UnvalidatedConfig, + configName: (K & keyof ConfigFlags) | K, + flagName: keyof ConfigFlags | null, + defaultValue: d.Config[K], +) => { + if (flagName) { + const flagValue = config.flags?.[flagName]; + if (isBoolean(flagValue)) { + config[configName] = flagValue; + } + } + + const userConfigName = getUserConfigName(config, configName); + + if (typeof config[userConfigName] === 'function') { + config[userConfigName] = !!config[userConfigName](); + } + + if (isBoolean(config[userConfigName])) { + config[configName] = config[userConfigName]; + } else { + config[configName] = defaultValue; + } +}; + +/** + * Find any possibly mis-capitalized configuration names on the config, logging + * and warning if one is found. + * + * @param config the user-supplied config that we're dealing with + * @param correctConfigName the configuration name that we're checking for right now + * @returns a string container a mis-capitalized config name found on the + * config object, if any. + */ +const getUserConfigName = (config: d.UnvalidatedConfig, correctConfigName: keyof d.Config): string => { + const userConfigNames = Object.keys(config); + + for (const userConfigName of userConfigNames) { + if (userConfigName.toLowerCase() === correctConfigName.toLowerCase()) { + if (userConfigName !== correctConfigName) { + config.logger?.warn(`config "${userConfigName}" should be "${correctConfigName}"`); + return userConfigName; + } + break; + } + } + + return correctConfigName; +}; diff --git a/packages/core/src/compiler/config/constants.ts b/packages/core/src/compiler/config/constants.ts new file mode 100644 index 00000000000..03e6f9c57f2 --- /dev/null +++ b/packages/core/src/compiler/config/constants.ts @@ -0,0 +1,13 @@ +import type * as d from '../../declarations'; + +type DefaultTargetComponentConfig = d.Config['docs']['markdown']['targetComponent']; + +export const DEFAULT_DEV_MODE = false; +export const DEFAULT_HASHED_FILENAME_LENGTH = 8; +export const MIN_HASHED_FILENAME_LENGTH = 4; +export const MAX_HASHED_FILENAME_LENGTH = 32; +export const DEFAULT_NAMESPACE = 'App'; +export const DEFAULT_TARGET_COMPONENT_STYLES: DefaultTargetComponentConfig = { + background: '#f9f', + textColor: '#333', +}; diff --git a/packages/core/src/compiler/config/load-config.ts b/packages/core/src/compiler/config/load-config.ts new file mode 100644 index 00000000000..b69cff10ea3 --- /dev/null +++ b/packages/core/src/compiler/config/load-config.ts @@ -0,0 +1,161 @@ +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; +}; diff --git a/packages/core/src/compiler/config/outputs/index.ts b/packages/core/src/compiler/config/outputs/index.ts new file mode 100644 index 00000000000..206cc45bc38 --- /dev/null +++ b/packages/core/src/compiler/config/outputs/index.ts @@ -0,0 +1,42 @@ +import { buildError, isValidConfigOutputTarget, VALID_CONFIG_OUTPUT_TARGETS } from '@utils'; + +import type * as d from '../../../declarations'; +import { validateCollection } from './validate-collection'; +import { validateCustomElement } from './validate-custom-element'; +import { validateCustomOutput } from './validate-custom-output'; +import { validateDist } from './validate-dist'; +import { validateDocs } from './validate-docs'; +import { validateHydrateScript } from './validate-hydrate-script'; +import { validateLazy } from './validate-lazy'; +import { validateStats } from './validate-stats'; +import { validateWww } from './validate-www'; + +export const validateOutputTargets = (config: d.ValidatedConfig, diagnostics: d.Diagnostic[]) => { + const userOutputs = (config.outputTargets || []).slice(); + + userOutputs.forEach((outputTarget) => { + if (!isValidConfigOutputTarget(outputTarget.type)) { + const err = buildError(diagnostics); + err.messageText = `Invalid outputTarget type "${ + outputTarget.type + }". Valid outputTarget types include: ${VALID_CONFIG_OUTPUT_TARGETS.map((t) => `"${t}"`).join(', ')}`; + } + }); + + config.outputTargets = [ + ...validateCollection(config, userOutputs), + ...validateCustomElement(config, userOutputs), + ...validateCustomOutput(config, diagnostics, userOutputs), + ...validateLazy(config, userOutputs), + ...validateWww(config, diagnostics, userOutputs), + ...validateDist(config, userOutputs), + ...validateDocs(config, diagnostics, userOutputs), + ...validateStats(config, userOutputs), + ]; + + // hydrate also gets info from the www output + config.outputTargets = [ + ...config.outputTargets, + ...validateHydrateScript(config, [...userOutputs, ...config.outputTargets]), + ]; +}; diff --git a/packages/core/src/compiler/config/outputs/validate-collection.ts b/packages/core/src/compiler/config/outputs/validate-collection.ts new file mode 100644 index 00000000000..842f0193861 --- /dev/null +++ b/packages/core/src/compiler/config/outputs/validate-collection.ts @@ -0,0 +1,27 @@ +import { isBoolean, isOutputTargetDistCollection } from '@utils'; + +import type * as d from '../../../declarations'; +import { getAbsolutePath } from '../config-utils'; + +/** + * Validate and return DIST_COLLECTION output targets, ensuring that the `dir` + * property is set on them. + * + * @param config a validated configuration object + * @param userOutputs an array of output targets + * @returns an array of validated DIST_COLLECTION output targets + */ +export const validateCollection = ( + config: d.ValidatedConfig, + userOutputs: d.OutputTarget[], +): d.OutputTargetDistCollection[] => { + return userOutputs.filter(isOutputTargetDistCollection).map((outputTarget) => { + return { + ...outputTarget, + transformAliasedImportPaths: isBoolean(outputTarget.transformAliasedImportPaths) + ? outputTarget.transformAliasedImportPaths + : true, + dir: getAbsolutePath(config, outputTarget.dir ?? 'dist/collection'), + }; + }); +}; diff --git a/packages/core/src/compiler/config/outputs/validate-custom-element.ts b/packages/core/src/compiler/config/outputs/validate-custom-element.ts new file mode 100644 index 00000000000..8a7dcc9040d --- /dev/null +++ b/packages/core/src/compiler/config/outputs/validate-custom-element.ts @@ -0,0 +1,90 @@ +import { COPY, DIST_TYPES, isBoolean, isOutputTargetDistCustomElements, join } from '@utils'; + +import type { + OutputTarget, + OutputTargetCopy, + OutputTargetDistCustomElements, + OutputTargetDistTypes, + ValidatedConfig, +} from '../../../declarations'; +import { CustomElementsExportBehaviorOptions } from '../../../declarations'; +import { getAbsolutePath } from '../config-utils'; +import { validateCopy } from '../validate-copy'; + +/** + * Validate one or more `dist-custom-elements` output targets. Validation of an output target may involve back-filling + * fields that are omitted with sensible defaults and/or creating additional supporting output targets that were not + * explicitly defined by the user + * @param config the Stencil configuration associated with the project being compiled + * @param userOutputs the output target(s) specified by the user + * @returns the validated output target(s) + */ +export const validateCustomElement = ( + config: ValidatedConfig, + userOutputs: ReadonlyArray, +): ReadonlyArray => { + const defaultDir = 'dist'; + + return userOutputs.filter(isOutputTargetDistCustomElements).reduce( + (outputs, o) => { + const outputTarget = { + ...o, + dir: getAbsolutePath(config, o.dir || join(defaultDir, 'components')), + }; + if (!isBoolean(outputTarget.empty)) { + outputTarget.empty = true; + } + if (!isBoolean(outputTarget.externalRuntime)) { + outputTarget.externalRuntime = true; + } + if (!isBoolean(outputTarget.generateTypeDeclarations)) { + outputTarget.generateTypeDeclarations = true; + } + // Export behavior must be defined on the validated target config and must + // be one of the export behavior valid values + if ( + outputTarget.customElementsExportBehavior == null || + !CustomElementsExportBehaviorOptions.includes(outputTarget.customElementsExportBehavior) + ) { + outputTarget.customElementsExportBehavior = 'default'; + } + + // Normalize autoLoader option + if (outputTarget.autoLoader === true) { + outputTarget.autoLoader = { + fileName: 'loader', + autoStart: true, + }; + } else if (outputTarget.autoLoader && typeof outputTarget.autoLoader === 'object') { + outputTarget.autoLoader = { + fileName: outputTarget.autoLoader.fileName || 'loader', + autoStart: outputTarget.autoLoader.autoStart !== false, + }; + } + + // unlike other output targets, Stencil does not allow users to define the output location of types at this time + if (outputTarget.generateTypeDeclarations) { + const typesDirectory = getAbsolutePath(config, join(defaultDir, 'types')); + outputs.push({ + type: DIST_TYPES, + dir: outputTarget.dir, + typesDir: typesDirectory, + }); + } + + outputTarget.copy = validateCopy(outputTarget.copy, []); + + if (outputTarget.copy.length > 0) { + outputs.push({ + type: COPY, + dir: config.rootDir, + copy: [...outputTarget.copy], + }); + } + outputs.push(outputTarget); + + return outputs; + }, + [] as (OutputTargetDistCustomElements | OutputTargetCopy | OutputTargetDistTypes)[], + ); +}; diff --git a/packages/core/src/compiler/config/outputs/validate-custom-output.ts b/packages/core/src/compiler/config/outputs/validate-custom-output.ts new file mode 100644 index 00000000000..9e722ff4ffe --- /dev/null +++ b/packages/core/src/compiler/config/outputs/validate-custom-output.ts @@ -0,0 +1,29 @@ +import { catchError, COPY, isOutputTargetCustom } from '@utils'; + +import type * as d from '../../../declarations'; + +export const validateCustomOutput = ( + config: d.ValidatedConfig, + diagnostics: d.Diagnostic[], + userOutputs: d.OutputTarget[], +) => { + return userOutputs.filter(isOutputTargetCustom).map((o) => { + if (o.validate) { + const localDiagnostics: d.Diagnostic[] = []; + try { + o.validate(config, diagnostics); + } catch (e: any) { + catchError(localDiagnostics, e); + } + if (o.copy && o.copy.length > 0) { + config.outputTargets.push({ + type: COPY, + dir: config.rootDir, + copy: [...o.copy], + }); + } + diagnostics.push(...localDiagnostics); + } + return o; + }); +}; diff --git a/packages/core/src/compiler/config/outputs/validate-dist.ts b/packages/core/src/compiler/config/outputs/validate-dist.ts new file mode 100644 index 00000000000..e597740fbf2 --- /dev/null +++ b/packages/core/src/compiler/config/outputs/validate-dist.ts @@ -0,0 +1,174 @@ +import { + COPY, + DIST_COLLECTION, + DIST_GLOBAL_STYLES, + DIST_LAZY, + DIST_LAZY_LOADER, + DIST_TYPES, + getComponentsDtsTypesFilePath, + isBoolean, + isOutputTargetDist, + isString, + join, + resolve, +} from '@utils'; +import { isAbsolute } from 'path'; + +import type * as d from '../../../declarations'; +import { getAbsolutePath } from '../config-utils'; +import { validateCopy } from '../validate-copy'; + +/** + * Validate that the "dist" output targets are valid and ready to go. + * + * This function will also add in additional output targets to its output, based on the input supplied. + * + * @param config the compiler config, what else? + * @param userOutputs a user-supplied list of output targets. + * @returns a list of OutputTargets which have been validated for us. + */ +export const validateDist = (config: d.ValidatedConfig, userOutputs: d.OutputTarget[]): d.OutputTarget[] => { + const distOutputTargets = userOutputs.filter(isOutputTargetDist); + + const outputs: d.OutputTarget[] = []; + + for (const outputTarget of distOutputTargets) { + const distOutputTarget = validateOutputTargetDist(config, outputTarget); + outputs.push(distOutputTarget); + + const namespace = config.fsNamespace || 'app'; + const lazyDir = join(distOutputTarget.buildDir, namespace); + + // Lazy build for CDN in dist + outputs.push({ + type: DIST_LAZY, + esmDir: lazyDir, + systemDir: config.buildEs5 ? lazyDir : undefined, + systemLoaderFile: config.buildEs5 ? join(lazyDir, namespace + '.js') : undefined, + legacyLoaderFile: join(distOutputTarget.buildDir, namespace + '.js'), + polyfills: outputTarget.polyfills !== undefined ? !!distOutputTarget.polyfills : true, + isBrowserBuild: true, + empty: distOutputTarget.empty, + }); + outputs.push({ + type: COPY, + dir: lazyDir, + copyAssets: 'dist', + copy: (distOutputTarget.copy ?? []).concat(), + }); + outputs.push({ + type: DIST_GLOBAL_STYLES, + file: join(lazyDir, `${config.fsNamespace}.css`), + }); + + outputs.push({ + type: DIST_TYPES, + dir: distOutputTarget.dir, + typesDir: distOutputTarget.typesDir, + }); + + if (config.buildDist) { + if (distOutputTarget.collectionDir) { + outputs.push({ + type: DIST_COLLECTION, + dir: distOutputTarget.dir, + collectionDir: distOutputTarget.collectionDir, + empty: distOutputTarget.empty, + transformAliasedImportPaths: distOutputTarget.transformAliasedImportPathsInCollection, + }); + outputs.push({ + type: COPY, + dir: distOutputTarget.collectionDir, + copyAssets: 'collection', + copy: [...distOutputTarget.copy, { src: '**/*.svg' }, { src: '**/*.js' }], + }); + } + + const esmDir = join(distOutputTarget.dir, 'esm'); + const esmEs5Dir = config.buildEs5 ? join(distOutputTarget.dir, 'esm-es5') : undefined; + const cjsDir = join(distOutputTarget.dir, 'cjs'); + + // Create lazy output-target + outputs.push({ + type: DIST_LAZY, + esmDir, + esmEs5Dir, + cjsDir, + + cjsIndexFile: join(distOutputTarget.dir, 'index.cjs.js'), + esmIndexFile: join(distOutputTarget.dir, 'index.js'), + polyfills: true, + empty: distOutputTarget.empty, + }); + + // Create output target that will generate the /loader entry-point + outputs.push({ + type: DIST_LAZY_LOADER, + dir: distOutputTarget.esmLoaderPath, + + esmDir, + esmEs5Dir, + cjsDir, + componentDts: getComponentsDtsTypesFilePath(distOutputTarget), + empty: distOutputTarget.empty, + }); + } + } + + return outputs; +}; + +/** + * Validate that an OutputTargetDist object has what it needs to do it's job. + * To enforce this, we have this function return + * `Required`, giving us a compile-time check that all + * properties are defined (with either user-supplied or default values). + * + * @param config the current config + * @param o the OutputTargetDist object we want to validate + * @returns `Required`, i.e. `d.OutputTargetDist` with all + * optional properties rendered un-optional. + */ +const validateOutputTargetDist = (config: d.ValidatedConfig, o: d.OutputTargetDist): Required => { + // we need to create an object with a bunch of default values here so that + // the typescript compiler can infer their types correctly + const outputTarget = { + ...o, + dir: getAbsolutePath(config, o.dir || DEFAULT_DIR), + buildDir: isString(o.buildDir) ? o.buildDir : DEFAULT_BUILD_DIR, + collectionDir: o.collectionDir !== undefined ? o.collectionDir : DEFAULT_COLLECTION_DIR, + typesDir: o.typesDir || DEFAULT_TYPES_DIR, + esmLoaderPath: o.esmLoaderPath || DEFAULT_ESM_LOADER_DIR, + copy: validateCopy(o.copy ?? [], []), + polyfills: isBoolean(o.polyfills) ? o.polyfills : false, + empty: isBoolean(o.empty) ? o.empty : true, + transformAliasedImportPathsInCollection: isBoolean(o.transformAliasedImportPathsInCollection) + ? o.transformAliasedImportPathsInCollection + : true, + isPrimaryPackageOutputTarget: o.isPrimaryPackageOutputTarget ?? false, + } satisfies Required; + + if (!isAbsolute(outputTarget.buildDir)) { + outputTarget.buildDir = join(outputTarget.dir, outputTarget.buildDir); + } + + if (outputTarget.collectionDir && !isAbsolute(outputTarget.collectionDir)) { + outputTarget.collectionDir = join(outputTarget.dir, outputTarget.collectionDir); + } + + if (!isAbsolute(outputTarget.esmLoaderPath)) { + outputTarget.esmLoaderPath = resolve(outputTarget.dir, outputTarget.esmLoaderPath); + } + + if (!isAbsolute(outputTarget.typesDir)) { + outputTarget.typesDir = join(outputTarget.dir, outputTarget.typesDir); + } + + return outputTarget; +}; + +const DEFAULT_DIR = 'dist'; +const DEFAULT_BUILD_DIR = ''; +const DEFAULT_COLLECTION_DIR = 'collection'; +const DEFAULT_TYPES_DIR = 'types'; +const DEFAULT_ESM_LOADER_DIR = 'loader'; diff --git a/packages/core/src/compiler/config/outputs/validate-docs.ts b/packages/core/src/compiler/config/outputs/validate-docs.ts new file mode 100644 index 00000000000..d27b2e70ca0 --- /dev/null +++ b/packages/core/src/compiler/config/outputs/validate-docs.ts @@ -0,0 +1,137 @@ +import { + buildError, + DOCS_JSON, + DOCS_README, + isFunction, + isOutputTargetDocsCustom, + isOutputTargetDocsCustomElementsManifest, + isOutputTargetDocsJson, + isOutputTargetDocsReadme, + isOutputTargetDocsVscode, + isString, + join, +} from '@utils'; +import { isAbsolute } from 'path'; + +import type * as d from '../../../declarations'; +import { NOTE } from '../../docs/constants'; + +export const validateDocs = (config: d.ValidatedConfig, diagnostics: d.Diagnostic[], userOutputs: d.OutputTarget[]) => { + const docsOutputs: d.OutputTarget[] = []; + + // json docs flag + if (isString(config.flags.docsJson)) { + docsOutputs.push( + validateJsonDocsOutputTarget(config, diagnostics, { + type: DOCS_JSON, + file: config.flags.docsJson, + }), + ); + } + + // json docs + const jsonDocsOutputs = userOutputs.filter(isOutputTargetDocsJson); + jsonDocsOutputs.forEach((jsonDocsOutput) => { + docsOutputs.push(validateJsonDocsOutputTarget(config, diagnostics, jsonDocsOutput)); + }); + + // readme docs flag + if (config.flags.docs || config.flags.task === 'docs') { + if (!userOutputs.some(isOutputTargetDocsReadme)) { + // didn't provide a docs config, so let's add one + docsOutputs.push(validateReadmeOutputTarget(config, { type: DOCS_README })); + } + } + + // readme docs + const readmeDocsOutputs = userOutputs.filter(isOutputTargetDocsReadme); + readmeDocsOutputs.forEach((readmeDocsOutput) => { + docsOutputs.push(validateReadmeOutputTarget(config, readmeDocsOutput)); + }); + + // custom docs + const customDocsOutputs = userOutputs.filter(isOutputTargetDocsCustom); + customDocsOutputs.forEach((jsonDocsOutput) => { + docsOutputs.push(validateCustomDocsOutputTarget(diagnostics, jsonDocsOutput)); + }); + + // vscode docs + const vscodeDocsOutputs = userOutputs.filter(isOutputTargetDocsVscode); + vscodeDocsOutputs.forEach((vscodeDocsOutput) => { + docsOutputs.push(validateVScodeDocsOutputTarget(diagnostics, vscodeDocsOutput)); + }); + + // custom elements manifest docs + const customElementsManifestOutputs = userOutputs.filter(isOutputTargetDocsCustomElementsManifest); + customElementsManifestOutputs.forEach((cemOutput) => { + docsOutputs.push(validateCustomElementsManifestOutputTarget(config, cemOutput)); + }); + + return docsOutputs; +}; + +const validateReadmeOutputTarget = (config: d.ValidatedConfig, outputTarget: d.OutputTargetDocsReadme) => { + if (!isString(outputTarget.dir)) { + outputTarget.dir = config.srcDir; + } + + if (!isAbsolute(outputTarget.dir)) { + outputTarget.dir = join(config.rootDir, outputTarget.dir); + } + + if (outputTarget.footer == null) { + outputTarget.footer = NOTE; + } + outputTarget.strict = !!outputTarget.strict; + return outputTarget; +}; + +const validateJsonDocsOutputTarget = ( + config: d.ValidatedConfig, + diagnostics: d.Diagnostic[], + outputTarget: d.OutputTargetDocsJson, +) => { + if (!isString(outputTarget.file)) { + const err = buildError(diagnostics); + err.messageText = `docs-json outputTarget missing the "file" option`; + } + + outputTarget.file = join(config.rootDir, outputTarget.file); + if (isString(outputTarget.typesFile)) { + outputTarget.typesFile = join(config.rootDir, outputTarget.typesFile); + } else if (outputTarget.typesFile !== null && outputTarget.file.endsWith('.json')) { + outputTarget.typesFile = outputTarget.file.replace(/\.json$/, '.d.ts'); + } + outputTarget.strict = !!outputTarget.strict; + return outputTarget; +}; + +const validateCustomDocsOutputTarget = (diagnostics: d.Diagnostic[], outputTarget: d.OutputTargetDocsCustom) => { + if (!isFunction(outputTarget.generator)) { + const err = buildError(diagnostics); + err.messageText = `docs-custom outputTarget missing the "generator" function`; + } + + outputTarget.strict = !!outputTarget.strict; + return outputTarget; +}; + +const validateVScodeDocsOutputTarget = (diagnostics: d.Diagnostic[], outputTarget: d.OutputTargetDocsVscode) => { + if (!isString(outputTarget.file)) { + const err = buildError(diagnostics); + err.messageText = `docs-vscode outputTarget missing the "file" path`; + } + return outputTarget; +}; + +const validateCustomElementsManifestOutputTarget = ( + config: d.ValidatedConfig, + outputTarget: d.OutputTargetDocsCustomElementsManifest, +) => { + if (!isString(outputTarget.file)) { + outputTarget.file = 'custom-elements.json'; + } + outputTarget.file = join(config.rootDir, outputTarget.file); + outputTarget.strict = !!outputTarget.strict; + return outputTarget; +}; diff --git a/packages/core/src/compiler/config/outputs/validate-hydrate-script.ts b/packages/core/src/compiler/config/outputs/validate-hydrate-script.ts new file mode 100644 index 00000000000..778fa3e31d9 --- /dev/null +++ b/packages/core/src/compiler/config/outputs/validate-hydrate-script.ts @@ -0,0 +1,78 @@ +import { + DIST_HYDRATE_SCRIPT, + isBoolean, + isOutputTargetDist, + isOutputTargetHydrate, + isOutputTargetWww, + isString, + join, +} from '@utils'; +import { isAbsolute } from 'path'; + +import type * as d from '../../../declarations'; + +export const validateHydrateScript = (config: d.ValidatedConfig, userOutputs: d.OutputTarget[]) => { + const output: d.OutputTargetHydrate[] = []; + + const hasHydrateOutputTarget = userOutputs.some(isOutputTargetHydrate); + + if (!hasHydrateOutputTarget) { + // we don't already have a hydrate output target + // let's still see if we require one because of other output targets + + const hasWwwOutput = userOutputs.filter(isOutputTargetWww).some((o) => isString(o.indexHtml)); + const shouldBuildHydrate = config.flags.prerender || config.flags.ssr; + + if (hasWwwOutput && shouldBuildHydrate) { + // we're prerendering a www output target, so we'll need a hydrate app + let hydrateDir: string; + const distOutput = userOutputs.find(isOutputTargetDist); + if (distOutput != null && isString(distOutput.dir)) { + hydrateDir = join(distOutput.dir, 'hydrate'); + } else { + hydrateDir = 'dist/hydrate'; + } + + const hydrateForWwwOutputTarget: d.OutputTargetHydrate = { + type: DIST_HYDRATE_SCRIPT, + dir: hydrateDir, + }; + userOutputs.push(hydrateForWwwOutputTarget); + } + } + + const hydrateOutputTargets = userOutputs.filter(isOutputTargetHydrate); + + hydrateOutputTargets.forEach((outputTarget) => { + if (!isString(outputTarget.dir)) { + // no directory given, see if we've got a dist to go off of + outputTarget.dir = 'hydrate'; + } + + if (!isAbsolute(outputTarget.dir)) { + outputTarget.dir = join(config.rootDir, outputTarget.dir); + } + + if (!isBoolean(outputTarget.empty)) { + outputTarget.empty = true; + } + + if (!isBoolean(outputTarget.minify)) { + outputTarget.minify = false; + } + + if (!isBoolean(outputTarget.generatePackageJson)) { + outputTarget.generatePackageJson = true; + } + + outputTarget.external = outputTarget.external || []; + + outputTarget.external.push('fs'); + outputTarget.external.push('path'); + outputTarget.external.push('crypto'); + + output.push(outputTarget); + }); + + return output; +}; diff --git a/packages/core/src/compiler/config/outputs/validate-lazy.ts b/packages/core/src/compiler/config/outputs/validate-lazy.ts new file mode 100644 index 00000000000..01cf70d7859 --- /dev/null +++ b/packages/core/src/compiler/config/outputs/validate-lazy.ts @@ -0,0 +1,20 @@ +import { DIST_LAZY, isBoolean, isOutputTargetDistLazy, join } from '@utils'; + +import type * as d from '../../../declarations'; +import { getAbsolutePath } from '../config-utils'; + +export const validateLazy = (config: d.ValidatedConfig, userOutputs: d.OutputTarget[]) => { + return userOutputs.filter(isOutputTargetDistLazy).map((o) => { + const dir = getAbsolutePath(config, o.dir || join('dist', config.fsNamespace)); + const lazyOutput: d.OutputTargetDistLazy = { + type: DIST_LAZY, + esmDir: dir, + systemDir: config.buildEs5 ? dir : undefined, + systemLoaderFile: config.buildEs5 ? join(dir, `${config.fsNamespace}.js`) : undefined, + polyfills: !!o.polyfills, + isBrowserBuild: true, + empty: isBoolean(o.empty) ? o.empty : true, + }; + return lazyOutput; + }); +}; diff --git a/packages/core/src/compiler/config/outputs/validate-stats.ts b/packages/core/src/compiler/config/outputs/validate-stats.ts new file mode 100644 index 00000000000..6d5e1ff2a19 --- /dev/null +++ b/packages/core/src/compiler/config/outputs/validate-stats.ts @@ -0,0 +1,37 @@ +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; +}; diff --git a/packages/core/src/compiler/config/outputs/validate-www.ts b/packages/core/src/compiler/config/outputs/validate-www.ts new file mode 100644 index 00000000000..ce9ffe664b1 --- /dev/null +++ b/packages/core/src/compiler/config/outputs/validate-www.ts @@ -0,0 +1,139 @@ +import { + buildError, + COPY, + DIST_GLOBAL_STYLES, + DIST_LAZY, + isBoolean, + isOutputTargetDist, + isOutputTargetWww, + isString, + join, + WWW, +} from '@utils'; +import { isAbsolute } from 'path'; + +import type * as d from '../../../declarations'; +import { getAbsolutePath } from '../config-utils'; +import { validateCopy } from '../validate-copy'; +import { validatePrerender } from '../validate-prerender'; +import { validateServiceWorker } from '../validate-service-worker'; + +export const validateWww = (config: d.ValidatedConfig, diagnostics: d.Diagnostic[], userOutputs: d.OutputTarget[]) => { + const hasOutputTargets = userOutputs.length > 0; + const hasE2eTests = !!config.flags.e2e; + const userWwwOutputs = userOutputs.filter(isOutputTargetWww); + + if ( + !hasOutputTargets || + (hasE2eTests && !userOutputs.some(isOutputTargetWww) && !userOutputs.some(isOutputTargetDist)) + ) { + userWwwOutputs.push({ type: WWW }); + } + + if (config.flags.prerender && userWwwOutputs.length === 0) { + const err = buildError(diagnostics); + err.messageText = `You need at least one "www" output target configured in your stencil.config.ts, when the "--prerender" flag is used`; + } + + return userWwwOutputs.reduce( + ( + outputs: (d.OutputTargetWww | d.OutputTargetDistLazy | d.OutputTargetCopy | d.OutputTargetDistGlobalStyles)[], + o, + ) => { + const outputTarget = validateWwwOutputTarget(config, o, diagnostics); + outputs.push(outputTarget); + + // Add dist-lazy output target + const buildDir = outputTarget.buildDir; + outputs.push({ + type: DIST_LAZY, + dir: buildDir, + esmDir: buildDir, + systemDir: config.buildEs5 ? buildDir : undefined, + systemLoaderFile: config.buildEs5 ? join(buildDir, `${config.fsNamespace}.js`) : undefined, + polyfills: outputTarget.polyfills, + isBrowserBuild: true, + }); + + // Copy for dist + outputs.push({ + type: COPY, + dir: buildDir, + copyAssets: 'dist', + }); + + // Copy for www + outputs.push({ + type: COPY, + dir: outputTarget.appDir, + copy: validateCopy(outputTarget.copy, [ + { src: 'assets', warn: false }, + { src: 'manifest.json', warn: false }, + ]), + }); + + // Generate global style with original name + outputs.push({ + type: DIST_GLOBAL_STYLES, + file: join(buildDir, `${config.fsNamespace}.css`), + }); + + return outputs; + }, + [], + ); +}; + +const validateWwwOutputTarget = ( + config: d.ValidatedConfig, + outputTarget: d.OutputTargetWww, + diagnostics: d.Diagnostic[], +) => { + if (!isString(outputTarget.baseUrl)) { + outputTarget.baseUrl = '/'; + } + + if (!outputTarget.baseUrl.endsWith('/')) { + // Make sure the baseUrl always finish with "/" + outputTarget.baseUrl += '/'; + } + + outputTarget.dir = getAbsolutePath(config, outputTarget.dir || 'www'); + + // Fix "dir" to account + const pathname = new URL(outputTarget.baseUrl, 'http://localhost/').pathname; + outputTarget.appDir = join(outputTarget.dir, pathname); + if (outputTarget.appDir.endsWith('/') || outputTarget.appDir.endsWith('\\')) { + outputTarget.appDir = outputTarget.appDir.substring(0, outputTarget.appDir.length - 1); + } + + if (!isString(outputTarget.buildDir)) { + outputTarget.buildDir = 'build'; + } + + if (!isAbsolute(outputTarget.buildDir)) { + outputTarget.buildDir = join(outputTarget.appDir, outputTarget.buildDir); + } + + if (!isString(outputTarget.indexHtml)) { + outputTarget.indexHtml = 'index.html'; + } + + if (!isAbsolute(outputTarget.indexHtml)) { + outputTarget.indexHtml = join(outputTarget.appDir, outputTarget.indexHtml); + } + + if (!isBoolean(outputTarget.empty)) { + outputTarget.empty = true; + } + + validatePrerender(config, diagnostics, outputTarget); + validateServiceWorker(config, outputTarget); + + if (outputTarget.polyfills === undefined) { + outputTarget.polyfills = true; + } + outputTarget.polyfills = !!outputTarget.polyfills; + + return outputTarget; +}; diff --git a/packages/core/src/compiler/config/test/fixtures/stencil.config.ts b/packages/core/src/compiler/config/test/fixtures/stencil.config.ts new file mode 100644 index 00000000000..4919bf2c0ee --- /dev/null +++ b/packages/core/src/compiler/config/test/fixtures/stencil.config.ts @@ -0,0 +1,5 @@ +import { Config } from '../../../../declarations'; + +export const config: Config = { + hashedFileNameLength: 13, +}; diff --git a/packages/core/src/compiler/config/test/fixtures/stencil.config2.ts b/packages/core/src/compiler/config/test/fixtures/stencil.config2.ts new file mode 100644 index 00000000000..2fd4f087052 --- /dev/null +++ b/packages/core/src/compiler/config/test/fixtures/stencil.config2.ts @@ -0,0 +1,11 @@ +import { Config } from '../../../../declarations'; + +export const config: Config = { + hashedFileNameLength: 27, + flags: { + dev: true, + }, + extras: { + enableImportInjection: true, + }, +}; diff --git a/packages/core/src/compiler/config/test/load-config.spec.ts b/packages/core/src/compiler/config/test/load-config.spec.ts new file mode 100644 index 00000000000..9fd5bb6c2be --- /dev/null +++ b/packages/core/src/compiler/config/test/load-config.spec.ts @@ -0,0 +1,130 @@ +import { mockCompilerSystem } from '@stencil/core/testing'; +import path from 'path'; +import ts from 'typescript'; + +import { ConfigFlags } from '../../../cli/config-flags'; +import type * as d from '../../../declarations'; +import { normalizePath } from '../../../utils'; +import { loadConfig } from '../load-config'; + +describe('load config', () => { + const configPath = require.resolve('./fixtures/stencil.config.ts'); + const configPath2 = require.resolve('./fixtures/stencil.config2.ts'); + + let sys: d.CompilerSystem; + + beforeEach(() => { + sys = mockCompilerSystem(); + + jest.spyOn(ts, 'getParsedCommandLineOfConfigFile').mockReturnValue({ + options: { + target: ts.ScriptTarget.ES2017, + module: ts.ModuleKind.ESNext, + }, + fileNames: [], + errors: [], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("merges a user's configuration with a stencil.config file on disk", async () => { + const loadedConfig = await loadConfig({ + configPath: configPath2, + sys, + config: { + hashedFileNameLength: 9, + rootDir: '/foo/bar', + }, + initTsConfig: true, + }); + + expect(loadedConfig.diagnostics).toHaveLength(0); + + const actualConfig = loadedConfig.config; + // this field is defined on the `init` argument, and should override the value found in the config on disk + expect(actualConfig).toBeDefined(); + expect(actualConfig.hashedFileNameLength).toEqual(9); + // these fields are defined in the config file on disk, and should be present + expect(actualConfig.flags).toEqual({ dev: true }); + expect(actualConfig.extras).toBeDefined(); + expect(actualConfig.extras!.enableImportInjection).toBe(true); + // respects custom root dir + expect(actualConfig.rootDir).toBe('/foo/bar'); + }); + + it('uses the provided config path when no initial config provided', async () => { + const loadedConfig = await loadConfig({ + configPath, + sys, + initTsConfig: true, + }); + + expect(loadedConfig.diagnostics).toHaveLength(0); + + const actualConfig = loadedConfig.config; + expect(actualConfig).toBeDefined(); + // set the config path based on the one provided in the init object + expect(actualConfig.configPath).toBe(normalizePath(configPath)); + // this field is defined in the config file on disk, and should be present + expect(actualConfig.hashedFileNameLength).toBe(13); + // this field should default to an empty object literal, since it wasn't present in the config file + expect(actualConfig.flags).toEqual({}); + }); + + describe('empty initialization argument', () => { + it('provides sensible default values with no config', async () => { + const loadedConfig = await loadConfig({ initTsConfig: true, sys }); + + const actualConfig = loadedConfig.config; + expect(actualConfig).toBeDefined(); + expect(actualConfig.sys).toBeDefined(); + expect(actualConfig.logger).toBeDefined(); + expect(actualConfig.configPath).toBe(null); + }); + + it('creates a tsconfig file when "initTsConfig" set', async () => { + const tsconfigPath = path.resolve(path.dirname(configPath), 'tsconfig.json'); + expect(sys.accessSync(tsconfigPath)).toBe(false); + const loadedConfig = await loadConfig({ initTsConfig: true, configPath, sys }); + expect(sys.accessSync(tsconfigPath)).toBe(true); + expect(loadedConfig.diagnostics).toHaveLength(0); + }); + + it('errors that a tsconfig file could not be created when "initTsConfig" isn\'t present', async () => { + const loadedConfig = await loadConfig({ configPath, sys }); + expect(loadedConfig.diagnostics).toHaveLength(1); + expect(loadedConfig.diagnostics[0]).toEqual({ + absFilePath: undefined, + header: 'Missing tsconfig.json', + level: 'error', + lines: [], + messageText: `Unable to load TypeScript config file. Please create a "tsconfig.json" file within the "${normalizePath( + path.dirname(configPath), + )}" directory.`, + relFilePath: undefined, + type: 'build', + }); + }); + }); + + describe('no initialization argument', () => { + it('errors that a tsconfig file cannot be found', async () => { + const loadConfigResults = await loadConfig({ sys }); + expect(loadConfigResults.diagnostics).toHaveLength(1); + expect(loadConfigResults.diagnostics[0]).toEqual({ + absFilePath: undefined, + header: 'Missing tsconfig.json', + level: 'error', + lines: [], + messageText: expect.stringMatching( + `Unable to load TypeScript config file. Please create a "tsconfig.json" file within the`, + ), + relFilePath: undefined, + type: 'build', + }); + }); + }); +}); diff --git a/packages/core/src/compiler/config/test/tsconfig.json b/packages/core/src/compiler/config/test/tsconfig.json new file mode 100644 index 00000000000..0ff0be83173 --- /dev/null +++ b/packages/core/src/compiler/config/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../testing/tsconfig.internal.json" +} diff --git a/packages/core/src/compiler/config/test/validate-config-sourcemap.spec.ts b/packages/core/src/compiler/config/test/validate-config-sourcemap.spec.ts new file mode 100644 index 00000000000..d82b4b47026 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-config-sourcemap.spec.ts @@ -0,0 +1,140 @@ +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); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-config.spec.ts b/packages/core/src/compiler/config/test/validate-config.spec.ts new file mode 100644 index 00000000000..391ae9dec4a --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-config.spec.ts @@ -0,0 +1,629 @@ +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(true); + expect(config.extras.additionalTagTransformers).toBe(false); + }); + + describe('extras.additionalTagTransformers', () => { + it('set extras.additionalTagTransformers false', () => { + userConfig.extras = { additionalTagTransformers: false }; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.additionalTagTransformers).toBe(false); + }); + + it('set extras.additionalTagTransformers true', () => { + userConfig.extras = { additionalTagTransformers: true }; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.additionalTagTransformers).toBe(true); + }); + + it('set extras.additionalTagTransformers true, dev mode', () => { + userConfig.devMode = true; + userConfig.extras = { additionalTagTransformers: true }; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.additionalTagTransformers).toBe(true); + }); + + it('prod mode, set extras.additionalTagTransformers', () => { + userConfig.devMode = false; + userConfig.extras = { additionalTagTransformers: true }; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.additionalTagTransformers).toBe(true); + }); + + it('build extras.additionalTagTransformers when set to "prod" and in prod', () => { + userConfig.devMode = false; + userConfig.extras = { additionalTagTransformers: 'prod' }; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.additionalTagTransformers).toBe(true); + }); + + it('do not build extras.additionalTagTransformers when set to "prod" and in dev', () => { + userConfig.devMode = true; + userConfig.extras = { additionalTagTransformers: 'prod' }; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.additionalTagTransformers).toBe(false); + }); + + it('prod mode default to only modern and not extras.additionalTagTransformers', () => { + userConfig.devMode = false; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.additionalTagTransformers).toBe(false); + }); + }); + + it('should set slot config based on `experimentalSlotFixes`', () => { + userConfig.extras = {}; + userConfig.extras.experimentalSlotFixes = true; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.appendChildSlotFix).toBe(true); + expect(config.extras.cloneNodeFix).toBe(true); + expect(config.extras.slotChildNodesFix).toBe(true); + expect(config.extras.scopedSlotTextContentFix).toBe(true); + }); + + it('should override slot fix config based on `experimentalSlotFixes`', () => { + // This test is to verify the flags get overwritten correctly even if an + // invalid config is ingested. Hence, the `any` cast + userConfig.extras = { + appendChildSlotFix: false, + slotChildNodesFix: false, + cloneNodeFix: false, + scopedSlotTextContentFix: false, + experimentalSlotFixes: true, + } as any; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.appendChildSlotFix).toBe(true); + expect(config.extras.cloneNodeFix).toBe(true); + expect(config.extras.slotChildNodesFix).toBe(true); + expect(config.extras.scopedSlotTextContentFix).toBe(true); + }); + + it('should set extras experimentalScopedSlotChanges `true` if set in user config', () => { + userConfig.extras = { + experimentalScopedSlotChanges: true, + }; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.extras.experimentalScopedSlotChanges).toBe(true); + }); + + it('should set taskQueue "async" by default', () => { + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.taskQueue).toBe('async'); + }); + + it('should set taskQueue', () => { + userConfig.taskQueue = 'congestionAsync'; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.taskQueue).toBe('congestionAsync'); + }); + + it('empty watchIgnoredRegex, all valid', () => { + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.watchIgnoredRegex).toEqual([]); + expect(isWatchIgnorePath(config, '/some/image.gif')).toBe(false); + expect(isWatchIgnorePath(config, '/some/typescript.ts')).toBe(false); + }); + + it('should change a single watchIgnoredRegex to an array', () => { + userConfig.watchIgnoredRegex = /\.(gif|jpe?g|png)$/i; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.watchIgnoredRegex).toHaveLength(1); + expect((config.watchIgnoredRegex as any[])[0]).toEqual(/\.(gif|jpe?g|png)$/i); + expect(isWatchIgnorePath(config, '/some/image.gif')).toBe(true); + expect(isWatchIgnorePath(config, '/some/typescript.ts')).toBe(false); + }); + + it('should clean up valid watchIgnoredRegex', () => { + userConfig.watchIgnoredRegex = [/\.(gif|jpe?g)$/i, null, 'me-regex' as any, /\.(png)$/i]; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.watchIgnoredRegex).toHaveLength(2); + expect((config.watchIgnoredRegex as any[])[0]).toEqual(/\.(gif|jpe?g)$/i); + expect((config.watchIgnoredRegex as any[])[1]).toEqual(/\.(png)$/i); + expect(isWatchIgnorePath(config, '/some/image.gif')).toBe(true); + expect(isWatchIgnorePath(config, '/some/image.jpg')).toBe(true); + expect(isWatchIgnorePath(config, '/some/image.png')).toBe(true); + expect(isWatchIgnorePath(config, '/some/typescript.ts')).toBe(false); + }); + + describe('sourceMap', () => { + it('sets the field to true when the set to true in the config', () => { + userConfig.sourceMap = true; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.sourceMap).toBe(true); + }); + + it('sets the field to false when set to false in the config', () => { + userConfig.sourceMap = false; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.sourceMap).toBe(false); + }); + + it('defaults to "dev" behavior when not set (true in dev mode)', () => { + userConfig.devMode = true; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.sourceMap).toBe(true); + }); + + it('defaults to "dev" behavior when not set (false in prod mode)', () => { + userConfig.devMode = false; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.sourceMap).toBe(false); + }); + + it('sets the field to true when set to "dev" and devMode is true', () => { + userConfig.sourceMap = 'dev'; + userConfig.devMode = true; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.sourceMap).toBe(true); + }); + + it('sets the field to false when set to "dev" and devMode is false', () => { + userConfig.sourceMap = 'dev'; + userConfig.devMode = false; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.sourceMap).toBe(false); + }); + + it('sets the field to true when set to "dev" and --dev flag is passed', () => { + userConfig.sourceMap = 'dev'; + userConfig.flags = createConfigFlags({ dev: true }); + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.sourceMap).toBe(true); + }); + + it('sets the field to false when set to "dev" and --prod flag is passed', () => { + userConfig.sourceMap = 'dev'; + userConfig.flags = createConfigFlags({ prod: true }); + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.sourceMap).toBe(false); + }); + }); + + describe('buildDist', () => { + it.each([true, false])('should set the field based on the config flag (%p)', (flag) => { + userConfig.flags = createConfigFlags({ esm: flag }); + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.buildDist).toBe(flag); + }); + + it.each([true, false])('should fallback to !devMode', (devMode) => { + userConfig.devMode = devMode; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.buildDist).toBe(!devMode); + }); + + it.each([true, false])('should fallback to buildEs5 in devMode', (buildEs5) => { + userConfig.devMode = true; + userConfig.buildEs5 = buildEs5; + const { config } = validateConfig(userConfig, bootstrapConfig); + expect(config.buildDist).toBe(config.buildEs5); + }); + }); + + describe('validatePrimaryPackageOutputTarget', () => { + it('should default to false', () => { + const { config } = validateConfig(userConfig, bootstrapConfig); + + expect(config.validatePrimaryPackageOutputTarget).toBe(false); + }); + + it.each([true, false])( + 'should set validatePrimaryPackageOutputTarget to %p', + (validatePrimaryPackageOutputTarget) => { + userConfig.validatePrimaryPackageOutputTarget = validatePrimaryPackageOutputTarget; + + const { config } = validateConfig(userConfig, bootstrapConfig); + + expect(config.validatePrimaryPackageOutputTarget).toBe(validatePrimaryPackageOutputTarget); + }, + ); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-copy.spec.ts b/packages/core/src/compiler/config/test/validate-copy.spec.ts new file mode 100644 index 00000000000..4d69a64d663 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-copy.spec.ts @@ -0,0 +1,52 @@ +import type * as d from '../../../declarations'; +import { validateCopy } from '../validate-copy'; + +describe('validate-copy', () => { + describe('validateCopy', () => { + it.each([false, null, undefined, []])('returns an empty array when the copy task is `%s`', (copyValue) => { + expect(validateCopy(copyValue, [])).toEqual([]); + }); + + it('pushes default tasks not found in the original copy list', () => { + const defaultCopyTasks: d.CopyTask[] = [ + { src: 'defaultSrc' }, + { src: 'anotherDefaultSrc', dest: 'anotherDefaultDest' }, + ]; + + expect(validateCopy([], defaultCopyTasks)).toEqual(defaultCopyTasks); + }); + + it('combines provided and default tasks', () => { + const tasksToValidate: d.CopyTask[] = [{ src: 'someSrc', dest: 'someDest', keepDirStructure: true, warn: false }]; + const defaultCopyTasks: d.CopyTask[] = [ + { src: 'defaultSrc' }, + { src: 'anotherDefaultSrc', dest: 'anotherDefaultDest' }, + ]; + + expect(validateCopy(tasksToValidate, defaultCopyTasks)).toEqual([...tasksToValidate, ...defaultCopyTasks]); + }); + + it('prefers provided tasks over default tasks', () => { + const tasksToValidate: d.CopyTask[] = [ + { src: 'aDuplicateSrc', dest: 'someDest', keepDirStructure: true, warn: false }, + ]; + const defaultCopyTasks: d.CopyTask[] = [ + { src: 'aDuplicateSrc' }, + { src: 'anotherDefaultSrc', dest: 'anotherDefaultDest' }, + ]; + + // the first task from the default task list is not used + expect(validateCopy(tasksToValidate, defaultCopyTasks)).toEqual([ + { src: 'aDuplicateSrc', dest: 'someDest', keepDirStructure: true, warn: false }, + { src: 'anotherDefaultSrc', dest: 'anotherDefaultDest' }, + ]); + }); + + it('de-duplicates copy tasks', () => { + const copyTask: d.CopyTask = { src: 'aDuplicateSrc', dest: 'someDest', keepDirStructure: true, warn: false }; + const tasksToValidate: d.CopyTask[] = [{ ...copyTask }, { ...copyTask }]; + + expect(validateCopy(tasksToValidate, [])).toEqual([{ ...copyTask }]); + }); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-custom.spec.ts b/packages/core/src/compiler/config/test/validate-custom.spec.ts new file mode 100644 index 00000000000..6a1e7c79769 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-custom.spec.ts @@ -0,0 +1,49 @@ +import type * as d from '@stencil/core/declarations'; +import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; +import { buildWarn } from '@utils'; + +import { validateConfig } from '../validate-config'; + +describe('validateCustom', () => { + let userConfig: d.Config; + + beforeEach(() => { + userConfig = mockConfig(); + }); + + it('should log warning', () => { + userConfig.outputTargets = [ + { + type: 'custom', + name: 'test', + validate: (_, diagnostics) => { + const warn = buildWarn(diagnostics); + warn.messageText = 'test warning'; + }, + generator: async () => { + return; + }, + }, + ]; + const { diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); + // TODO(STENCIL-1107): Decrement the right-hand side value from 2 to 1 + expect(diagnostics.length).toBe(2); + // TODO(STENCIL-1107): Keep this assertion + expect(diagnostics[0]).toEqual({ + header: 'Build Warn', + level: 'warn', + lines: [], + messageText: 'test warning', + type: 'build', + }); + // TODO(STENCIL-1107): Remove this assertion + expect(diagnostics[1]).toEqual({ + header: 'Build Warn', + level: 'warn', + lines: [], + messageText: + 'nodeResolve.customResolveOptions is a deprecated option in a Stencil Configuration file. If you need this option, please open a new issue in the Stencil repository (https://github.com/stenciljs/core/issues/new/choose)', + type: 'build', + }); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-dev-server.spec.ts b/packages/core/src/compiler/config/test/validate-dev-server.spec.ts new file mode 100644 index 00000000000..57aba93a547 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-dev-server.spec.ts @@ -0,0 +1,324 @@ +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); + }); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-docs.spec.ts b/packages/core/src/compiler/config/test/validate-docs.spec.ts new file mode 100644 index 00000000000..730fb0b632d --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-docs.spec.ts @@ -0,0 +1,72 @@ +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); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-hydrated.spec.ts b/packages/core/src/compiler/config/test/validate-hydrated.spec.ts new file mode 100644 index 00000000000..6205e63fb4a --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-hydrated.spec.ts @@ -0,0 +1,37 @@ +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', + }); + }); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-namespace.spec.ts b/packages/core/src/compiler/config/test/validate-namespace.spec.ts new file mode 100644 index 00000000000..142c0dff205 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-namespace.spec.ts @@ -0,0 +1,80 @@ +import type * as d from '@stencil/core/declarations'; + +import { validateNamespace } from '../validate-namespace'; + +// TODO(STENCIL-968): Update tests to check diagnostic messages +describe('validateNamespace', () => { + const diagnostics: d.Diagnostic[] = []; + + beforeEach(() => { + diagnostics.length = 0; + }); + + it('should not allow special characters in namespace', () => { + validateNamespace('My/Namespace', undefined, diagnostics); + expect(diagnostics).toHaveLength(1); + + diagnostics.length = 0; + validateNamespace('My%20Namespace', undefined, diagnostics); + expect(diagnostics).toHaveLength(1); + + diagnostics.length = 0; + validateNamespace('My:Namespace', undefined, diagnostics); + expect(diagnostics).toHaveLength(1); + }); + + it('should not allow spaces in namespace', () => { + validateNamespace('My Namespace', undefined, diagnostics); + expect(diagnostics).toHaveLength(1); + }); + + it('should not allow dash for last character of namespace', () => { + validateNamespace('MyNamespace-', undefined, diagnostics); + expect(diagnostics).toHaveLength(1); + }); + + it('should not allow dash for first character of namespace', () => { + validateNamespace('-MyNamespace', undefined, diagnostics); + expect(diagnostics).toHaveLength(1); + }); + + it('should not allow number for first character of namespace', () => { + validateNamespace('88MyNamespace', undefined, diagnostics); + expect(diagnostics).toHaveLength(1); + }); + + it('should enforce namespace being at least 3 characters', () => { + validateNamespace('ab', undefined, diagnostics); + expect(diagnostics).toHaveLength(1); + }); + + it('should allow $ in the namespace', () => { + const { namespace, fsNamespace } = validateNamespace('$MyNamespace', undefined, diagnostics); + expect(namespace).toBe('$MyNamespace'); + expect(fsNamespace).toBe('$mynamespace'); + }); + + it('should allow underscore in the namespace', () => { + const { namespace, fsNamespace } = validateNamespace('My_Namespace', undefined, diagnostics); + expect(namespace).toBe('My_Namespace'); + expect(fsNamespace).toBe('my_namespace'); + }); + + it('should allow dash in the namespace', () => { + const { namespace, fsNamespace } = validateNamespace('My-Namespace', undefined, diagnostics); + expect(namespace).toBe('MyNamespace'); + expect(fsNamespace).toBe('my-namespace'); + }); + + it('should set user namespace', () => { + const { namespace, fsNamespace } = validateNamespace('MyNamespace', undefined, diagnostics); + expect(namespace).toBe('MyNamespace'); + expect(fsNamespace).toBe('mynamespace'); + }); + + it('should set default namespace', () => { + const { namespace, fsNamespace } = validateNamespace(undefined, undefined, diagnostics); + expect(namespace).toBe('App'); + expect(fsNamespace).toBe('app'); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-output-dist-collection.spec.ts b/packages/core/src/compiler/config/test/validate-output-dist-collection.spec.ts new file mode 100644 index 00000000000..d2f7082bddc --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-output-dist-collection.spec.ts @@ -0,0 +1,88 @@ +import type * as d from '@stencil/core/declarations'; +import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; +import { join, resolve } from '@utils'; + +import { validateConfig } from '../validate-config'; + +describe('validateDistCollectionOutputTarget', () => { + let config: d.Config; + + const rootDir = resolve('/'); + const defaultDir = join(rootDir, 'dist', 'collection'); + + beforeEach(() => { + config = mockConfig(); + }); + + it('sets correct default values', () => { + const target: d.OutputTargetDistCollection = { + type: 'dist-collection', + empty: false, + dir: null, + collectionDir: null, + }; + config.outputTargets = [target]; + + const { config: validatedConfig } = validateConfig(config, mockLoadConfigInit()); + + expect(validatedConfig.outputTargets).toEqual([ + { + type: 'dist-collection', + empty: false, + dir: defaultDir, + collectionDir: null, + transformAliasedImportPaths: true, + }, + ]); + }); + + it('sets specified directory', () => { + const target: d.OutputTargetDistCollection = { + type: 'dist-collection', + empty: false, + dir: '/my-dist', + collectionDir: null, + }; + config.outputTargets = [target]; + + const { config: validatedConfig } = validateConfig(config, mockLoadConfigInit()); + + expect(validatedConfig.outputTargets).toEqual([ + { + type: 'dist-collection', + empty: false, + dir: '/my-dist', + collectionDir: null, + transformAliasedImportPaths: true, + }, + ]); + }); + + describe('transformAliasedImportPaths', () => { + it.each([false, true])( + "sets option '%s' when explicitly '%s' in config", + (transformAliasedImportPaths: boolean) => { + const target: d.OutputTargetDistCollection = { + type: 'dist-collection', + empty: false, + dir: null, + collectionDir: null, + transformAliasedImportPaths, + }; + config.outputTargets = [target]; + + const { config: validatedConfig } = validateConfig(config, mockLoadConfigInit()); + + expect(validatedConfig.outputTargets).toEqual([ + { + type: 'dist-collection', + empty: false, + dir: defaultDir, + collectionDir: null, + transformAliasedImportPaths, + }, + ]); + }, + ); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-output-dist-custom-element.spec.ts b/packages/core/src/compiler/config/test/validate-output-dist-custom-element.spec.ts new file mode 100644 index 00000000000..d72dddecfdf --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-output-dist-custom-element.spec.ts @@ -0,0 +1,480 @@ +import type * as d from '@stencil/core/declarations'; +import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; +import { COPY, DIST_CUSTOM_ELEMENTS, DIST_TYPES, join } from '@utils'; +import path from 'path'; + +import { validateConfig } from '../validate-config'; + +describe('validate-output-dist-custom-element', () => { + describe('validateCustomElement', () => { + // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) + const rootDir = path.resolve('/'); + const defaultDistDir = join(rootDir, 'dist', 'components'); + const distCustomElementsDir = 'my-dist-custom-elements'; + let userConfig: d.Config; + + beforeEach(() => { + userConfig = mockConfig(); + }); + + it('generates a default dist-custom-elements output target', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_TYPES, + dir: defaultDistDir, + typesDir: join(rootDir, 'dist', 'types'), + }, + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: true, + externalRuntime: true, + generateTypeDeclarations: true, + customElementsExportBehavior: 'default', + }, + ]); + }); + + it('uses a provided export behavior over the default value', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + customElementsExportBehavior: 'single-export-module', + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_TYPES, + dir: defaultDistDir, + typesDir: join(rootDir, 'dist', 'types'), + }, + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: true, + externalRuntime: true, + generateTypeDeclarations: true, + customElementsExportBehavior: 'single-export-module', + }, + ]); + }); + + it('uses the default export behavior if the specified value is invalid', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + customElementsExportBehavior: 'not-a-valid-option' as d.CustomElementsExportBehavior, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_TYPES, + dir: defaultDistDir, + typesDir: join(rootDir, 'dist', 'types'), + }, + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: true, + externalRuntime: true, + generateTypeDeclarations: true, + customElementsExportBehavior: 'default', + }, + ]); + }); + + it('uses a provided dir field over a default directory', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + dir: distCustomElementsDir, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: join(rootDir, distCustomElementsDir), + empty: true, + externalRuntime: true, + generateTypeDeclarations: false, + customElementsExportBehavior: 'default', + }, + ]); + }); + + describe('"empty" field', () => { + it('defaults the "empty" field to true if not provided', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + externalRuntime: false, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: true, + externalRuntime: false, + generateTypeDeclarations: false, + customElementsExportBehavior: 'default', + }, + ]); + }); + + it('defaults the "empty" field to true it\'s not a boolean', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + empty: undefined, + externalRuntime: false, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: true, + externalRuntime: false, + generateTypeDeclarations: false, + customElementsExportBehavior: 'default', + }, + ]); + }); + }); + + describe('"externalRuntime" field', () => { + it('defaults the "externalRuntime" field to true if not provided', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + empty: false, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: false, + externalRuntime: true, + generateTypeDeclarations: false, + customElementsExportBehavior: 'default', + }, + ]); + }); + + it('defaults the "externalRuntime" field to true it\'s not a boolean', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + empty: false, + externalRuntime: undefined, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: false, + externalRuntime: true, + generateTypeDeclarations: false, + customElementsExportBehavior: 'default', + }, + ]); + }); + }); + + describe('"generateTypeDeclarations" field', () => { + it('defaults the "generateTypeDeclarations" field to true if not provided', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + empty: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_TYPES, + dir: defaultDistDir, + typesDir: join(rootDir, 'dist', 'types'), + }, + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: false, + externalRuntime: true, + generateTypeDeclarations: true, + customElementsExportBehavior: 'default', + }, + ]); + }); + + it('defaults the "generateTypeDeclarations" field to true it\'s not a boolean', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + empty: false, + generateTypeDeclarations: undefined, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_TYPES, + dir: defaultDistDir, + typesDir: join(rootDir, 'dist', 'types'), + }, + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: false, + externalRuntime: true, + generateTypeDeclarations: true, + customElementsExportBehavior: 'default', + }, + ]); + }); + + it('creates a types directory when "generateTypeDeclarations" is true', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + empty: false, + externalRuntime: false, + generateTypeDeclarations: true, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_TYPES, + dir: defaultDistDir, + typesDir: join(rootDir, 'dist', 'types'), + }, + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: false, + externalRuntime: false, + generateTypeDeclarations: true, + customElementsExportBehavior: 'default', + }, + ]); + }); + + it('creates a types directory for a custom directory', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + dir: distCustomElementsDir, + empty: false, + externalRuntime: false, + generateTypeDeclarations: true, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_TYPES, + dir: join(rootDir, distCustomElementsDir), + typesDir: join(rootDir, 'dist', 'types'), + }, + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: join(rootDir, distCustomElementsDir), + empty: false, + externalRuntime: false, + generateTypeDeclarations: true, + customElementsExportBehavior: 'default', + }, + ]); + }); + + it('doesn\'t create a types directory when "generateTypeDeclarations" is false', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + empty: false, + externalRuntime: false, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: DIST_CUSTOM_ELEMENTS, + copy: [], + dir: defaultDistDir, + empty: false, + externalRuntime: false, + generateTypeDeclarations: false, + customElementsExportBehavior: 'default', + }, + ]); + }); + }); + + describe('copy tasks', () => { + it('copies existing copy tasks over to the output target', () => { + const copyOutputTarget: d.CopyTask = { + src: 'mock/src', + dest: 'mock/dest', + }; + const copyOutputTarget2: d.CopyTask = { + src: 'mock/src2', + dest: 'mock/dest2', + }; + + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + copy: [copyOutputTarget, copyOutputTarget2], + dir: distCustomElementsDir, + empty: false, + externalRuntime: false, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + type: COPY, + dir: rootDir, + copy: [copyOutputTarget, copyOutputTarget2], + }, + { + type: DIST_CUSTOM_ELEMENTS, + copy: [copyOutputTarget, copyOutputTarget2], + dir: join(rootDir, distCustomElementsDir), + empty: false, + externalRuntime: false, + generateTypeDeclarations: false, + customElementsExportBehavior: 'default', + }, + ]); + }); + }); + + describe('"autoLoader" field', () => { + it('normalizes autoLoader: true to an object with defaults', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + autoLoader: true, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const distCustomElementsTarget = config.outputTargets.find( + (o) => o.type === DIST_CUSTOM_ELEMENTS, + ) as d.OutputTargetDistCustomElements; + + expect(distCustomElementsTarget.autoLoader).toEqual({ + fileName: 'loader', + autoStart: true, + }); + }); + + it('normalizes autoLoader object with partial options', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + autoLoader: { fileName: 'my-loader' }, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const distCustomElementsTarget = config.outputTargets.find( + (o) => o.type === DIST_CUSTOM_ELEMENTS, + ) as d.OutputTargetDistCustomElements; + + expect(distCustomElementsTarget.autoLoader).toEqual({ + fileName: 'my-loader', + autoStart: true, + }); + }); + + it('normalizes autoLoader object with autoStart: false', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + autoLoader: { autoStart: false }, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const distCustomElementsTarget = config.outputTargets.find( + (o) => o.type === DIST_CUSTOM_ELEMENTS, + ) as d.OutputTargetDistCustomElements; + + expect(distCustomElementsTarget.autoLoader).toEqual({ + fileName: 'loader', + autoStart: false, + }); + }); + + it('does not set autoLoader when not provided', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const distCustomElementsTarget = config.outputTargets.find( + (o) => o.type === DIST_CUSTOM_ELEMENTS, + ) as d.OutputTargetDistCustomElements; + + expect(distCustomElementsTarget.autoLoader).toBeUndefined(); + }); + + it('does not set autoLoader when explicitly false', () => { + const outputTarget: d.OutputTargetDistCustomElements = { + type: DIST_CUSTOM_ELEMENTS, + autoLoader: false, + generateTypeDeclarations: false, + }; + userConfig.outputTargets = [outputTarget]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const distCustomElementsTarget = config.outputTargets.find( + (o) => o.type === DIST_CUSTOM_ELEMENTS, + ) as d.OutputTargetDistCustomElements; + + expect(distCustomElementsTarget.autoLoader).toBe(false); + }); + }); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-output-dist.spec.ts b/packages/core/src/compiler/config/test/validate-output-dist.spec.ts new file mode 100644 index 00000000000..0d0274892f9 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-output-dist.spec.ts @@ -0,0 +1,291 @@ +import type * as d from '@stencil/core/declarations'; +import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; +import { join } from '@utils'; +import path from 'path'; + +import { validateConfig } from '../validate-config'; + +describe('validateDistOutputTarget', () => { + // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) + const rootDir = path.resolve('/'); + + let userConfig: d.Config; + beforeEach(() => { + userConfig = mockConfig({ fsNamespace: 'testing' }); + }); + + it('should set dist values', () => { + const outputTarget: d.OutputTargetDist = { + type: 'dist', + dir: 'my-dist', + buildDir: 'my-build', + empty: false, + }; + userConfig.outputTargets = [outputTarget]; + userConfig.buildDist = true; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toEqual([ + { + buildDir: join(rootDir, 'my-dist', 'my-build'), + collectionDir: join(rootDir, 'my-dist', 'collection'), + copy: [], + dir: join(rootDir, 'my-dist'), + empty: false, + esmLoaderPath: join(rootDir, 'my-dist', 'loader'), + type: 'dist', + polyfills: false, + typesDir: join(rootDir, 'my-dist', 'types'), + transformAliasedImportPathsInCollection: true, + isPrimaryPackageOutputTarget: false, + }, + { + esmDir: join(rootDir, 'my-dist', 'my-build', 'testing'), + empty: false, + isBrowserBuild: true, + legacyLoaderFile: join(rootDir, 'my-dist', 'my-build', 'testing.js'), + polyfills: true, + systemDir: undefined, + systemLoaderFile: undefined, + type: 'dist-lazy', + }, + { + copyAssets: 'dist', + copy: [], + dir: join(rootDir, 'my-dist', 'my-build', 'testing'), + type: 'copy', + }, + { + file: join(rootDir, 'my-dist', 'my-build', 'testing', 'testing.css'), + type: 'dist-global-styles', + }, + { + dir: join(rootDir, 'my-dist'), + type: 'dist-types', + typesDir: join(rootDir, 'my-dist', 'types'), + }, + { + collectionDir: join(rootDir, 'my-dist', 'collection'), + dir: join(rootDir, '/my-dist'), + empty: false, + transformAliasedImportPaths: true, + type: 'dist-collection', + }, + { + copy: [{ src: '**/*.svg' }, { src: '**/*.js' }], + copyAssets: 'collection', + dir: join(rootDir, 'my-dist', 'collection'), + type: 'copy', + }, + { + type: 'dist-lazy', + cjsDir: join(rootDir, 'my-dist', 'cjs'), + cjsIndexFile: join(rootDir, 'my-dist', 'index.cjs.js'), + empty: false, + esmDir: join(rootDir, 'my-dist', 'esm'), + esmEs5Dir: undefined, + esmIndexFile: join(rootDir, 'my-dist', 'index.js'), + polyfills: true, + }, + { + cjsDir: join(rootDir, 'my-dist', 'cjs'), + componentDts: join(rootDir, 'my-dist', 'types', 'components.d.ts'), + dir: join(rootDir, 'my-dist', 'loader'), + empty: false, + esmDir: join(rootDir, 'my-dist', 'esm'), + esmEs5Dir: undefined, + type: 'dist-lazy-loader', + }, + ]); + }); + + it('should set defaults when outputTargets dist is empty', () => { + userConfig.outputTargets = [{ type: 'dist' }]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const outputTarget = config.outputTargets.find((o) => o.type === 'dist') as d.OutputTargetDist; + expect(outputTarget).toBeDefined(); + expect(outputTarget.dir).toBe(join(rootDir, 'dist')); + expect(outputTarget.buildDir).toBe(join(rootDir, '/dist')); + expect(outputTarget.empty).toBe(true); + }); + + it('should default to not add dist when outputTargets exists, but without dist', () => { + userConfig.outputTargets = []; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets.some((o) => o.type === 'dist')).toBe(false); + }); + + it('sets option to transform aliased import paths when enabled', () => { + const outputTarget: d.OutputTargetDist = { + type: 'dist', + dir: 'my-dist', + buildDir: 'my-build', + empty: false, + transformAliasedImportPathsInCollection: true, + }; + userConfig.outputTargets = [outputTarget]; + userConfig.buildDist = true; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.outputTargets).toEqual([ + { + buildDir: join(rootDir, 'my-dist', 'my-build'), + collectionDir: join(rootDir, 'my-dist', 'collection'), + copy: [], + dir: join(rootDir, 'my-dist'), + empty: false, + esmLoaderPath: join(rootDir, 'my-dist', 'loader'), + type: 'dist', + polyfills: false, + typesDir: join(rootDir, 'my-dist', 'types'), + transformAliasedImportPathsInCollection: true, + isPrimaryPackageOutputTarget: false, + }, + { + esmDir: join(rootDir, 'my-dist', 'my-build', 'testing'), + empty: false, + isBrowserBuild: true, + legacyLoaderFile: join(rootDir, 'my-dist', 'my-build', 'testing.js'), + polyfills: true, + systemDir: undefined, + systemLoaderFile: undefined, + type: 'dist-lazy', + }, + { + copyAssets: 'dist', + copy: [], + dir: join(rootDir, 'my-dist', 'my-build', 'testing'), + type: 'copy', + }, + { + file: join(rootDir, 'my-dist', 'my-build', 'testing', 'testing.css'), + type: 'dist-global-styles', + }, + { + dir: join(rootDir, 'my-dist'), + type: 'dist-types', + typesDir: join(rootDir, 'my-dist', 'types'), + }, + { + collectionDir: join(rootDir, 'my-dist', 'collection'), + dir: join(rootDir, '/my-dist'), + empty: false, + transformAliasedImportPaths: true, + type: 'dist-collection', + }, + { + copy: [{ src: '**/*.svg' }, { src: '**/*.js' }], + copyAssets: 'collection', + dir: join(rootDir, 'my-dist', 'collection'), + type: 'copy', + }, + { + type: 'dist-lazy', + cjsDir: join(rootDir, 'my-dist', 'cjs'), + cjsIndexFile: join(rootDir, 'my-dist', 'index.cjs.js'), + empty: false, + esmDir: join(rootDir, 'my-dist', 'esm'), + esmEs5Dir: undefined, + esmIndexFile: join(rootDir, 'my-dist', 'index.js'), + polyfills: true, + }, + { + cjsDir: join(rootDir, 'my-dist', 'cjs'), + componentDts: join(rootDir, 'my-dist', 'types', 'components.d.ts'), + dir: join(rootDir, 'my-dist', 'loader'), + empty: false, + esmDir: join(rootDir, 'my-dist', 'esm'), + esmEs5Dir: undefined, + type: 'dist-lazy-loader', + }, + ]); + }); + + it('sets option to validate primary package output target when enabled', () => { + const outputTarget: d.OutputTargetDist = { + type: 'dist', + dir: 'my-dist', + buildDir: 'my-build', + empty: false, + isPrimaryPackageOutputTarget: true, + }; + userConfig.outputTargets = [outputTarget]; + userConfig.buildDist = true; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.outputTargets).toEqual([ + { + buildDir: join(rootDir, 'my-dist', 'my-build'), + collectionDir: join(rootDir, 'my-dist', 'collection'), + copy: [], + dir: join(rootDir, 'my-dist'), + empty: false, + esmLoaderPath: join(rootDir, 'my-dist', 'loader'), + type: 'dist', + polyfills: false, + typesDir: join(rootDir, 'my-dist', 'types'), + transformAliasedImportPathsInCollection: true, + isPrimaryPackageOutputTarget: true, + }, + { + esmDir: join(rootDir, 'my-dist', 'my-build', 'testing'), + empty: false, + isBrowserBuild: true, + legacyLoaderFile: join(rootDir, 'my-dist', 'my-build', 'testing.js'), + polyfills: true, + systemDir: undefined, + systemLoaderFile: undefined, + type: 'dist-lazy', + }, + { + copyAssets: 'dist', + copy: [], + dir: join(rootDir, 'my-dist', 'my-build', 'testing'), + type: 'copy', + }, + { + file: join(rootDir, 'my-dist', 'my-build', 'testing', 'testing.css'), + type: 'dist-global-styles', + }, + { + dir: join(rootDir, 'my-dist'), + type: 'dist-types', + typesDir: join(rootDir, 'my-dist', 'types'), + }, + { + collectionDir: join(rootDir, 'my-dist', 'collection'), + dir: join(rootDir, '/my-dist'), + empty: false, + transformAliasedImportPaths: true, + type: 'dist-collection', + }, + { + copy: [{ src: '**/*.svg' }, { src: '**/*.js' }], + copyAssets: 'collection', + dir: join(rootDir, 'my-dist', 'collection'), + type: 'copy', + }, + { + type: 'dist-lazy', + cjsDir: join(rootDir, 'my-dist', 'cjs'), + cjsIndexFile: join(rootDir, 'my-dist', 'index.cjs.js'), + empty: false, + esmDir: join(rootDir, 'my-dist', 'esm'), + esmEs5Dir: undefined, + esmIndexFile: join(rootDir, 'my-dist', 'index.js'), + polyfills: true, + }, + { + cjsDir: join(rootDir, 'my-dist', 'cjs'), + componentDts: join(rootDir, 'my-dist', 'types', 'components.d.ts'), + dir: join(rootDir, 'my-dist', 'loader'), + empty: false, + esmDir: join(rootDir, 'my-dist', 'esm'), + esmEs5Dir: undefined, + type: 'dist-lazy-loader', + }, + ]); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-output-www.spec.ts b/packages/core/src/compiler/config/test/validate-output-www.spec.ts new file mode 100644 index 00000000000..330714a546f --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-output-www.spec.ts @@ -0,0 +1,380 @@ +import type * as d from '@stencil/core/declarations'; +import { mockLoadConfigInit } from '@stencil/core/testing'; +import { isOutputTargetCopy, isOutputTargetHydrate, isOutputTargetWww, join } from '@utils'; +import path from 'path'; + +import { ConfigFlags, createConfigFlags } from '../../../cli/config-flags'; +import { validateConfig } from '../validate-config'; + +describe('validateOutputTargetWww', () => { + // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) + const rootDir = path.resolve('/'); + let userConfig: d.Config; + let flags: ConfigFlags; + + beforeEach(() => { + flags = createConfigFlags(); + userConfig = { + rootDir: rootDir, + flags, + }; + }); + + it('should have default value', () => { + const outputTarget: d.OutputTargetWww = { + type: 'www', + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + dir: path.join('www', 'docs'), + }; + userConfig.outputTargets = [outputTarget]; + userConfig.buildEs5 = false; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.outputTargets).toEqual([ + { + appDir: join(rootDir, 'www', 'docs'), + baseUrl: '/', + buildDir: join(rootDir, 'www', 'docs', 'build'), + dir: join(rootDir, 'www', 'docs'), + empty: true, + indexHtml: join(rootDir, 'www', 'docs', 'index.html'), + polyfills: true, + serviceWorker: { + dontCacheBustURLsMatching: /p-\w{8}/, + globDirectory: join(rootDir, 'www', 'docs'), + globIgnores: [ + '**/host.config.json', + '**/*.system.entry.js', + '**/*.system.js', + '**/app.js', + '**/app.esm.js', + '**/app.css', + ], + globPatterns: ['*.html', '**/*.{js,css,json}'], + swDest: join(rootDir, 'www', 'docs', 'sw.js'), + }, + type: 'www', + }, + { + dir: join(rootDir, 'www', 'docs', 'build'), + esmDir: join(rootDir, 'www', 'docs', 'build'), + isBrowserBuild: true, + polyfills: true, + systemDir: undefined, + systemLoaderFile: undefined, + type: 'dist-lazy', + }, + { + copyAssets: 'dist', + dir: join(rootDir, 'www', 'docs', 'build'), + type: 'copy', + }, + { + copy: [ + { + src: 'assets', + warn: false, + }, + { + src: 'manifest.json', + warn: false, + }, + ], + dir: join(rootDir, 'www', 'docs'), + type: 'copy', + }, + { + file: join(rootDir, 'www', 'docs', 'build', 'app.css'), + type: 'dist-global-styles', + }, + ]); + }); + + it('should www with sub directory', () => { + const outputTarget: d.OutputTargetWww = { + type: 'www', + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + dir: path.join('www', 'docs'), + }; + userConfig.outputTargets = [outputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; + + expect(www.dir).toBe(join(rootDir, 'www', 'docs')); + expect(www.appDir).toBe(join(rootDir, 'www', 'docs')); + expect(www.buildDir).toBe(join(rootDir, 'www', 'docs', 'build')); + expect(www.indexHtml).toBe(join(rootDir, 'www', 'docs', 'index.html')); + }); + + it('should set www values', () => { + const outputTarget: d.OutputTargetWww = { + type: 'www', + dir: 'my-www', + buildDir: 'my-build', + indexHtml: 'my-index.htm', + empty: false, + }; + userConfig.outputTargets = [outputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; + + expect(www.type).toBe('www'); + expect(www.dir).toBe(join(rootDir, 'my-www')); + expect(www.buildDir).toBe(join(rootDir, 'my-www', 'my-build')); + expect(www.indexHtml).toBe(join(rootDir, 'my-www', 'my-index.htm')); + expect(www.empty).toBe(false); + }); + + it('should default to add www when outputTargets is undefined', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets).toHaveLength(5); + + const outputTarget = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; + expect(outputTarget.dir).toBe(join(rootDir, 'www')); + expect(outputTarget.buildDir).toBe(join(rootDir, 'www', 'build')); + expect(outputTarget.indexHtml).toBe(join(rootDir, 'www', 'index.html')); + expect(outputTarget.empty).toBe(true); + }); + + describe('baseUrl', () => { + it('baseUrl does not end with / with dir set', () => { + const outputTarget: d.OutputTargetWww = { + type: 'www', + dir: 'my-www', + baseUrl: '/docs', + }; + userConfig.outputTargets = [outputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; + + expect(www.type).toBe('www'); + expect(www.dir).toBe(join(rootDir, 'my-www')); + expect(www.baseUrl).toBe('/docs/'); + expect(www.appDir).toBe(join(rootDir, 'my-www/docs')); + + expect(www.buildDir).toBe(join(rootDir, 'my-www', 'docs', 'build')); + expect(www.indexHtml).toBe(join(rootDir, 'my-www', 'docs', 'index.html')); + }); + + it('baseUrl does not end with /', () => { + const outputTarget: d.OutputTargetWww = { + type: 'www', + baseUrl: '/docs', + }; + userConfig.outputTargets = [outputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; + + expect(www.type).toBe('www'); + expect(www.dir).toBe(join(rootDir, 'www')); + expect(www.baseUrl).toBe('/docs/'); + expect(www.appDir).toBe(join(rootDir, 'www/docs')); + + expect(www.buildDir).toBe(join(rootDir, 'www', 'docs', 'build')); + expect(www.indexHtml).toBe(join(rootDir, 'www', 'docs', 'index.html')); + }); + + it('baseUrl is a full url', () => { + const outputTarget: d.OutputTargetWww = { + type: 'www', + baseUrl: 'https://example.com/docs', + }; + userConfig.outputTargets = [outputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const www = config.outputTargets.find(isOutputTargetWww) as d.OutputTargetWww; + + expect(www.type).toBe('www'); + expect(www.dir).toBe(join(rootDir, 'www')); + expect(www.baseUrl).toBe('https://example.com/docs/'); + expect(www.appDir).toBe(join(rootDir, 'www/docs')); + + expect(www.buildDir).toBe(join(rootDir, 'www', 'docs', 'build')); + expect(www.indexHtml).toBe(join(rootDir, 'www', 'docs', 'index.html')); + }); + }); + + describe('copy', () => { + it('should add copy tasks', () => { + const outputTarget: d.OutputTargetWww = { + type: 'www', + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + dir: path.join('www', 'docs'), + copy: [ + { + src: 'index-modules.html', + dest: 'index-2.html', + }, + ], + }; + userConfig.outputTargets = [outputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + const copyTargets = config.outputTargets.filter(isOutputTargetCopy); + expect(copyTargets).toEqual([ + { + copyAssets: 'dist', + dir: join(rootDir, 'www', 'docs', 'build'), + type: 'copy', + }, + { + copy: [ + { + dest: 'index-2.html', + src: 'index-modules.html', + }, + { + src: 'assets', + warn: false, + }, + { + src: 'manifest.json', + warn: false, + }, + ], + dir: join(rootDir, 'www', 'docs'), + type: 'copy', + }, + ]); + }); + + it('should replace copy tasks', () => { + const outputTarget: d.OutputTargetWww = { + type: 'www', + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + dir: path.join('www', 'docs'), + copy: [ + { + src: 'assets', + dest: 'assets2', + }, + ], + }; + userConfig.outputTargets = [outputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + const copyTargets = config.outputTargets.filter(isOutputTargetCopy); + expect(copyTargets).toEqual([ + { + copyAssets: 'dist', + dir: join(rootDir, 'www', 'docs', 'build'), + type: 'copy', + }, + { + copy: [ + { + dest: 'assets2', + src: 'assets', + }, + { + src: 'manifest.json', + warn: false, + }, + ], + dir: join(rootDir, 'www', 'docs'), + type: 'copy', + }, + ]); + }); + + it('should disable copy tasks', () => { + const outputTarget: d.OutputTargetWww = { + type: 'www', + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + dir: path.join('www', 'docs'), + copy: null, + }; + userConfig.outputTargets = [outputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + const copyTargets = config.outputTargets.filter(isOutputTargetCopy); + expect(copyTargets).toEqual([ + { + copyAssets: 'dist', + dir: join(rootDir, 'www', 'docs', 'build'), + type: 'copy', + }, + { + copy: [], + dir: join(rootDir, 'www', 'docs'), + type: 'copy', + }, + ]); + }); + }); + + describe('dist-hydrate-script', () => { + it('should not add hydrate by default', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(false); + expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); + }); + + it('should not add hydrate with user www', () => { + const wwwOutputTarget: d.OutputTargetWww = { + type: 'www', + }; + userConfig.outputTargets = [wwwOutputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(false); + expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); + }); + + it('should add hydrate with user hydrate and www outputs', () => { + const wwwOutputTarget: d.OutputTargetWww = { + type: 'www', + }; + const hydrateOutputTarget: d.OutputTargetHydrate = { + type: 'dist-hydrate-script', + }; + userConfig.outputTargets = [wwwOutputTarget, hydrateOutputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(true); + expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); + }); + + it('should add hydrate with --prerender flag', () => { + userConfig.flags = { ...flags, prerender: true }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(true); + expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); + }); + + it('should add hydrate with --ssr flag', () => { + userConfig.flags = { ...flags, ssr: true }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets.some((o) => o.type === 'dist-hydrate-script')).toBe(true); + expect(config.outputTargets.some((o) => o.type === 'www')).toBe(true); + }); + + it('should add externals and defaults', () => { + const hydrateOutputTarget: d.OutputTargetHydrate = { + type: 'dist-hydrate-script', + external: ['lodash', 'left-pad'], + }; + userConfig.outputTargets = [hydrateOutputTarget]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const o = config.outputTargets.find(isOutputTargetHydrate) as d.OutputTargetHydrate; + expect(o.external).toContain('lodash'); + expect(o.external).toContain('left-pad'); + expect(o.external).toContain('fs'); + expect(o.external).toContain('path'); + expect(o.external).toContain('crypto'); + }); + + it('should add node builtins to external by default', () => { + userConfig.flags = { ...flags, prerender: true }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const o = config.outputTargets.find(isOutputTargetHydrate) as d.OutputTargetHydrate; + expect(o.external).toContain('fs'); + expect(o.external).toContain('path'); + expect(o.external).toContain('crypto'); + }); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-paths.spec.ts b/packages/core/src/compiler/config/test/validate-paths.spec.ts new file mode 100644 index 00000000000..666071c7143 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-paths.spec.ts @@ -0,0 +1,176 @@ +import type * as d from '@stencil/core/declarations'; +import { mockCompilerSystem, mockLoadConfigInit, mockLogger } from '@stencil/core/testing'; +import { join } from '@utils'; +import path from 'path'; + +import { validateConfig } from '../validate-config'; + +describe('validatePaths', () => { + let userConfig: d.Config; + const logger = mockLogger(); + const sys = mockCompilerSystem(); + + // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) + const ROOT = path.resolve('/'); + + beforeEach(() => { + userConfig = { + sys: sys as any, + logger: logger, + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + rootDir: path.join(ROOT, 'User', 'my-app'), + namespace: 'Testing', + }; + }); + + it('should set absolute cacheDir', () => { + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + userConfig.cacheDir = path.join(ROOT, 'some', 'custom', 'cache'); + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.cacheDir).toBe(join(ROOT, 'some', 'custom', 'cache')); + }); + + it('should set relative cacheDir', () => { + userConfig.cacheDir = 'custom-cache'; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.cacheDir).toBe(join(ROOT, 'User', 'my-app', 'custom-cache')); + }); + + it('should set default cacheDir', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.cacheDir).toBe(join(ROOT, 'User', 'my-app', '.stencil')); + }); + + it('should set default wwwIndexHtml and convert to absolute path', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(path.basename((config.outputTargets as d.OutputTargetWww[])[0].indexHtml!)).toBe('index.html'); + expect(path.isAbsolute((config.outputTargets as d.OutputTargetWww[])[0].indexHtml!)).toBe(true); + }); + + it('should convert a custom wwwIndexHtml to absolute path', () => { + userConfig.outputTargets = [ + { + type: 'www', + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + indexHtml: path.join('assets', 'custom-index.html'), + }, + ] as d.OutputTargetWww[]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(path.basename((config.outputTargets as d.OutputTargetWww[])[0].indexHtml!)).toBe('custom-index.html'); + expect(path.isAbsolute((config.outputTargets as d.OutputTargetWww[])[0].indexHtml!)).toBe(true); + }); + + it('should set default indexHtmlSrc and convert to absolute path', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(path.basename(config.srcIndexHtml)).toBe('index.html'); + expect(path.isAbsolute(config.srcIndexHtml)).toBe(true); + }); + + it('should set emptyDist to false', () => { + userConfig.outputTargets = [ + { + type: 'www', + empty: false, + }, + ] as d.OutputTargetWww[]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect((config.outputTargets as d.OutputTargetWww[])[0].empty).toBe(false); + }); + + it('should set default emptyWWW to true', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect((config.outputTargets as d.OutputTargetWww[])[0].empty).toBe(true); + }); + + it('should set emptyWWW to false', () => { + userConfig.outputTargets = [ + { + type: 'www', + empty: false, + }, + ] as d.OutputTargetWww[]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect((config.outputTargets as d.OutputTargetWww[])[0].empty).toBe(false); + }); + + it('should set default collection dir and convert to absolute path', () => { + userConfig.outputTargets = [ + { + type: 'dist', + }, + ]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(path.basename((config.outputTargets as d.OutputTargetDist[])[0].collectionDir!)).toBe('collection'); + expect(path.isAbsolute((config.outputTargets as d.OutputTargetDist[])[0].collectionDir!)).toBe(true); + }); + + it('should set default types dir and convert to absolute path', () => { + userConfig.outputTargets = [ + { + type: 'dist', + }, + ]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(path.basename((config.outputTargets as d.OutputTargetDist[])[0].typesDir!)).toBe('types'); + expect(path.isAbsolute((config.outputTargets as d.OutputTargetDist[])[0].typesDir!)).toBe(true); + }); + + it('should set default build dir and convert to absolute path', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + // the path will be normalized by Stencil us use '/', split on that regardless of platform + const parts = (config.outputTargets as d.OutputTargetDist[])[0].buildDir!.split('/'); + expect(parts[parts.length - 1]).toBe('build'); + expect(parts[parts.length - 2]).toBe('www'); + expect(path.isAbsolute((config.outputTargets as d.OutputTargetDist[])[0].buildDir!)).toBe(true); + }); + + it('should set build dir w/ custom www', () => { + userConfig.outputTargets = [ + { + type: 'www', + dir: 'custom-www', + }, + ] as d.OutputTargetWww[]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + // the path will be normalized by Stencil us use '/', split on that regardless of platform + const parts = (config.outputTargets as d.OutputTargetDist[])[0].buildDir!.split('/'); + expect(parts[parts.length - 1]).toBe('build'); + expect(parts[parts.length - 2]).toBe('custom-www'); + expect(path.isAbsolute((config.outputTargets as d.OutputTargetDist[])[0].buildDir!)).toBe(true); + }); + + it('should set default src dir and convert to absolute path', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(path.basename(config.srcDir)).toBe('src'); + expect(path.isAbsolute(config.srcDir)).toBe(true); + }); + + it('should set src dir and convert to absolute path', () => { + userConfig.srcDir = 'app'; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(path.basename(config.srcDir)).toBe('app'); + expect(path.isAbsolute(config.srcDir)).toBe(true); + }); + + it('should convert globalScript to absolute path, if a globalScript property was provided', () => { + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + userConfig.globalScript = path.join('src', 'global', 'index.ts'); + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(path.basename(config.globalScript!)).toBe('index.ts'); + expect(path.isAbsolute(config.globalScript!)).toBe(true); + }); + + it('should convert globalStyle string to absolute path array, if a globalStyle property was provided', () => { + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + userConfig.globalStyle = path.join('src', 'global', 'styles.css'); + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(path.basename(config.globalStyle!)).toBe('styles.css'); + expect(path.isAbsolute(config.globalStyle!)).toBe(true); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-rollup-config.spec.ts b/packages/core/src/compiler/config/test/validate-rollup-config.spec.ts new file mode 100644 index 00000000000..61c298282b7 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-rollup-config.spec.ts @@ -0,0 +1,82 @@ +import type * as d from '@stencil/core/declarations'; + +import { validateRollupConfig } from '../validate-rollup-config'; + +describe('validateStats', () => { + let config: d.Config; + + beforeEach(() => { + config = {}; + }); + + it('should use default if no config provided', () => { + const rollupConfig = validateRollupConfig(config); + expect(rollupConfig).toEqual({ + inputOptions: {}, + outputOptions: {}, + }); + }); + + it('should set based on inputOptions if provided', () => { + config.rollupConfig = { + inputOptions: { + context: 'window', + }, + }; + const rollupConfig = validateRollupConfig(config); + expect(rollupConfig).toEqual({ + inputOptions: { + context: 'window', + }, + outputOptions: {}, + }); + }); + + it('should use default if inputOptions is not provided but outputOptions is', () => { + config.rollupConfig = { + outputOptions: { + globals: { + jquery: '$', + }, + }, + }; + + const rollupConfig = validateRollupConfig(config); + expect(rollupConfig).toEqual({ + inputOptions: {}, + outputOptions: { + globals: { + jquery: '$', + }, + }, + }); + }); + + it('should pass all valid config data through and not those that are extraneous', () => { + config.rollupConfig = { + inputOptions: { + context: 'window', + external: 'external_symbol', + notAnOption: {}, + }, + outputOptions: { + globals: { + jquery: '$', + }, + }, + } as d.RollupConfig; + + const rollupConfig = validateRollupConfig(config); + expect(rollupConfig).toEqual({ + inputOptions: { + context: 'window', + external: 'external_symbol', + }, + outputOptions: { + globals: { + jquery: '$', + }, + }, + }); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-service-worker.spec.ts b/packages/core/src/compiler/config/test/validate-service-worker.spec.ts new file mode 100644 index 00000000000..66de1a68bcb --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-service-worker.spec.ts @@ -0,0 +1,193 @@ +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); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-stats.spec.ts b/packages/core/src/compiler/config/test/validate-stats.spec.ts new file mode 100644 index 00000000000..2c2eaa90a56 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-stats.spec.ts @@ -0,0 +1,100 @@ +import type * as d from '@stencil/core/declarations'; +import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; + +import { validateConfig } from '../validate-config'; + +describe('validateStats', () => { + let userConfig: d.Config; + + beforeEach(() => { + userConfig = mockConfig(); + }); + + it('adds stats from flags, w/ no outputTargets', () => { + // the flags field is expected to have been set by the mock creation function for unvalidated configs, hence the + // bang operator + userConfig.flags!.stats = true; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; + expect(o).toBeDefined(); + expect(o.file).toContain('stencil-stats.json'); + }); + + it('uses stats config, custom path', () => { + userConfig.outputTargets = [ + { + type: 'stats', + file: 'custom-path.json', + } as d.OutputTargetStats, + ]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; + expect(o).toBeDefined(); + expect(o.file).toContain('custom-path.json'); + }); + + it('uses stats config, defaults file', () => { + userConfig.outputTargets = [ + { + type: 'stats', + }, + ]; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; + expect(o).toBeDefined(); + expect(o.file).toContain('stencil-stats.json'); + }); + + it('default no stats', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.outputTargets.some((o) => o.type === 'stats')).toBe(false); + }); + + it('adds stats from flags with custom path string', () => { + // Test --stats dist/stats.json behavior + userConfig.flags!.stats = 'dist/custom-stats.json'; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; + expect(o).toBeDefined(); + expect(o.file).toContain('dist/custom-stats.json'); + }); + + it('adds stats from flags with custom path (absolute)', () => { + // Test --stats /tmp/stats.json behavior + userConfig.flags!.stats = '/tmp/absolute-stats.json'; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; + expect(o).toBeDefined(); + expect(o.file).toBe('/tmp/absolute-stats.json'); + }); + + it('flags stats path takes precedence over default when no outputTarget', () => { + // When --stats has a path, it should be used instead of the default + userConfig.flags!.stats = 'custom-location/stats.json'; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const o = config.outputTargets.find((o) => o.type === 'stats') as d.OutputTargetStats; + expect(o).toBeDefined(); + expect(o.file).toContain('custom-location/stats.json'); + expect(o.file).not.toContain('stencil-stats.json'); + }); + + it('does not override existing stats outputTarget when flag has path', () => { + // When there's already a stats outputTarget in config, flag should not add another + userConfig.outputTargets = [ + { + type: 'stats', + file: 'config-defined.json', + } as d.OutputTargetStats, + ]; + userConfig.flags!.stats = 'flag-defined.json'; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const statsTargets = config.outputTargets.filter((o) => o.type === 'stats'); + expect(statsTargets.length).toBe(1); + expect(statsTargets[0].file).toContain('config-defined.json'); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-testing.spec.ts b/packages/core/src/compiler/config/test/validate-testing.spec.ts new file mode 100644 index 00000000000..565ec1f0ea2 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-testing.spec.ts @@ -0,0 +1,941 @@ +import type * as d from '@stencil/core/declarations'; +import { mockCompilerSystem, mockLoadConfigInit, mockLogger } from '@stencil/core/testing'; +import { join } from '@utils'; +import path from 'path'; + +import { ConfigFlags, createConfigFlags } from '../../../cli/config-flags'; +import { validateConfig } from '../validate-config'; + +describe('validateTesting', () => { + // use Node's resolve() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) + const ROOT = path.resolve('/'); + const sys = mockCompilerSystem(); + const logger = mockLogger(); + let userConfig: d.Config; + let flags: ConfigFlags; + + beforeEach(() => { + flags = createConfigFlags(); + userConfig = { + sys: sys as any, + logger: logger, + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + rootDir: path.join(ROOT, 'User', 'some', 'path'), + srcDir: path.join(ROOT, 'User', 'some', 'path', 'src'), + flags, + namespace: 'Testing', + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + configPath: path.join(ROOT, 'User', 'some', 'path', 'stencil.config.ts'), + }; + userConfig.outputTargets = [ + { + type: 'www', + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + dir: path.join(ROOT, 'www'), + } as any as d.OutputTargetStats, + ]; + }); + + describe('no testing flags', () => { + it('returns an empty testing config when no testing config nor testing flags are provided', () => { + userConfig.flags = { ...flags, e2e: false, spec: false }; + delete userConfig.testing; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing).toEqual({}); + }); + + it('returns the provided testing config when neither testing flag is provided', () => { + const testingConfig: d.TestingConfig = { + bail: false, + }; + userConfig.flags = { ...flags, e2e: false, spec: false }; + userConfig.testing = testingConfig; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing).toEqual(testingConfig); + }); + }); + + describe('browserHeadless', () => { + const originalCI = process.env.CI; + beforeEach(() => { + delete process.env.CI; + }); + + afterEach(() => { + process.env.CI = originalCI; + }); + + describe("using 'headless' value from cli", () => { + it.each([false, 'shell'])('sets browserHeadless to %s', (headless) => { + userConfig.flags = { ...flags, e2e: true, headless }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe(headless); + }); + + it('throws if browser headless is set to deprecated value `true`', () => { + userConfig.flags = { ...flags, e2e: true, headless: true }; + expect(() => validateConfig(userConfig, mockLoadConfigInit())).toThrow( + 'Setting "browserHeadless" config to `true` is not supported anymore, please set it to "shell"!', + ); + }); + + it('defaults to "shell" outside of CI', () => { + userConfig.flags = { ...flags, e2e: true }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe('shell'); + }); + }); + + describe('with ci enabled', () => { + it("forces using the shell headless mode when 'headless: false'", () => { + userConfig.flags = { ...flags, ci: true, e2e: true, headless: false }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe('shell'); + }); + + it('allows the shell headless mode to be used', () => { + userConfig.flags = { ...flags, ci: true, e2e: true, headless: 'shell' }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe('shell'); + }); + }); + + describe('`testing` configuration', () => { + beforeEach(() => { + userConfig.flags = { ...flags, e2e: true, headless: undefined }; + }); + + it.each([false, 'shell'])( + 'uses %s browserHeadless mode from testing config', + (browserHeadlessValue) => { + userConfig.testing = { browserHeadless: browserHeadlessValue }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe(browserHeadlessValue); + }, + ); + + it('throws if browser headless is set to deprecated value `true`', () => { + userConfig.testing = { browserHeadless: true }; + expect(() => validateConfig(userConfig, mockLoadConfigInit())).toThrow( + 'Setting "browserHeadless" config to `true` is not supported anymore, please set it to "shell"!', + ); + }); + + it('defaults the headless mode to "shell" when browserHeadless is not provided', () => { + userConfig.testing = {}; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe('shell'); + }); + }); + }); + + describe('devTools', () => { + const originalCI = process.env.CI; + beforeEach(() => { + delete process.env.CI; + }); + + afterEach(() => { + process.env.CI = originalCI; + }); + + it('ignores devTools settings if CI is enabled', () => { + userConfig.flags = { ...flags, ci: true, devtools: true, e2e: true }; + userConfig.testing = {}; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.browserDevtools).toBeUndefined(); + }); + + it('sets browserDevTools to true when the devtools flag is set', () => { + userConfig.flags = { ...flags, devtools: true, e2e: true }; + userConfig.testing = {}; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.browserDevtools).toBe(true); + // browserHeadless must be false to enabled dev tools (which are headful by definition) + expect(config.testing.browserHeadless).toBe(false); + }); + + it("sets browserDevTools to true when set in a project's config", () => { + userConfig.flags = { ...flags, devtools: false, e2e: true }; + userConfig.testing = { browserDevtools: true }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.browserDevtools).toBe(true); + // browserHeadless must be false to enabled dev tools (which are headful by definition) + expect(config.testing.browserHeadless).toBe(false); + }); + }); + + describe('browserWaitUntil', () => { + it('sets the default to "load" if no value is provided', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = {}; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.browserWaitUntil).toBe('load'); + }); + + it('does not override a provided value', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + browserWaitUntil: 'domcontentloaded', + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.browserWaitUntil).toBe('domcontentloaded'); + }); + }); + + describe('browserArgs', () => { + const originalCI = process.env.CI; + beforeEach(() => { + delete process.env.CI; + }); + + afterEach(() => { + process.env.CI = originalCI; + }); + + it('does not add duplicate default fields', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + browserArgs: ['--unique', '--font-render-hinting=medium'], + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.browserArgs).toEqual(['--unique', '--font-render-hinting=medium', '--incognito']); + }); + + describe('adds default browser args', () => { + const originalCI = process.env.CI; + + beforeAll(() => { + delete process.env.CI; + }); + + afterAll(() => { + process.env.CI = originalCI; + }); + + it('adds default browser args when not in CI', () => { + userConfig.flags = { ...flags, e2e: true }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.browserArgs).toEqual(['--font-render-hinting=medium', '--incognito']); + }); + }); + + it("adds additional browser args when the 'ci' flag is set", () => { + userConfig.flags = { ...flags, ci: true, e2e: true }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserArgs).toEqual([ + '--font-render-hinting=medium', + '--incognito', + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + ]); + }); + + describe('adds additional browser args when process.env.CI is set', () => { + const originalCI = process.env.CI; + beforeAll(() => { + process.env.CI = 'true'; + }); + + afterAll(() => { + process.env.CI = originalCI; + }); + + it('adds default browser args when CI is set', () => { + userConfig.flags = { ...flags, ci: true, e2e: true }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserArgs).toEqual([ + '--font-render-hinting=medium', + '--incognito', + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + ]); + }); + }); + }); + + describe('browserArgs in CI', () => { + const originalCI = process.env.CI; + beforeEach(() => { + process.env.CI = 'true'; + }); + + afterEach(() => { + process.env.CI = originalCI; + }); + + it("adds additional browser args when 'CI' environment variable is set", () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + browserArgs: ['--unique', '--font-render-hinting=medium'], + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.browserArgs).toEqual([ + '--unique', + '--font-render-hinting=medium', + '--incognito', + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + ]); + }); + }); + + describe('screenshotConnector', () => { + it('assigns the screenshotConnector value from the provided flags', () => { + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + userConfig.flags = { ...flags, e2e: true, screenshotConnector: path.join(ROOT, 'mock', 'path') }; + userConfig.testing = { screenshotConnector: path.join(ROOT, 'another', 'mock', 'path') }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.screenshotConnector).toBe(join(ROOT, 'mock', 'path')); + }); + + it("uses the config's root dir to make the screenshotConnector path absolute", () => { + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + userConfig.flags = { ...flags, e2e: true, screenshotConnector: path.join('mock', 'path') }; + userConfig.testing = { screenshotConnector: path.join('another', 'mock', 'path') }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.screenshotConnector).toBe(join(ROOT, 'User', 'some', 'path', 'mock', 'path')); + }); + + it('sets screenshotConnector if a non-string is provided', () => { + userConfig.flags = { ...flags, e2e: true }; + // the nature of this test is to evaluate a non-string, hence the type assertion + userConfig.testing = { screenshotConnector: true as unknown as string }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.screenshotConnector).toBe(join('screenshot', 'local-connector.js')); + }); + }); + + describe('screenshotTimeout', () => { + it('sets screenshotTimeout to null if not provided', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = {}; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.screenshotTimeout).toEqual(null); + }); + + it('sets screenshotTimeout to null if it has an unexpected value', () => { + userConfig.flags = { ...flags, e2e: true }; + // @ts-expect-error - the nature of this test requires a non-string value + userConfig.testing = { screenshotTimeout: '4s' }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.screenshotTimeout).toEqual(null); + }); + + it('keeps the value if set correctly', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { screenshotTimeout: 4000 }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.screenshotTimeout).toEqual(4000); + }); + }); + + describe('testPathIgnorePatterns', () => { + it('does not alter a provided testPathIgnorePatterns', () => { + userConfig.flags = { ...flags, e2e: true }; + + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + const mockPath1 = path.join('this', 'is', 'a', 'mock', 'path'); + const mockPath2 = path.join('this', 'is', 'another', 'mock', 'path'); + userConfig.testing = { testPathIgnorePatterns: [mockPath1, mockPath2] }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.testPathIgnorePatterns).toEqual([mockPath1, mockPath2]); + }); + + it('sets the default testPathIgnorePatterns if no array is provided', () => { + userConfig.flags = { ...flags, e2e: true }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.testPathIgnorePatterns).toEqual([ + join(ROOT, 'User', 'some', 'path', '.vscode'), + join(ROOT, 'User', 'some', 'path', '.stencil'), + join(ROOT, 'User', 'some', 'path', 'node_modules'), + // use Node's join() here as the normalization process doesn't necessarily occur for this field + path.join(ROOT, 'www'), + ]); + }); + + it('sets the default testPathIgnorePatterns with custom outputTargets', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.outputTargets = [ + { type: 'dist', dir: 'dist-folder' }, + { type: 'www', dir: 'www-folder' }, + { type: 'docs-readme', dir: 'docs' }, + ]; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.testPathIgnorePatterns).toEqual([ + join(ROOT, 'User', 'some', 'path', '.vscode'), + join(ROOT, 'User', 'some', 'path', '.stencil'), + join(ROOT, 'User', 'some', 'path', 'node_modules'), + join(ROOT, 'User', 'some', 'path', 'www-folder'), + join(ROOT, 'User', 'some', 'path', 'dist-folder'), + ]); + }); + }); + + describe('preset', () => { + it.each([null, true])("uses stencil's default preset if a non-string (%s) is provided", (nonStringPreset) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + // the nature of this test requires a non-string value, hence the type assertion + preset: nonStringPreset as unknown as string, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + // 'testing' is the internal directory where `jest-preset.js` can be found + expect(config.testing.preset).toEqual('testing'); + }); + + it('forces a provided preset path to be absolute', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + preset: path.join('mock', 'path'), + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.preset).toEqual(join(ROOT, 'User', 'some', 'path', 'mock', 'path')); + }); + + it('does not change an already absolute preset path', () => { + userConfig.flags = { ...flags, e2e: true }; + + // use Node's join() here to simulate a user using either Win/Posix separators (depending on the platform these + // tests are run on) for their input + const presetPath = path.join(ROOT, 'mock', 'path'); + userConfig.testing = { + preset: presetPath, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + // per the test name, we should not change an already absolute path - assert against the preset path that was + // generated using Node's join() + expect(config.testing.preset).toEqual(presetPath); + }); + }); + + describe('setupFilesAfterEnv', () => { + it.each([null, true])('forces a non-array (%s) of setup files to a default', (nonSetupFiles) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + // the nature of this test requires a non-string value, hence the type assertion + setupFilesAfterEnv: nonSetupFiles as unknown as string[], + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + // 'testing' is the internal directory where the default setup file can be found + expect(config.testing.setupFilesAfterEnv).toEqual([join('testing', 'jest-setuptestframework.js')]); + }); + + it.each([[[]], [['mock-setup-file.js']]])( + "prepends stencil's default file to an array: %s", + (setupFilesAfterEnv) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + setupFilesAfterEnv: [...setupFilesAfterEnv], + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.setupFilesAfterEnv).toEqual([ + // 'testing' is the internal directory where the default setup file can be found + join('testing', 'jest-setuptestframework.js'), + ...setupFilesAfterEnv, + ]); + }, + ); + }); + + describe('testEnvironment', () => { + it('sets a relative testEnvironment to absolute', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + testEnvironment: './rel-path.js', + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(path.isAbsolute(config.testing.testEnvironment)).toBe(true); + expect(path.basename(config.testing.testEnvironment)).toEqual('rel-path.js'); + }); + + it('allows a node module testEnvironment', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + testEnvironment: 'jsdom', + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.testEnvironment).toEqual('jsdom'); + }); + + it('does nothing for an empty testEnvironment', () => { + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.testEnvironment).toBeUndefined(); + }); + }); + + describe('allowableMismatchedPixels', () => { + it.each([0, 123])('does nothing is a non-negative number (%s) is provided', (pixelCount) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + allowableMismatchedPixels: pixelCount, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.allowableMismatchedPixels).toBe(pixelCount); + }); + + it('creates an error if a negative number is provided', () => { + const pixelCount = -1; + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + allowableMismatchedPixels: pixelCount, + }; + + const { config, diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.allowableMismatchedPixels).toBe(pixelCount); + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]).toEqual({ + absFilePath: undefined, + header: 'Build Error', + level: 'error', + lines: [], + messageText: 'allowableMismatchedPixels must be a value that is 0 or greater', + relFilePath: undefined, + type: 'build', + }); + }); + + it.each([true, null])('defaults to a reasonable value if a non-number (%s) is provided', (pixelCount) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + // the nature of this test requires using a non-number, hence th type assertion + allowableMismatchedPixels: pixelCount as unknown as number, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.allowableMismatchedPixels).toBe(100); + }); + }); + + describe('allowableMismatchedRatio', () => { + it.each([-0, 0, 0.5, 1.0])( + 'does nothing if a value between 0 and 1 is provided (%s)', + (allowableMismatchedRatio) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + allowableMismatchedRatio, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.allowableMismatchedRatio).toBe(allowableMismatchedRatio); + }, + ); + + it.each([-1, -0.1, 1.1, 2])( + 'creates an error if a number outside 0 and 1 is provided (%s)', + (allowableMismatchedRatio) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + allowableMismatchedRatio, + }; + + const { config, diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.allowableMismatchedRatio).toBe(allowableMismatchedRatio); + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]).toEqual({ + absFilePath: undefined, + header: 'Build Error', + level: 'error', + lines: [], + messageText: 'allowableMismatchedRatio must be a value ranging from 0 to 1', + relFilePath: undefined, + type: 'build', + }); + }, + ); + + it.each([true, null])('does nothing when a non-number (%s) is provided', (allowableMismatchedRatio) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + // the nature of this test requires using a non-number, hence th type assertion + allowableMismatchedRatio: allowableMismatchedRatio as unknown as number, + }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.allowableMismatchedRatio).toBe(allowableMismatchedRatio); + }); + }); + + describe('pixelmatchThreshold', () => { + it.each([-0, 0, 0.5, 1.0])('does nothing if a value between 0 and 1 is provided (%s)', (pixelmatchThreshold) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + pixelmatchThreshold, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.pixelmatchThreshold).toBe(pixelmatchThreshold); + }); + + it.each([-0.1, -1, 1.1, 2])( + 'creates an error if a number outside 0 and 1 is provided (%s)', + (pixelmatchThreshold) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + pixelmatchThreshold, + }; + + const { config, diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.pixelmatchThreshold).toBe(pixelmatchThreshold); + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]).toEqual({ + absFilePath: undefined, + header: 'Build Error', + level: 'error', + lines: [], + messageText: 'pixelmatchThreshold must be a value ranging from 0 to 1', + relFilePath: undefined, + type: 'build', + }); + }, + ); + + it.each([true, null])('defaults to a reasonable value if a non-number (%s) is provided', (pixelmatchThreshold) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + // the nature of this test requires using a non-number, hence th type assertion + pixelmatchThreshold: pixelmatchThreshold as unknown as number, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.allowableMismatchedPixels).toBe(100); + }); + }); + + describe('testRegex', () => { + let testRegex: RegExp; + + beforeEach(() => { + userConfig.flags = { ...flags, spec: true }; + + const { testing: testConfig } = validateConfig(userConfig, mockLoadConfigInit()).config; + const testRegexSetting = testConfig?.testRegex; + + if (!testRegexSetting) { + throw new Error('No testRegex was found in the Stencil TestingConfig. Failing test.'); + } + + testRegex = new RegExp(testRegexSetting[0]); + }); + + describe('test.* extensions', () => { + it.each([ + 'my-component.test.ts', + 'my-component.test.tsx', + 'my-component.test.js', + 'my-component.test.jsx', + 'some/path/test.ts', + 'some/path/test.tsx', + 'some/path/test.js', + 'some/path/test.jsx', + ])(`matches the file '%s'`, (filename) => { + expect(testRegex.test(filename)).toBe(true); + }); + + it.each([ + 'my-component.test.ts.snap', + 'my-component.test.tsx.snap', + 'my-component.test.js.snap', + 'my-component.test.jsx.snap', + 'my-component-test.ts', + 'my-component-test.tsx', + 'my-component-test.js', + 'my-component-test.jsx', + 'my-component.test.t', + 'my-component.test.j', + ])(`doesn't match the file '%s'`, (filename) => { + expect(testRegex.test(filename)).toBe(false); + }); + }); + + describe('spec.* extensions', () => { + it.each([ + 'my-component.spec.ts', + 'my-component.spec.tsx', + 'my-component.spec.js', + 'my-component.spec.jsx', + 'some/path/spec.ts', + 'some/path/spec.tsx', + 'some/path/spec.js', + 'some/path/spec.jsx', + ])(`matches the file '%s'`, (filename) => { + expect(testRegex.test(filename)).toBe(true); + }); + + it.each([ + 'my-component.spec.ts.snap', + 'my-component.spec.tsx.snap', + 'my-component.spec.js.snap', + 'my-component.spec.jsx.snap', + 'my-component-spec.ts', + 'my-component-spec.tsx', + 'my-component-spec.js', + 'my-component-spec.jsx', + 'my-component.spec.t', + 'my-component.spec.j', + ])(`doesn't match the file '%s'`, (filename) => { + expect(testRegex.test(filename)).toBe(false); + }); + }); + + describe('e2e.* extensions', () => { + it.each([ + 'my-component.e2e.ts', + 'my-component.e2e.tsx', + 'my-component.e2e.js', + 'my-component.e2e.jsx', + 'some/path/e2e.ts', + 'some/path/e2e.tsx', + 'some/path/e2e.js', + 'some/path/e2e.jsx', + ])(`matches the file '%s'`, (filename) => { + expect(testRegex.test(filename)).toBe(true); + }); + + it.each([ + 'my-component.e2e.ts.snap', + 'my-component.e2e.tsx.snap', + 'my-component.e2e.js.snap', + 'my-component.e2e.jsx.snap', + 'my-component-e2e.ts', + 'my-component-e2e.tsx', + 'my-component-e2e.js', + 'my-component-e2e.jsx', + 'my-component.e2e.t', + 'my-component.e2e.j', + ])(`doesn't match the file '%s'`, (filename) => { + expect(testRegex.test(filename)).toBe(false); + }); + }); + }); + + describe('testMatch', () => { + it('removes testRegex from the config when testMatch is an array', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + testMatch: ['mockMatcher'], + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.testMatch).toEqual(['mockMatcher']); + expect(config.testing.testRegex).toBeUndefined(); + }); + + it('removes testMatch from the config when testRegex is a string', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + testMatch: undefined, + testRegex: ['/regexStr/'], + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.testMatch).toBeUndefined(); + expect(config.testing.testRegex).toEqual(['/regexStr/']); + }); + + it('transforms testRegex to an array if passed in as string', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + testMatch: undefined, + // @ts-expect-error invalid type because of type update + testRegex: '/regexStr/', + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.testMatch).toBeUndefined(); + expect(config.testing.testRegex).toEqual(['/regexStr/']); + }); + }); + + describe('runner', () => { + it('does nothing if the runner property is a string', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + runner: 'my-runner.js', + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.runner).toEqual('my-runner.js'); + }); + + it('sets the runner if a non-string value is provided', () => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + runner: undefined, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + // 'testing' is the internal directory where the default runner file can be found + expect(config.testing.runner).toEqual(join('testing', 'jest-runner.js')); + }); + }); + + describe('waitBeforeScreenshot', () => { + it.each([-0, 0, 0.5, 1.0])('does nothing for a non-negative value (%s)', (waitBeforeScreenshot) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + waitBeforeScreenshot, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.waitBeforeScreenshot).toBe(waitBeforeScreenshot); + }); + + it('creates an error if the value provided is negative', () => { + const waitBeforeScreenshot = -1; + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + waitBeforeScreenshot, + }; + + const { config, diagnostics } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.waitBeforeScreenshot).toBe(waitBeforeScreenshot); + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]).toEqual({ + absFilePath: undefined, + header: 'Build Error', + level: 'error', + lines: [], + messageText: 'waitBeforeScreenshot must be a value that is 0 or greater', + relFilePath: undefined, + type: 'build', + }); + }); + + it.each([true, null])('defaults to a reasonable value if a non-number (%s) is provided', (waitBeforeScreenshot) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + // the nature of this test requires using a non-number, hence the type assertion + pixelmatchThreshold: waitBeforeScreenshot as unknown as number, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.waitBeforeScreenshot).toBe(10); + }); + }); + + describe('emulate', () => { + it.each([[undefined], [[]]])('provides a reasonable default for %s', (emulate) => { + userConfig.flags = { ...flags, e2e: true }; + userConfig.testing = { + emulate, + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.emulate).toEqual([ + { + userAgent: 'default', + viewport: { + width: 600, + height: 600, + deviceScaleFactor: 1, + isMobile: false, + hasTouch: false, + isLandscape: false, + }, + }, + ]); + }); + + it('does nothing when a non-zero length array is provided', () => { + userConfig.flags = { ...flags, e2e: true }; + + const emulateConfig: d.EmulateConfig = { + userAgent: 'mockAgent', + viewport: { + width: 100, + height: 100, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }; + userConfig.testing = { + emulate: [emulateConfig], + }; + + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + + expect(config.testing.emulate).toEqual([emulateConfig]); + }); + }); +}); diff --git a/packages/core/src/compiler/config/test/validate-workers.spec.ts b/packages/core/src/compiler/config/test/validate-workers.spec.ts new file mode 100644 index 00000000000..715a37eb0c1 --- /dev/null +++ b/packages/core/src/compiler/config/test/validate-workers.spec.ts @@ -0,0 +1,52 @@ +import type * as d from '@stencil/core/declarations'; +import { mockLoadConfigInit, mockLogger } from '@stencil/core/testing'; +import path from 'path'; + +import { createConfigFlags } from '../../../cli/config-flags'; +import { validateConfig } from '../validate-config'; + +describe('validate-workers', () => { + let userConfig: d.Config; + const logger = mockLogger(); + + beforeEach(() => { + userConfig = { + sys: { + path: path, + } as any, + logger: logger, + rootDir: '/', + namespace: 'Testing', + }; + }); + + it('set maxConcurrentWorkers, but dont let it go under 0', () => { + userConfig.maxConcurrentWorkers = -1; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.maxConcurrentWorkers).toBe(0); + }); + + it('set maxConcurrentWorkers from ci flags', () => { + userConfig.flags = createConfigFlags({ + ci: true, + }); + userConfig.maxConcurrentWorkers = 2; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.maxConcurrentWorkers).toBe(4); + }); + + it('set maxConcurrentWorkers from flags', () => { + userConfig.flags = createConfigFlags({ + maxWorkers: 1, + }); + userConfig.maxConcurrentWorkers = 4; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.maxConcurrentWorkers).toBe(1); + }); + + it('set maxConcurrentWorkers', () => { + userConfig.maxConcurrentWorkers = 4; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.maxConcurrentWorkers).toBe(4); + }); +}); diff --git a/packages/core/src/compiler/config/transpile-options.ts b/packages/core/src/compiler/config/transpile-options.ts new file mode 100644 index 00000000000..4b92fdd3676 --- /dev/null +++ b/packages/core/src/compiler/config/transpile-options.ts @@ -0,0 +1,204 @@ +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']); diff --git a/packages/core/src/compiler/config/validate-config.ts b/packages/core/src/compiler/config/validate-config.ts new file mode 100644 index 00000000000..fe3daa8f6cf --- /dev/null +++ b/packages/core/src/compiler/config/validate-config.ts @@ -0,0 +1,293 @@ +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 = validatedConfig.extras.addGlobalStyleToComponents !== false; + + // TODO(STENCIL-914): remove when `experimentalSlotFixes` is the default behavior + // If the user set `experimentalSlotFixes` and any individual slot fix flags to `false`, we need to log a warning + // to the user that we will "override" the individual flags + if (validatedConfig.extras.experimentalSlotFixes === true) { + const possibleFlags: (keyof ConfigExtras)[] = [ + 'appendChildSlotFix', + 'slotChildNodesFix', + 'cloneNodeFix', + 'scopedSlotTextContentFix', + 'experimentalScopedSlotChanges', + ]; + const conflictingFlags = possibleFlags.filter((flag) => validatedConfig.extras[flag] === false); + if (conflictingFlags.length > 0) { + const warning = buildError(diagnostics); + warning.level = 'warn'; + warning.messageText = `If the 'experimentalSlotFixes' flag is enabled it will override any slot fix flags which are disabled. In particular, the following currently-disabled flags will be ignored: ${conflictingFlags.join( + ', ', + )}. Please update your Stencil config accordingly.`; + } + } + + // TODO(STENCIL-914): remove `experimentalSlotFixes` when it's the default behavior + validatedConfig.extras.experimentalSlotFixes = !!validatedConfig.extras.experimentalSlotFixes; + if (validatedConfig.extras.experimentalSlotFixes === true) { + validatedConfig.extras.appendChildSlotFix = true; + validatedConfig.extras.cloneNodeFix = true; + validatedConfig.extras.slotChildNodesFix = true; + validatedConfig.extras.scopedSlotTextContentFix = true; + validatedConfig.extras.experimentalScopedSlotChanges = true; + } else { + validatedConfig.extras.appendChildSlotFix = !!validatedConfig.extras.appendChildSlotFix; + validatedConfig.extras.cloneNodeFix = !!validatedConfig.extras.cloneNodeFix; + validatedConfig.extras.slotChildNodesFix = !!validatedConfig.extras.slotChildNodesFix; + validatedConfig.extras.scopedSlotTextContentFix = !!validatedConfig.extras.scopedSlotTextContentFix; + // TODO(STENCIL-1086): remove this option when it's the default behavior + validatedConfig.extras.experimentalScopedSlotChanges = !!validatedConfig.extras.experimentalScopedSlotChanges; + } + + setBooleanConfig(validatedConfig, 'watch', 'watch', false); + setBooleanConfig(validatedConfig, 'buildDocs', 'docs', !validatedConfig.devMode); + setBooleanConfig(validatedConfig, 'buildDist', 'esm', !validatedConfig.devMode || !!validatedConfig.buildEs5); + setBooleanConfig(validatedConfig, 'profile', 'profile', validatedConfig.devMode); + setBooleanConfig(validatedConfig, 'writeLog', 'log', false); + setBooleanConfig(validatedConfig, 'buildAppCore', null, true); + setBooleanConfig(validatedConfig, 'autoprefixCss', null, validatedConfig.buildEs5); + setBooleanConfig(validatedConfig, 'validateTypes', null, !validatedConfig._isTesting); + setBooleanConfig(validatedConfig, 'allowInlineScripts', null, true); + setBooleanConfig(validatedConfig, 'suppressReservedPublicNameWarnings', null, false); + + if (!isString(validatedConfig.taskQueue)) { + validatedConfig.taskQueue = 'async'; + } + + // hash file names + if (!isBoolean(validatedConfig.hashFileNames)) { + validatedConfig.hashFileNames = !validatedConfig.devMode; + } + if (!isNumber(validatedConfig.hashedFileNameLength)) { + validatedConfig.hashedFileNameLength = DEFAULT_HASHED_FILENAME_LENGTH; + } + if (validatedConfig.hashedFileNameLength < MIN_HASHED_FILENAME_LENGTH) { + const err = buildError(diagnostics); + err.messageText = `validatedConfig.hashedFileNameLength must be at least ${MIN_HASHED_FILENAME_LENGTH} characters`; + } + if (validatedConfig.hashedFileNameLength > MAX_HASHED_FILENAME_LENGTH) { + const err = buildError(diagnostics); + err.messageText = `validatedConfig.hashedFileNameLength cannot be more than ${MAX_HASHED_FILENAME_LENGTH} characters`; + } + if (!validatedConfig.env) { + validatedConfig.env = {}; + } + + // outputTargets + validateOutputTargets(validatedConfig, diagnostics); + + // plugins + validatePlugins(validatedConfig, diagnostics); + + // dev server + validatedConfig.devServer = validateDevServer(validatedConfig, diagnostics); + + // testing + validateTesting(validatedConfig, diagnostics); + + // bundles + if (Array.isArray(validatedConfig.bundles)) { + validatedConfig.bundles = sortBy(validatedConfig.bundles, (a: ConfigBundle) => a.components.length); + } else { + validatedConfig.bundles = []; + } + + // exclude components (tag list) + if (!Array.isArray(validatedConfig.excludeComponents)) { + validatedConfig.excludeComponents = []; + } + + // validate how many workers we can use + validateWorkers(validatedConfig); + + // default devInspector to whatever devMode is + setBooleanConfig(validatedConfig, 'devInspector', null, validatedConfig.devMode); + + if (!validatedConfig._isTesting) { + validateDistNamespace(validatedConfig, diagnostics); + } + + setBooleanConfig(validatedConfig, 'enableCache', 'cache', true); + + if (!Array.isArray(validatedConfig.watchIgnoredRegex) && validatedConfig.watchIgnoredRegex != null) { + validatedConfig.watchIgnoredRegex = [validatedConfig.watchIgnoredRegex]; + } + validatedConfig.watchIgnoredRegex = ((validatedConfig.watchIgnoredRegex as RegExp[]) || []).reduce((arr, reg) => { + if (reg instanceof RegExp) { + arr.push(reg); + } + return arr; + }, [] as RegExp[]); + + // TODO(STENCIL-1107): Remove this check. It'll be unneeded (and raise a compilation error when we build Stencil) once + // this property is removed. + if (validatedConfig.nodeResolve?.customResolveOptions) { + const warn = buildWarn(diagnostics); + // this message is particularly long - let the underlying logger implementation take responsibility for breaking it + // up to fit in a terminal window + warn.messageText = `nodeResolve.customResolveOptions is a deprecated option in a Stencil Configuration file. If you need this option, please open a new issue in the Stencil repository (https://github.com/stenciljs/core/issues/new/choose)`; + } + + CACHED_VALIDATED_CONFIG = validatedConfig; + + return { + config: validatedConfig, + diagnostics, + }; +}; diff --git a/packages/core/src/compiler/config/validate-copy.ts b/packages/core/src/compiler/config/validate-copy.ts new file mode 100644 index 00000000000..cb58cba79ec --- /dev/null +++ b/packages/core/src/compiler/config/validate-copy.ts @@ -0,0 +1,28 @@ +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}`); +}; diff --git a/packages/core/src/compiler/config/validate-dev-server.ts b/packages/core/src/compiler/config/validate-dev-server.ts new file mode 100644 index 00000000000..c23a011b440 --- /dev/null +++ b/packages/core/src/compiler/config/validate-dev-server.ts @@ -0,0 +1,198 @@ +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; +}; diff --git a/packages/core/src/compiler/config/validate-docs.ts b/packages/core/src/compiler/config/validate-docs.ts new file mode 100644 index 00000000000..9b2976d49a8 --- /dev/null +++ b/packages/core/src/compiler/config/validate-docs.ts @@ -0,0 +1,42 @@ +import * as d from '../../declarations'; +import { UnvalidatedConfig } from '../../declarations'; +import { isHexColor } from '../docs/readme/docs-util'; +import { DEFAULT_TARGET_COMPONENT_STYLES } from './constants'; + +/** + * Validate the `.docs` property on the supplied config object and + * return a properly-validated value. + * + * @param config the configuration we're examining + * @param logger the logger that will be set on the config + * @returns a suitable/default value for the docs property + */ +export const validateDocs = (config: UnvalidatedConfig, logger: d.Logger): d.ValidatedConfig['docs'] => { + const { background: defaultBackground, textColor: defaultTextColor } = DEFAULT_TARGET_COMPONENT_STYLES; + + let { background = defaultBackground, textColor = defaultTextColor } = + config.docs?.markdown?.targetComponent ?? DEFAULT_TARGET_COMPONENT_STYLES; + + if (!isHexColor(background)) { + logger.warn( + `'${background}' is not a valid hex color. The default value for diagram backgrounds ('${defaultBackground}') will be used.`, + ); + background = defaultBackground; + } + + if (!isHexColor(textColor)) { + logger.warn( + `'${textColor}' is not a valid hex color. The default value for diagram text ('${defaultTextColor}') will be used.`, + ); + textColor = defaultTextColor; + } + + return { + markdown: { + targetComponent: { + background, + textColor, + }, + }, + }; +}; diff --git a/packages/core/src/compiler/config/validate-hydrated.ts b/packages/core/src/compiler/config/validate-hydrated.ts new file mode 100644 index 00000000000..de0424fddde --- /dev/null +++ b/packages/core/src/compiler/config/validate-hydrated.ts @@ -0,0 +1,52 @@ +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; +}; diff --git a/packages/core/src/compiler/config/validate-namespace.ts b/packages/core/src/compiler/config/validate-namespace.ts new file mode 100644 index 00000000000..5afffb13771 --- /dev/null +++ b/packages/core/src/compiler/config/validate-namespace.ts @@ -0,0 +1,75 @@ +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.`; + } + } +}; diff --git a/packages/core/src/compiler/config/validate-paths.ts b/packages/core/src/compiler/config/validate-paths.ts new file mode 100644 index 00000000000..b594e49dcf0 --- /dev/null +++ b/packages/core/src/compiler/config/validate-paths.ts @@ -0,0 +1,85 @@ +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'; diff --git a/packages/core/src/compiler/config/validate-plugins.ts b/packages/core/src/compiler/config/validate-plugins.ts new file mode 100644 index 00000000000..55655eb779a --- /dev/null +++ b/packages/core/src/compiler/config/validate-plugins.ts @@ -0,0 +1,43 @@ +import { buildWarn } from '@utils'; + +import type * as d from '../../declarations'; + +export const validatePlugins = (config: d.UnvalidatedConfig, diagnostics: d.Diagnostic[]) => { + const userPlugins = config.plugins; + + if (!config.rollupPlugins) { + config.rollupPlugins = {}; + } + if (!Array.isArray(userPlugins)) { + config.plugins = []; + return; + } + + const rollupPlugins = userPlugins.filter((plugin) => { + return !!(plugin && typeof plugin === 'object' && !plugin.pluginType); + }); + + const hasResolveNode = rollupPlugins.some((p) => p.name === 'node-resolve'); + const hasCommonjs = rollupPlugins.some((p) => p.name === 'commonjs'); + + if (hasCommonjs) { + const warn = buildWarn(diagnostics); + warn.messageText = `Stencil already uses "@rollup/plugin-commonjs", please remove it from your "stencil.config.ts" plugins. + You can configure the commonjs settings using the "commonjs" property in "stencil.config.ts`; + } + + if (hasResolveNode) { + const warn = buildWarn(diagnostics); + warn.messageText = `Stencil already uses "@rollup/plugin-commonjs", please remove it from your "stencil.config.ts" plugins. + You can configure the commonjs settings using the "commonjs" property in "stencil.config.ts`; + } + + config.rollupPlugins.before = [ + ...(config.rollupPlugins.before || []), + ...rollupPlugins.filter(({ name }) => name !== 'node-resolve' && name !== 'commonjs'), + ]; + + config.plugins = userPlugins.filter((plugin) => { + return !!(plugin && typeof plugin === 'object' && plugin.pluginType); + }); +}; diff --git a/packages/core/src/compiler/config/validate-prerender.ts b/packages/core/src/compiler/config/validate-prerender.ts new file mode 100644 index 00000000000..78c52343cdb --- /dev/null +++ b/packages/core/src/compiler/config/validate-prerender.ts @@ -0,0 +1,38 @@ +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); + } + } +}; diff --git a/packages/core/src/compiler/config/validate-rollup-config.ts b/packages/core/src/compiler/config/validate-rollup-config.ts new file mode 100644 index 00000000000..d3940836a9a --- /dev/null +++ b/packages/core/src/compiler/config/validate-rollup-config.ts @@ -0,0 +1,52 @@ +import { isObject, pluck } from '@utils'; + +import type * as d from '../../declarations'; + +/** + * Ensure that a valid baseline rollup configuration is set on the validated + * config. + * + * If a config is present this will return a new config based on the user + * supplied one. + * + * If no config is present, this will return a default config. + * + * @param config a validated user-supplied configuration object + * @returns a validated rollup configuration + */ +export const validateRollupConfig = (config: d.Config): d.RollupConfig => { + let cleanRollupConfig = { ...DEFAULT_ROLLUP_CONFIG }; + + const rollupConfig = config.rollupConfig; + + if (!rollupConfig || !isObject(rollupConfig)) { + return cleanRollupConfig; + } + + if (rollupConfig.inputOptions && isObject(rollupConfig.inputOptions)) { + cleanRollupConfig = { + ...cleanRollupConfig, + inputOptions: pluck(rollupConfig.inputOptions, [ + 'context', + 'moduleContext', + 'treeshake', + 'external', + 'maxParallelFileOps', + ]), + }; + } + + if (rollupConfig.outputOptions && isObject(rollupConfig.outputOptions)) { + cleanRollupConfig = { + ...cleanRollupConfig, + outputOptions: pluck(rollupConfig.outputOptions, ['globals']), + }; + } + + return cleanRollupConfig; +}; + +const DEFAULT_ROLLUP_CONFIG: d.RollupConfig = { + inputOptions: {}, + outputOptions: {}, +}; diff --git a/packages/core/src/compiler/config/validate-service-worker.ts b/packages/core/src/compiler/config/validate-service-worker.ts new file mode 100644 index 00000000000..de067b309d3 --- /dev/null +++ b/packages/core/src/compiler/config/validate-service-worker.ts @@ -0,0 +1,105 @@ +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'; diff --git a/packages/core/src/compiler/config/validate-testing.ts b/packages/core/src/compiler/config/validate-testing.ts new file mode 100644 index 00000000000..767c7ef288e --- /dev/null +++ b/packages/core/src/compiler/config/validate-testing.ts @@ -0,0 +1,217 @@ +import { buildError, isOutputTargetDist, isOutputTargetWww, isString, join, normalizePath } from '@utils'; +import { basename, dirname, isAbsolute } from 'path'; + +import type * as d from '../../declarations'; +import { isLocalModule } from '../sys/resolve/resolve-utils'; + +export const validateTesting = (config: d.ValidatedConfig, diagnostics: d.Diagnostic[]) => { + const testing = (config.testing = Object.assign({}, config.testing || {})); + + if (!config.flags.e2e && !config.flags.spec) { + return; + } + + let configPathDir = config.configPath!; + if (isString(configPathDir)) { + if (basename(configPathDir).includes('.')) { + configPathDir = dirname(configPathDir); + } + } else { + configPathDir = config.rootDir!; + } + + if (typeof config.flags.headless === 'boolean' || config.flags.headless === 'shell') { + testing.browserHeadless = config.flags.headless; + } else if (typeof testing.browserHeadless !== 'boolean' && testing.browserHeadless !== 'shell') { + testing.browserHeadless = 'shell'; + } + + /** + * Using the deprecated `browserHeadless: true` flag causes Chrome to crash when running tests. + * Ensure users don't run into this by throwing a deliberate error. + */ + if (typeof testing.browserHeadless === 'boolean' && testing.browserHeadless) { + throw new Error(`Setting "browserHeadless" config to \`true\` is not supported anymore, please set it to "shell"!`); + } + + if (!testing.browserWaitUntil) { + testing.browserWaitUntil = 'load'; + } + + /** + * ensure we always test on stable Chrome + */ + if (!isString(testing.browserChannel)) { + testing.browserChannel = 'chrome'; + } + + testing.browserArgs = testing.browserArgs || []; + addTestingConfigOption(testing.browserArgs, '--font-render-hinting=medium'); + addTestingConfigOption(testing.browserArgs, '--incognito'); + if (config.flags.ci || process.env.CI) { + addTestingConfigOption(testing.browserArgs, '--no-sandbox'); + addTestingConfigOption(testing.browserArgs, '--disable-setuid-sandbox'); + addTestingConfigOption(testing.browserArgs, '--disable-dev-shm-usage'); + testing.browserHeadless = 'shell'; + } else if (config.flags.devtools || testing.browserDevtools) { + testing.browserDevtools = true; + testing.browserHeadless = false; + } + + if (typeof testing.rootDir === 'string') { + if (!isAbsolute(testing.rootDir)) { + testing.rootDir = join(config.rootDir!, testing.rootDir); + } + } else { + testing.rootDir = config.rootDir; + } + + if (typeof config.flags.screenshotConnector === 'string') { + testing.screenshotConnector = config.flags.screenshotConnector; + } + + if (typeof testing.screenshotConnector === 'string') { + if (!isAbsolute(testing.screenshotConnector)) { + testing.screenshotConnector = join(config.rootDir!, testing.screenshotConnector); + } else { + testing.screenshotConnector = normalizePath(testing.screenshotConnector); + } + } else { + testing.screenshotConnector = join( + config.sys!.getCompilerExecutingPath(), + '..', + '..', + 'screenshot', + 'local-connector.js', + ); + } + + /** + * We only allow numbers or null for the screenshotTimeout, so if we detect anything + * else, we set it to null. + */ + if (typeof testing.screenshotTimeout != 'number') { + testing.screenshotTimeout = null; + } + + if (!Array.isArray(testing.testPathIgnorePatterns)) { + testing.testPathIgnorePatterns = DEFAULT_IGNORE_PATTERNS.map((ignorePattern) => { + return join(testing.rootDir!, ignorePattern); + }); + + (config.outputTargets ?? []) + .filter( + (o): o is d.OutputTargetWww | d.OutputTargetDist => (isOutputTargetDist(o) || isOutputTargetWww(o)) && !!o.dir, + ) + .forEach((outputTarget) => { + testing.testPathIgnorePatterns?.push(outputTarget.dir!); + }); + } + + if (typeof testing.preset !== 'string') { + testing.preset = join(config.sys!.getCompilerExecutingPath(), '..', '..', 'testing'); + } else if (!isAbsolute(testing.preset)) { + testing.preset = join(configPathDir, testing.preset); + } + + if (!Array.isArray(testing.setupFilesAfterEnv)) { + testing.setupFilesAfterEnv = []; + } + + testing.setupFilesAfterEnv.unshift( + join(config.sys!.getCompilerExecutingPath(), '..', '..', 'testing', 'jest-setuptestframework.js'), + ); + + if (isString(testing.testEnvironment)) { + if (!isAbsolute(testing.testEnvironment) && isLocalModule(testing.testEnvironment)) { + testing.testEnvironment = join(configPathDir, testing.testEnvironment); + } + } + + if (typeof testing.allowableMismatchedPixels === 'number') { + if (testing.allowableMismatchedPixels < 0) { + const err = buildError(diagnostics); + err.messageText = `allowableMismatchedPixels must be a value that is 0 or greater`; + } + } else { + testing.allowableMismatchedPixels = DEFAULT_ALLOWABLE_MISMATCHED_PIXELS; + } + + if (typeof testing.allowableMismatchedRatio === 'number') { + if (testing.allowableMismatchedRatio < 0 || testing.allowableMismatchedRatio > 1) { + const err = buildError(diagnostics); + err.messageText = `allowableMismatchedRatio must be a value ranging from 0 to 1`; + } + } + + if (typeof testing.pixelmatchThreshold === 'number') { + if (testing.pixelmatchThreshold < 0 || testing.pixelmatchThreshold > 1) { + const err = buildError(diagnostics); + err.messageText = `pixelmatchThreshold must be a value ranging from 0 to 1`; + } + } else { + testing.pixelmatchThreshold = DEFAULT_PIXEL_MATCH_THRESHOLD; + } + + if (testing.testRegex === undefined) { + /** + * The test regex covers cases of: + * - files under a `__tests__` directory + * - the case where a test file has a name such as `test.ts`, `spec.ts` or `e2e.ts`. + * - these files can use any of the following file extensions: .ts, .tsx, .js, .jsx. + * - this regex only handles the entire path of a file, e.g. `/some/path/e2e.ts` + * - the case where a test file ends with `.test.ts`, `.spec.ts`, or `.e2e.ts`. + * - these files can use any of the following file extensions: .ts, .tsx, .js, .jsx. + * - this regex case shall match file names such as `my-cmp.spec.ts`, `test.spec.ts` + * - this regex case shall not match file names such as `attest.ts`, `bespec.ts` + */ + testing.testRegex = ['(/__tests__/.*|(\\.|/)(test|spec|e2e))\\.[jt]sx?$']; + } else if (typeof testing.testRegex === 'string') { + testing.testRegex = [testing.testRegex]; + } + + if (Array.isArray(testing.testMatch)) { + delete testing.testRegex; + } else if (typeof testing.testRegex === 'string') { + delete testing.testMatch; + } + + if (typeof testing.runner !== 'string') { + testing.runner = join(config.sys!.getCompilerExecutingPath(), '..', '..', 'testing', 'jest-runner.js'); + } + + if (typeof testing.waitBeforeScreenshot === 'number') { + if (testing.waitBeforeScreenshot < 0) { + const err = buildError(diagnostics); + err.messageText = `waitBeforeScreenshot must be a value that is 0 or greater`; + } + } else { + testing.waitBeforeScreenshot = 10; + } + + if (!Array.isArray(testing.emulate) || testing.emulate.length === 0) { + testing.emulate = [ + { + userAgent: 'default', + viewport: { + width: 600, + height: 600, + deviceScaleFactor: 1, + isMobile: false, + hasTouch: false, + isLandscape: false, + }, + }, + ]; + } +}; + +const addTestingConfigOption = (setArray: string[], option: string) => { + if (!setArray.includes(option)) { + setArray.push(option); + } +}; + +const DEFAULT_ALLOWABLE_MISMATCHED_PIXELS = 100; +const DEFAULT_PIXEL_MATCH_THRESHOLD = 0.1; +const DEFAULT_IGNORE_PATTERNS = ['.vscode', '.stencil', 'node_modules']; diff --git a/packages/core/src/compiler/config/validate-workers.ts b/packages/core/src/compiler/config/validate-workers.ts new file mode 100644 index 00000000000..82ab0467c83 --- /dev/null +++ b/packages/core/src/compiler/config/validate-workers.ts @@ -0,0 +1,19 @@ +import type * as d from '../../declarations'; + +export const validateWorkers = (config: d.ValidatedConfig) => { + if (typeof config.maxConcurrentWorkers !== 'number') { + config.maxConcurrentWorkers = 8; + } + + if (typeof config.flags.maxWorkers === 'number') { + config.maxConcurrentWorkers = config.flags.maxWorkers; + } else if (config.flags.ci) { + config.maxConcurrentWorkers = 4; + } + + config.maxConcurrentWorkers = Math.max(Math.min(config.maxConcurrentWorkers, 16), 0); + + if (config.devServer) { + config.devServer.worker = config.maxConcurrentWorkers > 0; + } +}; diff --git a/packages/core/src/compiler/docs/cem/index.ts b/packages/core/src/compiler/docs/cem/index.ts new file mode 100644 index 00000000000..39aef525446 --- /dev/null +++ b/packages/core/src/compiler/docs/cem/index.ts @@ -0,0 +1,369 @@ +import { dashToPascalCase, isOutputTargetDocsCustomElementsManifest } from '@utils'; + +import type * as d from '../../../declarations'; + +/** + * Generate Custom Elements Manifest (custom-elements.json) output + * conforming to the Custom Elements Manifest specification. + * @see https://github.com/webcomponents/custom-elements-manifest + * + * @param compilerCtx the current compiler context + * @param docsData the generated docs data from Stencil components + * @param outputTargets the output targets configured for the build + */ +export const generateCustomElementsManifestDocs = async ( + compilerCtx: d.CompilerCtx, + docsData: d.JsonDocs, + outputTargets: d.OutputTarget[], +): Promise => { + const cemOutputTargets = outputTargets.filter(isOutputTargetDocsCustomElementsManifest); + if (cemOutputTargets.length === 0) { + return; + } + + const manifest = generateManifest(docsData); + const jsonContent = JSON.stringify(manifest, null, 2); + + await Promise.all(cemOutputTargets.map((outputTarget) => compilerCtx.fs.writeFile(outputTarget.file!, jsonContent))); +}; + +/** + * Generate the Custom Elements Manifest from Stencil docs data + * @param docsData the generated docs data + * @returns the Custom Elements Manifest object + */ +const generateManifest = (docsData: d.JsonDocs): CustomElementsManifest => { + // Group components by their source file path + const componentsByFile = new Map(); + + for (const component of docsData.components) { + const filePath = component.filePath; + if (!componentsByFile.has(filePath)) { + componentsByFile.set(filePath, []); + } + componentsByFile.get(filePath)!.push(component); + } + + const modules: JavaScriptModule[] = []; + + for (const [filePath, components] of componentsByFile) { + const declarations: CustomElementDeclaration[] = components.map((component) => componentToDeclaration(component)); + + const exports: (JavaScriptExport | CustomElementExport)[] = components.flatMap((component) => { + const className = dashToPascalCase(component.tag); + return [ + { + kind: 'js' as const, + name: className, + declaration: { + name: className, + }, + }, + { + kind: 'custom-element-definition' as const, + name: component.tag, + declaration: { + name: className, + }, + }, + ]; + }); + + modules.push({ + kind: 'javascript-module', + path: filePath, + declarations, + exports, + }); + } + + return { + schemaVersion: '2.1.0', + modules, + }; +}; + +/** + * Convert Stencil's ComponentCompilerTypeReferences to CEM TypeReference array + * @param references Stencil's type references map + * @returns CEM TypeReference array + */ +const convertTypeReferences = (references?: d.ComponentCompilerTypeReferences): TypeReference[] | undefined => { + if (!references || Object.keys(references).length === 0) { + return undefined; + } + + return Object.entries(references).map(([name, ref]) => ({ + name, + // Global types (like HTMLElement, Array) get 'global:' package + ...(ref.location === 'global' && { package: 'global:' }), + // Imported types get their module path + ...(ref.location === 'import' && ref.path && { module: ref.path }), + // Local types don't need package or module (they're in the same module) + })); +}; + +/** + * Create a CEM Type object from a type string and optional references + * @param text the type string + * @param references Stencil's type references map + * @returns CEM Type object + */ +const createType = (text: string, references?: d.ComponentCompilerTypeReferences): Type => { + const typeRefs = convertTypeReferences(references); + return { + text, + ...(typeRefs && { references: typeRefs }), + }; +}; + +/** + * Convert a Stencil component to a Custom Element Declaration + * @param component the Stencil component docs data + * @returns the Custom Element Declaration + */ +const componentToDeclaration = (component: d.JsonDocsComponent): CustomElementDeclaration => { + const className = dashToPascalCase(component.tag); + + const attributes: Attribute[] = component.props + .filter((prop) => prop.attr !== undefined) + .map((prop) => ({ + name: prop.attr!, + ...(prop.docs && { description: prop.docs }), + ...(prop.type && { type: createType(prop.type, prop.complexType?.references) }), + ...(prop.default !== undefined && { default: prop.default }), + fieldName: prop.name, + ...(prop.deprecation !== undefined && { deprecated: prop.deprecation || true }), + })); + + const members: (CustomElementField | ClassMethod)[] = [ + // Fields (properties) + ...component.props.map( + (prop): CustomElementField => ({ + kind: 'field', + name: prop.name, + ...(prop.docs && { description: prop.docs }), + ...(prop.type && { type: createType(prop.type, prop.complexType?.references) }), + ...(prop.default !== undefined && { default: prop.default }), + ...(prop.deprecation !== undefined && { deprecated: prop.deprecation || true }), + ...(!prop.mutable && { readonly: true }), + ...(prop.attr && { attribute: prop.attr }), + ...(prop.reflectToAttr && { reflects: true }), + }), + ), + // Methods + ...component.methods.map( + (method): ClassMethod => ({ + kind: 'method', + name: method.name, + ...(method.docs && { description: method.docs }), + ...(method.deprecation !== undefined && { deprecated: method.deprecation || true }), + ...(method.parameters && + method.parameters.length > 0 && { + parameters: method.parameters.map((param) => ({ + name: param.name, + ...(param.docs && { description: param.docs }), + ...(param.type && { type: createType(param.type, method.complexType?.references) }), + })), + }), + ...(method.returns && { + return: { + ...(method.returns.type && { type: createType(method.returns.type, method.complexType?.references) }), + ...(method.returns.docs && { description: method.returns.docs }), + }, + }), + }), + ), + ]; + + const events: Event[] = component.events.map((event) => ({ + name: event.event, + ...(event.docs && { description: event.docs }), + type: createType(event.detail ? `CustomEvent<${event.detail}>` : 'CustomEvent', event.complexType?.references), + ...(event.deprecation !== undefined && { deprecated: event.deprecation || true }), + })); + + const slots: Slot[] = component.slots.map((slot) => ({ + name: slot.name, + ...(slot.docs && { description: slot.docs }), + })); + + const cssParts: CssPart[] = component.parts.map((part) => ({ + name: part.name, + ...(part.docs && { description: part.docs }), + })); + + const cssProperties: CssCustomProperty[] = component.styles + .filter((style) => style.annotation === 'prop') + .map((style) => ({ + name: style.name, + ...(style.docs && { description: style.docs }), + })); + + // Generate demos from usage examples + const demos: Demo[] = Object.entries(component.usage || {}).map(([name, content]) => ({ + // Create relative URL from usagesDir + filename + url: component.usagesDir ? `${component.usagesDir}/${name}.md` : `${name}.md`, + ...(content && { description: content }), + })); + + return { + kind: 'class', + customElement: true, + tagName: component.tag, + name: className, + ...(component.docs && { description: component.docs }), + ...(component.deprecation !== undefined && { deprecated: component.deprecation || true }), + ...(attributes.length > 0 && { attributes }), + ...(members.length > 0 && { members }), + ...(events.length > 0 && { events }), + ...(slots.length > 0 && { slots }), + ...(cssParts.length > 0 && { cssParts }), + ...(cssProperties.length > 0 && { cssProperties }), + ...(component.customStates.length > 0 && { + customStates: component.customStates.map((state) => ({ + name: state.name, + initialValue: state.initialValue, + ...(state.docs && { description: state.docs }), + })), + }), + ...(demos.length > 0 && { demos }), + }; +}; + +// Custom Elements Manifest Types +// Based on https://github.com/webcomponents/custom-elements-manifest/blob/main/schema.d.ts + +interface CustomElementsManifest { + schemaVersion: string; + modules: JavaScriptModule[]; +} + +interface JavaScriptModule { + kind: 'javascript-module'; + path: string; + declarations?: CustomElementDeclaration[]; + exports?: (JavaScriptExport | CustomElementExport)[]; +} + +interface JavaScriptExport { + kind: 'js'; + name: string; + declaration: Reference; +} + +interface CustomElementExport { + kind: 'custom-element-definition'; + name: string; + declaration: Reference; +} + +interface Reference { + name: string; + package?: string; + module?: string; +} + +interface CustomElementDeclaration { + kind: 'class'; + customElement: true; + tagName: string; + name: string; + description?: string; + deprecated?: boolean | string; + attributes?: Attribute[]; + members?: (CustomElementField | ClassMethod)[]; + events?: Event[]; + slots?: Slot[]; + cssParts?: CssPart[]; + cssProperties?: CssCustomProperty[]; + customStates?: CustomState[]; + demos?: Demo[]; +} + +interface Demo { + url: string; + description?: string; +} + +interface Attribute { + name: string; + description?: string; + type?: Type; + default?: string; + fieldName?: string; + deprecated?: boolean | string; +} + +interface Type { + text: string; + references?: TypeReference[]; +} + +interface TypeReference { + name: string; + package?: string; + module?: string; +} + +interface CustomElementField { + kind: 'field'; + name: string; + description?: string; + type?: Type; + default?: string; + deprecated?: boolean | string; + readonly?: boolean; + attribute?: string; + reflects?: boolean; +} + +interface ClassMethod { + kind: 'method'; + name: string; + description?: string; + deprecated?: boolean | string; + parameters?: Parameter[]; + return?: { + type?: Type; + description?: string; + }; +} + +interface Parameter { + name: string; + description?: string; + type?: Type; +} + +interface Event { + name: string; + description?: string; + type: Type; + deprecated?: boolean | string; +} + +interface Slot { + name: string; + description?: string; +} + +interface CssPart { + name: string; + description?: string; +} + +/** + * Custom state that can be targeted with the CSS :state() pseudo-class. + * This is a custom extension to the CEM spec. + */ +interface CustomState { + name: string; + initialValue: boolean; + description?: string; +} + +interface CssCustomProperty { + name: string; + description?: string; +} diff --git a/packages/core/src/compiler/docs/constants.ts b/packages/core/src/compiler/docs/constants.ts new file mode 100644 index 00000000000..8a5546584bd --- /dev/null +++ b/packages/core/src/compiler/docs/constants.ts @@ -0,0 +1,2 @@ +export const AUTO_GENERATE_COMMENT = ``; +export const NOTE = `*Built with [StencilJS](https://stenciljs.com/)*`; diff --git a/packages/core/src/compiler/docs/custom/index.ts b/packages/core/src/compiler/docs/custom/index.ts new file mode 100644 index 00000000000..600f25e1a78 --- /dev/null +++ b/packages/core/src/compiler/docs/custom/index.ts @@ -0,0 +1,23 @@ +import { isOutputTargetDocsCustom } from '@utils'; + +import type * as d from '../../../declarations'; + +export const generateCustomDocs = async ( + config: d.ValidatedConfig, + docsData: d.JsonDocs, + outputTargets: d.OutputTarget[], +) => { + const customOutputTargets = outputTargets.filter(isOutputTargetDocsCustom); + if (customOutputTargets.length === 0) { + return; + } + await Promise.all( + customOutputTargets.map(async (customOutput) => { + try { + await customOutput.generator(docsData, config); + } catch (e) { + config.logger.error(`uncaught custom docs error: ${e}`); + } + }), + ); +}; diff --git a/packages/core/src/compiler/docs/generate-doc-data.ts b/packages/core/src/compiler/docs/generate-doc-data.ts new file mode 100644 index 00000000000..c3e24aead63 --- /dev/null +++ b/packages/core/src/compiler/docs/generate-doc-data.ts @@ -0,0 +1,535 @@ +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; +}; diff --git a/packages/core/src/compiler/docs/json/index.ts b/packages/core/src/compiler/docs/json/index.ts new file mode 100644 index 00000000000..24dd13418f7 --- /dev/null +++ b/packages/core/src/compiler/docs/json/index.ts @@ -0,0 +1,77 @@ +import { isOutputTargetDocsJson, join } from '@utils'; + +import type * as d from '../../../declarations'; + +export const generateJsonDocs = async ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + docsData: d.JsonDocs, + outputTargets: d.OutputTarget[], +) => { + const jsonOutputTargets = outputTargets.filter(isOutputTargetDocsJson); + if (jsonOutputTargets.length === 0) { + return; + } + const docsDtsPath = join(config.sys.getCompilerExecutingPath(), '..', '..', 'internal', 'stencil-public-docs.d.ts'); + let docsDts = await compilerCtx.fs.readFile(docsDtsPath); + // this file was written by dts-bundle-generator, which uses tabs for + // indentation. Instead, let's replace those with spaces! + docsDts = docsDts + .split('\n') + .map((line) => line.replace(/\t/g, ' ')) + .join('\n'); + + const typesContent = ` +/** + * This is an autogenerated file created by the Stencil compiler. + * DO NOT MODIFY IT MANUALLY + */ +${docsDts} +declare const _default: JsonDocs; +export default _default; +`; + + const json = { + ...docsData, + components: docsData.components.map((cmp) => ({ + filePath: cmp.filePath, + + encapsulation: cmp.encapsulation, + tag: cmp.tag, + readme: cmp.readme, + docs: cmp.docs, + docsTags: cmp.docsTags, + usage: cmp.usage, + props: cmp.props, + methods: cmp.methods, + events: cmp.events, + listeners: cmp.listeners, + styles: cmp.styles, + slots: cmp.slots, + parts: cmp.parts, + states: cmp.customStates, + dependents: cmp.dependents, + dependencies: cmp.dependencies, + dependencyGraph: cmp.dependencyGraph, + deprecation: cmp.deprecation, + })), + }; + const jsonContent = JSON.stringify(json, null, 2); + await Promise.all( + jsonOutputTargets.map((jsonOutput) => { + return writeDocsOutput(compilerCtx, jsonOutput, jsonContent, typesContent); + }), + ); +}; + +export const writeDocsOutput = async ( + compilerCtx: d.CompilerCtx, + jsonOutput: d.OutputTargetDocsJson, + jsonContent: string, + typesContent: string, +) => { + return Promise.all([ + compilerCtx.fs.writeFile(jsonOutput.file, jsonContent), + jsonOutput.typesFile ? compilerCtx.fs.writeFile(jsonOutput.typesFile, typesContent) : (Promise.resolve() as any), + ]); +}; diff --git a/packages/core/src/compiler/docs/readme/docs-util.ts b/packages/core/src/compiler/docs/readme/docs-util.ts new file mode 100644 index 00000000000..0394b671b7c --- /dev/null +++ b/packages/core/src/compiler/docs/readme/docs-util.ts @@ -0,0 +1,158 @@ +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; +} diff --git a/packages/core/src/compiler/docs/readme/index.ts b/packages/core/src/compiler/docs/readme/index.ts new file mode 100644 index 00000000000..db1b2e5797f --- /dev/null +++ b/packages/core/src/compiler/docs/readme/index.ts @@ -0,0 +1,51 @@ +import { isOutputTargetDocsReadme } from '@utils'; + +import type * as d from '../../../declarations'; +import { generateReadme } from './output-docs'; + +export const generateReadmeDocs = async ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + docsData: d.JsonDocs, + outputTargets: d.OutputTarget[], +) => { + const readmeOutputTargets = outputTargets.filter(isOutputTargetDocsReadme); + if (readmeOutputTargets.length === 0) { + return; + } + const strictCheck = readmeOutputTargets.some((o) => o.strict); + if (strictCheck) { + strictCheckDocs(config, docsData); + } + + await Promise.all( + docsData.components.map((cmpData) => { + return generateReadme(config, compilerCtx, readmeOutputTargets, cmpData, docsData.components); + }), + ); +}; + +export const strictCheckDocs = (config: d.ValidatedConfig, docsData: d.JsonDocs) => { + docsData.components.forEach((component) => { + component.props.forEach((prop) => { + if (!prop.docs && prop.deprecation === undefined) { + config.logger.warn(`Property "${prop.name}" of "${component.tag}" is not documented. ${component.filePath}`); + } + }); + component.methods.forEach((method) => { + if (!method.docs && method.deprecation === undefined) { + config.logger.warn(`Method "${method.name}" of "${component.tag}" is not documented. ${component.filePath}`); + } + }); + component.events.forEach((ev) => { + if (!ev.docs && ev.deprecation === undefined) { + config.logger.warn(`Event "${ev.event}" of "${component.tag}" is not documented. ${component.filePath}`); + } + }); + component.parts.forEach((ev) => { + if (ev.docs === '') { + config.logger.warn(`Part "${ev.name}" of "${component.tag}" is not documented. ${component.filePath}`); + } + }); + }); +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-css-props.ts b/packages/core/src/compiler/docs/readme/markdown-css-props.ts new file mode 100644 index 00000000000..2784d34b12c --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-css-props.ts @@ -0,0 +1,25 @@ +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; +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-custom-states.ts b/packages/core/src/compiler/docs/readme/markdown-custom-states.ts new file mode 100644 index 00000000000..9c2f98631dd --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-custom-states.ts @@ -0,0 +1,30 @@ +import type * as d from '../../../declarations'; +import { MarkdownTable } from './docs-util'; + +/** + * Converts a list of Custom States metadata to a table written in Markdown + * @param customStates the Custom States metadata to convert + * @returns a list of strings that make up the Markdown table + */ +export const customStatesToMarkdown = (customStates: d.JsonDocsCustomState[]): ReadonlyArray => { + const content: string[] = []; + if (customStates.length === 0) { + return content; + } + + content.push(`## Custom States`); + content.push(``); + + const table = new MarkdownTable(); + table.addHeader(['State', 'Initial Value', 'Description']); + + customStates.forEach((state) => { + table.addRow([`\`:state(${state.name})\``, state.initialValue ? '`true`' : '`false`', state.docs]); + }); + + content.push(...table.toMarkdown()); + content.push(``); + content.push(``); + + return content; +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-dependencies.ts b/packages/core/src/compiler/docs/readme/markdown-dependencies.ts new file mode 100644 index 00000000000..ab38410ea95 --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-dependencies.ts @@ -0,0 +1,62 @@ +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; +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-events.ts b/packages/core/src/compiler/docs/readme/markdown-events.ts new file mode 100644 index 00000000000..cd754a63623 --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-events.ts @@ -0,0 +1,34 @@ +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}`; +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-methods.ts b/packages/core/src/compiler/docs/readme/markdown-methods.ts new file mode 100644 index 00000000000..45f25fae862 --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-methods.ts @@ -0,0 +1,55 @@ +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}`; +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-overview.ts b/packages/core/src/compiler/docs/readme/markdown-overview.ts new file mode 100644 index 00000000000..f9ace8783ed --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-overview.ts @@ -0,0 +1,18 @@ +/** + * 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; +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-parts.ts b/packages/core/src/compiler/docs/readme/markdown-parts.ts new file mode 100644 index 00000000000..d105939191e --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-parts.ts @@ -0,0 +1,30 @@ +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; +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-props.ts b/packages/core/src/compiler/docs/readme/markdown-props.ts new file mode 100644 index 00000000000..2da4ddea309 --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-props.ts @@ -0,0 +1,56 @@ +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}\``; +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-slots.ts b/packages/core/src/compiler/docs/readme/markdown-slots.ts new file mode 100644 index 00000000000..4c76c0ad212 --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-slots.ts @@ -0,0 +1,30 @@ +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; +}; diff --git a/packages/core/src/compiler/docs/readme/markdown-usage.ts b/packages/core/src/compiler/docs/readme/markdown-usage.ts new file mode 100644 index 00000000000..fcfce66bfd1 --- /dev/null +++ b/packages/core/src/compiler/docs/readme/markdown-usage.ts @@ -0,0 +1,44 @@ +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; +}; diff --git a/packages/core/src/compiler/docs/readme/output-docs.ts b/packages/core/src/compiler/docs/readme/output-docs.ts new file mode 100644 index 00000000000..208271ddee8 --- /dev/null +++ b/packages/core/src/compiler/docs/readme/output-docs.ts @@ -0,0 +1,206 @@ +import { join, normalizePath, relative } from '@utils'; + +import type * as d from '../../../declarations'; +import { AUTO_GENERATE_COMMENT } from '../constants'; +import { getUserReadmeContent } from '../generate-doc-data'; +import { stylesToMarkdown } from './markdown-css-props'; +import { customStatesToMarkdown } from './markdown-custom-states'; +import { depsToMarkdown } from './markdown-dependencies'; +import { eventsToMarkdown } from './markdown-events'; +import { methodsToMarkdown } from './markdown-methods'; +import { overviewToMarkdown } from './markdown-overview'; +import { partsToMarkdown } from './markdown-parts'; +import { propsToMarkdown } from './markdown-props'; +import { slotsToMarkdown } from './markdown-slots'; +import { usageToMarkdown } from './markdown-usage'; + +/** + * Generate a README for a given component and write it to disk. + * + * Typically the README is going to be a 'sibling' to the component's source + * code (i.e. written to the same directory) but the user may also configure a + * custom output directory by setting {@link d.OutputTargetDocsReadme.dir}. + * + * Output readme files also include {@link AUTO_GENERATE_COMMENT}, and any + * text located _above_ that comment is preserved when the new readme is written + * to disk. + * + * @param config a validated Stencil config + * @param compilerCtx the current compiler context + * @param readmeOutputs docs-readme output targets + * @param docsData documentation data for the component of interest + * @param cmps metadata for all the components in the project + */ +export const generateReadme = async ( + config: d.ValidatedConfig, + compilerCtx: d.CompilerCtx, + readmeOutputs: d.OutputTargetDocsReadme[], + docsData: d.JsonDocsComponent, + cmps: d.JsonDocsComponent[], +) => { + const isUpdate = !!docsData.readme; + const userContent = isUpdate ? docsData.readme : getDefaultReadme(docsData); + + await Promise.all( + readmeOutputs.map(async (readmeOutput) => { + if (readmeOutput.dir) { + const relativeReadmePath = relative(config.srcDir, docsData.readmePath); + const readmeOutputPath = join(readmeOutput.dir, relativeReadmePath); + + const currentReadmeContent = + readmeOutput.overwriteExisting === true + ? // Overwrite explicitly requested: always use the provided user content. + userContent + : normalizePath(readmeOutput.dir) !== normalizePath(config.srcDir) + ? (readmeOutput.overwriteExisting === 'if-missing' && + // Validate a file exists at the output path + (await compilerCtx.fs.access(readmeOutputPath))) || + // False and undefined case: follow the changes made in #5648 + (readmeOutput.overwriteExisting ?? false) === false + ? // Existing file found: The user set a custom `.dir` property, which is + // where we're going to write the updated README. We need to read the + // non-automatically generated content from that file and preserve that. + await getUserReadmeContent(compilerCtx, readmeOutputPath) + : // No existing file found: use the provided user content. + userContent + : // Default case: writing to srcDir, so use the provided user content. + userContent; + + // CSS Custom Properties preservation is now handled centrally in outputDocs + const readmeContent = generateMarkdown(currentReadmeContent, docsData, cmps, readmeOutput, config); + + const results = await compilerCtx.fs.writeFile(readmeOutputPath, readmeContent); + if (results.changedContent) { + if (isUpdate) { + config.logger.info(`updated readme docs: ${docsData.tag}`); + } else { + config.logger.info(`created readme docs: ${docsData.tag}`); + } + } + } + }), + ); +}; + +export const generateMarkdown = ( + userContent: string | undefined, + cmp: d.JsonDocsComponent, + cmps: d.JsonDocsComponent[], + readmeOutput: d.OutputTargetDocsReadme, + config?: d.ValidatedConfig, +) => { + //If the readmeOutput.dependencies is true or undefined the dependencies will be generated. + const dependencies = readmeOutput.dependencies !== false ? depsToMarkdown(cmp, cmps, config) : []; + + return [ + userContent || '', + AUTO_GENERATE_COMMENT, + '', + '', + ...getDocsDeprecation(cmp), + ...overviewToMarkdown(cmp.overview), + ...usageToMarkdown(cmp.usage), + ...propsToMarkdown(cmp.props), + ...eventsToMarkdown(cmp.events), + ...methodsToMarkdown(cmp.methods), + ...slotsToMarkdown(cmp.slots), + ...partsToMarkdown(cmp.parts), + ...customStatesToMarkdown(cmp.customStates), + ...stylesToMarkdown(cmp.styles), + ...dependencies, + `----------------------------------------------`, + '', + readmeOutput.footer, + '', + ].join('\n'); +}; + +const getDocsDeprecation = (cmp: d.JsonDocsComponent) => { + if (cmp.deprecation !== undefined) { + return [`> **[DEPRECATED]** ${cmp.deprecation}`, '']; + } + return []; +}; + +/** + * Get a minimal default README for a Stencil component + * + * @param docsData documentation data for the component of interest + * @returns a minimal README template for that component + */ +const getDefaultReadme = (docsData: d.JsonDocsComponent) => { + return [`# ${docsData.tag}`, '', '', ''].join('\n'); +}; + +/** + * Extract the existing CSS Custom Properties section from a README file. + * This is used to preserve CSS props documentation when running `stencil docs` + * without building styles. + * + * @param compilerCtx the current compiler context + * @param readmePath the path to the README file to read + * @returns array of CSS custom properties styles, or undefined if none found + */ +export const extractExistingCssProps = async ( + compilerCtx: d.CompilerCtx, + readmePath: string, +): Promise => { + try { + const existingContent = await compilerCtx.fs.readFile(readmePath); + + // Find the CSS Custom Properties section + const cssPropsSectionMatch = existingContent.match( + /## CSS Custom Properties\s*\n\s*\n([\s\S]*?)(?=\n##|\n-{4,}|$)/, + ); + if (!cssPropsSectionMatch) { + return undefined; + } + + const cssPropsSection = cssPropsSectionMatch[1]; + const styles: d.JsonDocsStyle[] = []; + + // Parse the markdown table to extract CSS custom properties + // Table format: + // | Name | Description | + // | ---- | ----------- | + // | `--prop-name` | Description text | + const lines = cssPropsSection.split('\n'); + let inTable = false; + + for (const line of lines) { + const trimmedLine = line.trim(); + + // Skip header and separator rows + if (trimmedLine.startsWith('| Name') || trimmedLine.startsWith('| ---')) { + inTable = true; + continue; + } + + // Parse table rows + if (inTable && trimmedLine.startsWith('|')) { + const parts = trimmedLine + .split('|') + .map((p) => p.trim()) + .filter((p) => p); + if (parts.length >= 2) { + // Extract the CSS variable name (remove backticks) + const name = parts[0].replace(/`/g, '').trim(); + const docs = parts[1].trim(); + + if (name.startsWith('--')) { + styles.push({ + name, + docs, + annotation: 'prop', + mode: undefined, + }); + } + } + } + } + + return styles.length > 0 ? styles : undefined; + } catch (e) { + return undefined; + } +}; diff --git a/packages/core/src/compiler/docs/style-docs.ts b/packages/core/src/compiler/docs/style-docs.ts new file mode 100644 index 00000000000..c037fd64ea5 --- /dev/null +++ b/packages/core/src/compiler/docs/style-docs.ts @@ -0,0 +1,103 @@ +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'; diff --git a/packages/core/src/compiler/docs/test/custom-elements-manifest.spec.ts b/packages/core/src/compiler/docs/test/custom-elements-manifest.spec.ts new file mode 100644 index 00000000000..7d6d0c21ff1 --- /dev/null +++ b/packages/core/src/compiler/docs/test/custom-elements-manifest.spec.ts @@ -0,0 +1,615 @@ +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, + }; +} diff --git a/packages/core/src/compiler/docs/test/docs-util.spec.ts b/packages/core/src/compiler/docs/test/docs-util.spec.ts new file mode 100644 index 00000000000..48ae9b8caf3 --- /dev/null +++ b/packages/core/src/compiler/docs/test/docs-util.spec.ts @@ -0,0 +1,73 @@ +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); + }); +}); diff --git a/packages/core/src/compiler/docs/test/generate-doc-data.spec.ts b/packages/core/src/compiler/docs/test/generate-doc-data.spec.ts new file mode 100644 index 00000000000..2e21f74ed08 --- /dev/null +++ b/packages/core/src/compiler/docs/test/generate-doc-data.spec.ts @@ -0,0 +1,411 @@ +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', + }, + ]); + }); + }); +}); diff --git a/packages/core/src/compiler/docs/test/markdown-dependencies.spec.ts b/packages/core/src/compiler/docs/test/markdown-dependencies.spec.ts new file mode 100644 index 00000000000..4403fa9cd5a --- /dev/null +++ b/packages/core/src/compiler/docs/test/markdown-dependencies.spec.ts @@ -0,0 +1,153 @@ +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`, + '```', + '', + ]); + }); +}); diff --git a/packages/core/src/compiler/docs/test/markdown-overview.spec.ts b/packages/core/src/compiler/docs/test/markdown-overview.spec.ts new file mode 100644 index 00000000000..c487968c387 --- /dev/null +++ b/packages/core/src/compiler/docs/test/markdown-overview.spec.ts @@ -0,0 +1,50 @@ +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. +`); + }); + }); +}); diff --git a/packages/core/src/compiler/docs/test/markdown-props.spec.ts b/packages/core/src/compiler/docs/test/markdown-props.spec.ts new file mode 100644 index 00000000000..ba1e543538c --- /dev/null +++ b/packages/core/src/compiler/docs/test/markdown-props.spec.ts @@ -0,0 +1,130 @@ +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\` | + +`); + }); +}); diff --git a/packages/core/src/compiler/docs/test/output-docs.spec.ts b/packages/core/src/compiler/docs/test/output-docs.spec.ts new file mode 100644 index 00000000000..898c5829d21 --- /dev/null +++ b/packages/core/src/compiler/docs/test/output-docs.spec.ts @@ -0,0 +1,110 @@ +import type * as d from '../../../declarations'; +import { generateMarkdown } from '../readme/output-docs'; + +describe('css-props to markdown', () => { + describe('generateMarkdown', () => { + const mockReadmeOutput: d.OutputTargetDocsReadme = { + type: 'docs-readme', + footer: '*Built with StencilJS*', + }; + + const mockComponent: d.JsonDocsComponent = { + tag: 'my-component', + filePath: 'src/components/my-component/my-component.tsx', + fileName: 'my-component.tsx', + dirPath: 'src/components/my-component', + readmePath: 'src/components/my-component/readme.md', + usagesDir: 'src/components/my-component/usage', + encapsulation: 'shadow', + docs: '', + docsTags: [], + usage: {}, + props: [], + methods: [], + events: [], + listeners: [], + styles: [], + slots: [], + parts: [], + dependents: [], + dependencies: [], + dependencyGraph: {}, + customStates: [], + readme: '', + }; + + it.each([ + { + name: 'component styles when available', + componentStyles: [ + { name: '--background', docs: 'Background color', annotation: 'prop' as const, mode: undefined }, + { name: '--color', docs: 'Text color', annotation: 'prop' as const, mode: undefined }, + ], + shouldContain: ['## CSS Custom Properties', '`--background`', 'Background color', '`--color`', 'Text color'], + shouldNotContain: [], + }, + { + name: 'preserved CSS props (already in component.styles)', + componentStyles: [ + { + name: '--bg', + docs: 'Defaults to var(--nano-color-blue-cerulean-1000);', + annotation: 'prop' as const, + mode: undefined, + }, + { name: '--text-color', docs: 'Text color of the component', annotation: 'prop' as const, mode: undefined }, + ], + shouldContain: ['## CSS Custom Properties', '`--bg`', 'Defaults to var(--nano-color-blue-cerulean-1000);'], + shouldNotContain: [], + }, + { + name: 'no CSS section when styles are empty', + componentStyles: [], + shouldContain: [], + shouldNotContain: ['## CSS Custom Properties'], + }, + { + name: 'updated component styles', + componentStyles: [ + { name: '--new-prop', docs: 'New property from build', annotation: 'prop' as const, mode: undefined }, + ], + shouldContain: ['`--new-prop`', 'New property from build'], + shouldNotContain: [], + }, + ])('should use $name', ({ componentStyles, shouldContain, shouldNotContain }) => { + const component: d.JsonDocsComponent = { + ...mockComponent, + styles: componentStyles, + }; + + const markdown = generateMarkdown('# my-component', component, [], mockReadmeOutput); + + shouldContain.forEach((expected) => { + expect(markdown).toContain(expected); + }); + + shouldNotContain.forEach((unexpected) => { + expect(markdown).not.toContain(unexpected); + }); + }); + + it('should escape special characters in CSS prop descriptions', () => { + const component: d.JsonDocsComponent = { + ...mockComponent, + styles: [ + { + name: '--bg', + docs: 'Defaults to var(--nano-color-blue-cerulean-1000); with | pipes', + annotation: 'prop', + mode: undefined, + }, + ], + }; + + const markdown = generateMarkdown('# my-component', component, [], mockReadmeOutput); + + // Pipe characters are escaped in markdown tables + expect(markdown).toContain('Defaults to var(--nano-color-blue-cerulean-1000); with \\| pipes'); + }); + }); +}); diff --git a/packages/core/src/compiler/docs/test/style-docs.spec.ts b/packages/core/src/compiler/docs/test/style-docs.spec.ts new file mode 100644 index 00000000000..fb2e3a02c90 --- /dev/null +++ b/packages/core/src/compiler/docs/test/style-docs.spec.ts @@ -0,0 +1,185 @@ +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 }]); + }); +}); diff --git a/packages/core/src/compiler/docs/test/tsconfig.json b/packages/core/src/compiler/docs/test/tsconfig.json new file mode 100644 index 00000000000..0ff0be83173 --- /dev/null +++ b/packages/core/src/compiler/docs/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../testing/tsconfig.internal.json" +} diff --git a/packages/core/src/compiler/docs/vscode/index.ts b/packages/core/src/compiler/docs/vscode/index.ts new file mode 100644 index 00000000000..41a1794110f --- /dev/null +++ b/packages/core/src/compiler/docs/vscode/index.ts @@ -0,0 +1,139 @@ +import { isOutputTargetDocsVscode, join } from '@utils'; + +import type * as d from '../../../declarations'; +import { getNameText } from '../generate-doc-data'; + +/** + * Generate [custom data](https://github.com/microsoft/vscode-custom-data) to augment existing HTML types in VS Code. + * This function writes the custom data as a JSON file to disk, which can be used in VS Code to inform the IDE about + * custom elements generated by Stencil. + * + * The JSON generated by this function must conform to the + * [HTML custom data schema](https://github.com/microsoft/vscode-html-languageservice/blob/e7ae8a7170df5e721a13cee1b86e293b24eb3b20/docs/customData.schema.json). + * + * This function generates custom data for HTML only at this time (it does not generate custom data for CSS). + * + * @param compilerCtx the current compiler context + * @param docsData an intermediate representation documentation derived from compiled Stencil components + * @param outputTargets the output target(s) the associated with the current build + */ +export const generateVscodeDocs = async ( + compilerCtx: d.CompilerCtx, + docsData: d.JsonDocs, + outputTargets: d.OutputTarget[], +): Promise => { + const vsCodeOutputTargets = outputTargets.filter(isOutputTargetDocsVscode); + if (vsCodeOutputTargets.length === 0) { + return; + } + + await Promise.all( + vsCodeOutputTargets.map(async (outputTarget: d.OutputTargetDocsVscode): Promise => { + const json = { + /** + * the 'version' top-level field is required by the schema. changes to the JSON generated by Stencil must: + * - comply with v1.X of the schema _OR_ + * - increment this field as a part of updating the JSON generation. This should be considered a breaking change + * + * {@link https://github.com/microsoft/vscode-html-languageservice/blob/e7ae8a7170df5e721a13cee1b86e293b24eb3b20/src/htmlLanguageTypes.ts#L184} + */ + version: 1.1, + tags: docsData.components.map((cmp: d.JsonDocsComponent) => ({ + name: cmp.tag, + description: { + kind: 'markdown', + value: cmp.docs, + }, + attributes: cmp.props + .filter((p: d.JsonDocsProp): p is DocPropWithAttribute => p.attr !== undefined && p.attr.length > 0) + .map(serializeAttribute), + references: getReferences(cmp, outputTarget.sourceCodeBaseUrl), + })), + }; + + // fields in the custom data may have a value of `undefined`. calling `stringify` will remove such fields. + const jsonContent = JSON.stringify(json, null, 2); + await compilerCtx.fs.writeFile(outputTarget.file, jsonContent); + }), + ); +}; + +/** + * This type describes external references for a custom element. + * + * An internal representation of Microsoft/VS Code's [`IReference` type](https://github.com/microsoft/vscode-html-languageservice/blob/e7ae8a7170df5e721a13cee1b86e293b24eb3b20/src/htmlLanguageTypes.ts#L153). + */ +type TagReference = { + name: string; + url: string; +}; + +/** + * Generate a 'references' section for a component's documentation. + * @param cmp the Stencil component to generate a references section for + * @param repoBaseUrl an optional URL, that when provided, will add a reference to the source code for the component + * @returns the generated references section, or undefined if no references could be generated + */ +const getReferences = (cmp: d.JsonDocsComponent, repoBaseUrl: string | undefined): TagReference[] | undefined => { + // collect any `@reference` JSDoc tags on the component + const references = getNameText('reference', cmp.docsTags).map(([name, url]) => ({ name, url })); + + if (repoBaseUrl) { + references.push({ + name: 'Source code', + url: join(repoBaseUrl, cmp.filePath ?? ''), + }); + } + if (references.length > 0) { + return references; + } + return undefined; +}; + +/** + * A type that describes the attributes that can be used with a custom element. + * + * An internal representation of Microsoft/VS Code's [`IAttributeData` type](https://github.com/microsoft/vscode-html-languageservice/blob/e7ae8a7170df5e721a13cee1b86e293b24eb3b20/src/htmlLanguageTypes.ts#L165). + */ +type AttributeData = { + name: string; + description: string; + values?: { name: string }[]; +}; + +/** + * Utility that provides a type-safe way of making a key K on a type T required. + * + * This is preferable than using an intersection of `T & {K: someType}` as it ensures that: + * - the type of K will always match the type T[K] + * - it should error should K not exist in `keyof T` + */ +type WithRequired = T & { [P in K]-?: T[P] }; + +/** + * A `@Prop` documentation type with a required 'attr' field + */ +type DocPropWithAttribute = WithRequired; + +/** + * Serialize a component's class member decorated with `@Prop` to be written to disk + * @param prop the intermediate representation of the documentation to serialize + * @returns the serialized data + */ +const serializeAttribute = (prop: DocPropWithAttribute): AttributeData => { + const attribute: AttributeData = { + name: prop.attr, + description: prop.docs, + }; + const values = prop.values + .filter( + (jsonDocValue: d.JsonDocsValue): jsonDocValue is Required => + jsonDocValue.type === 'string' && jsonDocValue.value !== undefined, + ) + .map((jsonDocValue: Required) => ({ name: jsonDocValue.value })); + + if (values.length > 0) { + attribute.values = values; + } + return attribute; +}; diff --git a/packages/core/src/compiler/entries/component-bundles.ts b/packages/core/src/compiler/entries/component-bundles.ts new file mode 100644 index 00000000000..cc39a63c845 --- /dev/null +++ b/packages/core/src/compiler/entries/component-bundles.ts @@ -0,0 +1,204 @@ +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; +} diff --git a/packages/core/src/compiler/entries/component-graph.ts b/packages/core/src/compiler/entries/component-graph.ts new file mode 100644 index 00000000000..41eee6df6c6 --- /dev/null +++ b/packages/core/src/compiler/entries/component-graph.ts @@ -0,0 +1,15 @@ +import type * as d from '../../declarations'; +import { getScopeId } from '../style/scope-css'; + +export const generateModuleGraph = (cmps: d.ComponentCompilerMeta[], bundleModules: ReadonlyArray) => { + const cmpMap = new Map(); + cmps.forEach((cmp) => { + const bundle = bundleModules.find((b) => b.cmps.includes(cmp)); + if (bundle) { + // add default case for no mode + cmpMap.set(getScopeId(cmp.tagName), bundle.rollupResult.imports); + } + }); + + return cmpMap; +}; diff --git a/packages/core/src/compiler/entries/default-bundles.ts b/packages/core/src/compiler/entries/default-bundles.ts new file mode 100644 index 00000000000..f9b88322268 --- /dev/null +++ b/packages/core/src/compiler/entries/default-bundles.ts @@ -0,0 +1,88 @@ +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; +} diff --git a/packages/core/src/compiler/entries/resolve-component-dependencies.ts b/packages/core/src/compiler/entries/resolve-component-dependencies.ts new file mode 100644 index 00000000000..a054fa29143 --- /dev/null +++ b/packages/core/src/compiler/entries/resolve-component-dependencies.ts @@ -0,0 +1,126 @@ +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(); +} diff --git a/packages/core/src/compiler/events.ts b/packages/core/src/compiler/events.ts new file mode 100644 index 00000000000..7cdb280d035 --- /dev/null +++ b/packages/core/src/compiler/events.ts @@ -0,0 +1,73 @@ +import type * as d from '../declarations'; + +export const buildEvents = (): d.BuildEvents => { + const evCallbacks: EventCallback[] = []; + + const off = (callback: any) => { + const index = evCallbacks.findIndex((ev) => ev.callback === callback); + if (index > -1) { + evCallbacks.splice(index, 1); + return true; + } + return false; + }; + + const on = (arg0: any, arg1?: any): d.BuildOnEventRemove => { + if (typeof arg0 === 'function') { + const eventName: null = null; + const callback = arg0; + evCallbacks.push({ + eventName, + callback, + }); + return () => off(callback); + } else if (typeof arg0 === 'string' && typeof arg1 === 'function') { + const eventName = arg0.toLowerCase().trim(); + const callback = arg1; + + evCallbacks.push({ + eventName, + callback, + }); + + return () => off(callback); + } + return () => false; + }; + + const emit = (eventName: d.CompilerEventName, data: any) => { + const normalizedEventName = eventName.toLowerCase().trim(); + const callbacks = evCallbacks.slice(); + + for (const ev of callbacks) { + if (ev.eventName == null) { + try { + ev.callback(eventName, data); + } catch (e) { + console.error(e); + } + } else if (ev.eventName === normalizedEventName) { + try { + ev.callback(data); + } catch (e) { + console.error(e); + } + } + } + }; + + const unsubscribeAll = () => { + evCallbacks.length = 0; + }; + + return { + emit, + on, + unsubscribeAll, + }; +}; + +interface EventCallback { + eventName: string | null; + callback: Function; +} diff --git a/packages/core/src/compiler/fs-watch/fs-watch-rebuild.ts b/packages/core/src/compiler/fs-watch/fs-watch-rebuild.ts new file mode 100644 index 00000000000..d355d68919b --- /dev/null +++ b/packages/core/src/compiler/fs-watch/fs-watch-rebuild.ts @@ -0,0 +1,165 @@ +import { isOutputTargetDocsJson, isOutputTargetDocsVscode, isOutputTargetStats, isString, unique } from '@utils'; +import { basename } from 'path'; + +import type * as d from '../../declarations'; + +export const filesChanged = (buildCtx: d.BuildCtx) => { + // files changed include updated, added and deleted + return unique([...buildCtx.filesUpdated, ...buildCtx.filesAdded, ...buildCtx.filesDeleted]).sort(); +}; + +/** + * Unary helper function mapping string to string and wrapping `basename`, + * which normally takes two string arguments. This means it cannot be passed + * to `Array.prototype.map`, but this little helper can! + * + * @param filePath a filepath to check out + * @returns the basename for that filepath + */ +const unaryBasename = (filePath: string): string => basename(filePath); + +/** + * Get the file extension for a path + * + * @param filePath a path + * @returns the file extension (well, characters after the last `'.'`) or + * `null` if no extension exists. + */ +const getExt = (filePath: string): string | null => { + const fileParts = filePath.split('.'); + + return fileParts.length > 1 ? fileParts.pop()!.toLowerCase() : null; +}; + +/** + * Script extensions which we want to be able to recognize + */ +const SCRIPT_EXT = ['ts', 'tsx', 'js', 'jsx']; + +/** + * Helper to check if a filepath has a script extension + * + * @param filePath a file extension + * @returns whether the filepath has a script extension or not + */ +export const hasScriptExt = (filePath: string): boolean => { + const ext = getExt(filePath); + + return ext ? SCRIPT_EXT.includes(ext) : false; +}; + +const STYLE_EXT = ['css', 'scss', 'sass', 'pcss', 'styl', 'stylus', 'less']; + +/** + * Helper to check if a filepath has a style extension + * + * @param filePath a file extension to check + * @returns whether the filepath has a style extension or not + */ +export const hasStyleExt = (filePath: string): boolean => { + const ext = getExt(filePath); + + return ext ? STYLE_EXT.includes(ext) : false; +}; + +/** + * Get all scripts from a build context that were added + * + * @param buildCtx the build context + * @returns an array of filepaths that were added + */ +export const scriptsAdded = (buildCtx: d.BuildCtx): string[] => + buildCtx.filesAdded.filter(hasScriptExt).map(unaryBasename); + +/** + * Get all scripts from a build context that were deleted + * + * @param buildCtx the build context + * @returns an array of deleted filepaths + */ +export const scriptsDeleted = (buildCtx: d.BuildCtx): string[] => + buildCtx.filesDeleted.filter(hasScriptExt).map(unaryBasename); + +/** + * Check whether a build has script changes + * + * @param buildCtx the build context + * @returns whether or not there are script changes + */ +export const hasScriptChanges = (buildCtx: d.BuildCtx): boolean => buildCtx.filesChanged.some(hasScriptExt); + +/** + * Check whether a build has style changes + * + * @param buildCtx the build context + * @returns whether or not there are style changes + */ +export const hasStyleChanges = (buildCtx: d.BuildCtx): boolean => buildCtx.filesChanged.some(hasStyleExt); + +/** + * Check whether a build has html changes + * + * @param config the current config + * @param buildCtx the build context + * @returns whether or not HTML files were changed + */ +export const hasHtmlChanges = (config: d.ValidatedConfig, buildCtx: d.BuildCtx): boolean => { + const anyHtmlChanged = buildCtx.filesChanged.some((f) => f.toLowerCase().endsWith('.html')); + + if (anyHtmlChanged) { + // any *.html in any directory that changes counts and rebuilds + return true; + } + + const srcIndexHtmlChanged = buildCtx.filesChanged.some((fileChanged) => { + // the src index index.html file has changed + // this file name could be something other than index.html + return fileChanged === config.srcIndexHtml; + }); + + return srcIndexHtmlChanged; +}; + +export const updateCacheFromRebuild = (compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { + buildCtx.filesChanged.forEach((filePath) => { + compilerCtx.fs.clearFileCache(filePath); + }); + + buildCtx.dirsAdded.forEach((dirAdded) => { + compilerCtx.fs.clearDirCache(dirAdded); + }); + + buildCtx.dirsDeleted.forEach((dirDeleted) => { + compilerCtx.fs.clearDirCache(dirDeleted); + }); +}; + +/** + * Checks if a path is ignored by the watch configuration + * + * @param config The validated config for the Stencil project + * @param path The path to check + * @returns Whether the path is ignored by the watch configuration + */ +export const isWatchIgnorePath = (config: d.ValidatedConfig, path: string) => { + if (!isString(path)) { + return false; + } + + const isWatchIgnore = (config.watchIgnoredRegex as RegExp[]).some((reg) => reg.test(path)); + if (isWatchIgnore) { + return true; + } + const outputTargets = config.outputTargets; + const ignoreFiles = [ + ...outputTargets.filter(isOutputTargetDocsJson).map((o) => o.file), + ...outputTargets.filter(isOutputTargetDocsJson).map((o) => o.typesFile), + ...outputTargets.filter(isOutputTargetStats).map((o) => o.file), + ...outputTargets.filter(isOutputTargetDocsVscode).map((o) => o.file), + ]; + if (ignoreFiles.includes(path)) { + return true; + } + + return false; +}; diff --git a/packages/core/src/compiler/html/add-script-attr.ts b/packages/core/src/compiler/html/add-script-attr.ts new file mode 100644 index 00000000000..007056bf382 --- /dev/null +++ b/packages/core/src/compiler/html/add-script-attr.ts @@ -0,0 +1,23 @@ +import { join } from '@utils'; + +import type * as d from '../../declarations'; +import { getAbsoluteBuildDir } from './html-utils'; + +export const addScriptDataAttribute = (config: d.ValidatedConfig, doc: Document, outputTarget: d.OutputTargetWww) => { + const resourcesUrl = getAbsoluteBuildDir(outputTarget); + const entryEsmFilename = `${config.fsNamespace}.esm.js`; + const entryNoModuleFilename = `${config.fsNamespace}.js`; + const expectedEsmSrc = join(resourcesUrl, entryEsmFilename); + const expectedNoModuleSrc = join(resourcesUrl, entryNoModuleFilename); + + const scripts = Array.from(doc.querySelectorAll('script')); + const scriptEsm = scripts.find((s) => s.getAttribute('src') === expectedEsmSrc); + const scriptNomodule = scripts.find((s) => s.getAttribute('src') === expectedNoModuleSrc); + + if (scriptEsm) { + scriptEsm.setAttribute('data-stencil', ''); + } + if (scriptNomodule) { + scriptNomodule.setAttribute('data-stencil', ''); + } +}; diff --git a/packages/core/src/compiler/html/canonical-link.ts b/packages/core/src/compiler/html/canonical-link.ts new file mode 100644 index 00000000000..0b9df789cca --- /dev/null +++ b/packages/core/src/compiler/html/canonical-link.ts @@ -0,0 +1,27 @@ +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); + } + } + } +}; diff --git a/packages/core/src/compiler/html/html-utils.ts b/packages/core/src/compiler/html/html-utils.ts new file mode 100644 index 00000000000..9953030f6cd --- /dev/null +++ b/packages/core/src/compiler/html/html-utils.ts @@ -0,0 +1,15 @@ +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) + '/'; +}; diff --git a/packages/core/src/compiler/html/inject-module-preloads.ts b/packages/core/src/compiler/html/inject-module-preloads.ts new file mode 100644 index 00000000000..9d3d6e4eada --- /dev/null +++ b/packages/core/src/compiler/html/inject-module-preloads.ts @@ -0,0 +1,37 @@ +import { join } from '@utils'; + +import type * as d from '../../declarations'; +import { getAbsoluteBuildDir } from './html-utils'; + +export const optimizeCriticalPath = (doc: Document, criticalBundlers: string[], outputTarget: d.OutputTargetWww) => { + const buildDir = getAbsoluteBuildDir(outputTarget); + const paths = criticalBundlers.map((path) => join(buildDir, path)); + injectModulePreloads(doc, paths); +}; + +export const injectModulePreloads = (doc: Document, paths: string[]) => { + const existingLinks = (Array.from(doc.querySelectorAll('link[rel=modulepreload]')) as HTMLLinkElement[]).map((link) => + link.getAttribute('href'), + ); + + const addLinks = paths.filter((path) => !existingLinks.includes(path)).map((path) => createModulePreload(doc, path)); + + const head = doc.head; + const firstScript = head.querySelector('script'); + if (firstScript) { + for (const link of addLinks) { + head.insertBefore(link, firstScript); + } + } else { + for (const link of addLinks) { + head.appendChild(link); + } + } +}; + +const createModulePreload = (doc: Document, href: string) => { + const link = doc.createElement('link'); + link.setAttribute('rel', 'modulepreload'); + link.setAttribute('href', href); + return link; +}; diff --git a/packages/core/src/compiler/html/inject-sw-script.ts b/packages/core/src/compiler/html/inject-sw-script.ts new file mode 100644 index 00000000000..1f892d8027f --- /dev/null +++ b/packages/core/src/compiler/html/inject-sw-script.ts @@ -0,0 +1,39 @@ +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; +}; diff --git a/packages/core/src/compiler/html/inline-esm-import.ts b/packages/core/src/compiler/html/inline-esm-import.ts new file mode 100644 index 00000000000..3f417164790 --- /dev/null +++ b/packages/core/src/compiler/html/inline-esm-import.ts @@ -0,0 +1,161 @@ +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 ` + * ``` + */ + 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); + } +}; diff --git a/packages/core/src/runtime/mixin.ts b/packages/core/src/runtime/mixin.ts new file mode 100644 index 00000000000..01d40987c58 --- /dev/null +++ b/packages/core/src/runtime/mixin.ts @@ -0,0 +1,9 @@ +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); +} diff --git a/packages/core/src/runtime/mode.ts b/packages/core/src/runtime/mode.ts new file mode 100644 index 00000000000..489e7d113d9 --- /dev/null +++ b/packages/core/src/runtime/mode.ts @@ -0,0 +1,10 @@ +import { getHostRef, modeResolutionChain } from '@platform'; + +import type * as d from '../declarations'; + +// Private +export const computeMode = (elm: d.HostElement) => modeResolutionChain.map((h) => h(elm)).find((m) => !!m); + +// Public +export const setMode = (handler: d.ResolutionHandler) => modeResolutionChain.push(handler); +export const getMode = (ref: d.RuntimeRef) => getHostRef(ref)?.$modeName$; diff --git a/packages/core/src/runtime/nonce.ts b/packages/core/src/runtime/nonce.ts new file mode 100644 index 00000000000..752a9a85e60 --- /dev/null +++ b/packages/core/src/runtime/nonce.ts @@ -0,0 +1,9 @@ +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); diff --git a/packages/core/src/runtime/parse-property-value.ts b/packages/core/src/runtime/parse-property-value.ts new file mode 100644 index 00000000000..0ba0ed65d97 --- /dev/null +++ b/packages/core/src/runtime/parse-property-value.ts @@ -0,0 +1,85 @@ +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; +}; diff --git a/packages/core/src/runtime/platform-options.ts b/packages/core/src/runtime/platform-options.ts new file mode 100644 index 00000000000..42e550464cd --- /dev/null +++ b/packages/core/src/runtime/platform-options.ts @@ -0,0 +1,20 @@ +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); diff --git a/packages/core/src/runtime/profile.ts b/packages/core/src/runtime/profile.ts new file mode 100644 index 00000000000..9b0058edcde --- /dev/null +++ b/packages/core/src/runtime/profile.ts @@ -0,0 +1,98 @@ +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; + }; + } +}; diff --git a/packages/core/src/runtime/proxy-component.ts b/packages/core/src/runtime/proxy-component.ts new file mode 100644 index 00000000000..2b92e92d8ec --- /dev/null +++ b/packages/core/src/runtime/proxy-component.ts @@ -0,0 +1,420 @@ +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 { 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 } = Object.getOwnPropertyDescriptor(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; +}; diff --git a/packages/core/src/runtime/readme.md b/packages/core/src/runtime/readme.md new file mode 100644 index 00000000000..c0003c0027b --- /dev/null +++ b/packages/core/src/runtime/readme.md @@ -0,0 +1,126 @@ +## Lifecycle Order Of Operations + +Component lifecycle events fire `componentWillLoad` from top to bottom, then fire `componentDidLoad` from bottom to top. It should take into account each component can finish lazy-loaded requests in any random order. Additionally, any `componentWillLoad` can return a promise that all child components should wait on until it's resolved, while still keeping the correct firing order. + +``` + + + + + + +cmp-a - componentWillLoad +cmp-b - componentWillLoad +cmp-c - componentWillLoad +cmp-c - componentDidLoad +cmp-b - componentDidLoad +cmp-a - componentDidLoad +``` + + +## Hydrated CSS Visibility + +By default, components are assigned `visibility: hidden` using their tag name as the css selector. Therefore, before the components and their descendants have finished hydrating, each component is hidden by default. This is done to prevent janky flickering as components hydrate asynchronously. As each component fully loads the `hydrated` css class is then added. + +The `hydrated` css class that's added to the component assigns `visibility: inherit` style to the element. If any parent component is still hydrating then this component will not show until the top most component has added the `hydrated` css class. + + + +## Lifecycle Process + +- **Connect**: Synchronously within `connectedCallback`, each component looks for an ancestor component and adds itself as a child component if an ancestor is found. + + - Climb up the parent elements with a while loop. + + - Stop at the first element that has an `s-init` function. + + - If the ancestor component we found hasn't ran its lifecycle update yet, then add this component to the ancestor's `s-al` set. The `s-al` is a set of child components that are actively loading. + + - If no ancestor component is found then continue without the component setting an ancestor component. + + +- **Initialize Component**: Initialize the component for the first time within `initializeComponent`. + + - If the component has already initialized loading then do nothing. Data to know if the component has started to initialize is in the host ref data, which ensures it doesn't try to initialize more than once. + + - Async request the lazy-loaded component constructor and await the response. + + - After the component implementation constructor request has been received, create a new instance of the component with the lazy-loaded constructor. + + - The constructor will directly wire the host element and lazy-loaded component instance together with the host ref data. + + - If the component has an ancestor component, but the ancestor hasn't ran its lifecycle update yet, then this component should not be initialized at this moment and shouldn't fire its `componentWillLoad` yet. Instead, this component should be added to the ancestor component's array of render callbacks `s-rc`, which would call `initializeComponent` again after its ready. Once the ancestor component has ran its lifecycle update, it'll then call all of its child render callbacks so that the `componentWillLoad` lifecycle events are in the correct order. + + - If there is no ancestor component, or the ancestor component has already rendered, then fire off the first update. + + - When ready, `updateComponent` will be added as an async write task and ran asynchronously. + + +- **First Update**: The first component update and render from within `updateComponent`. + + - Set the lifecycle ready value `s-lr` to `false` signifying that the lifecycle update is not ready for this component. + + - Fire off `componentWillLoad` lifecycle. + + - Fire off `componentWillRender` lifecycle. + + - Add scoped css data and classes for scoped encapsulation or shadow dom encapsulation without shadow dom browser support. + + - Attach shadow root for shadow dom components. + + - Attach styles to shadow root or document depending on encapsulation. + + - First render. + + - Set the lifecycle ready value `s-lr` to `true` signifying that the lifecycle update has happened and the component is now ready for child component lifecycles. + + - Fire off all of this component's child render callbacks within `s-rc`. Each of the child render callbacks will fire off their own initialize component process. + + - All component descendants should fire `componentWillLoad` lifecycle in the correct order, top to bottom. + + - Fire `postUpdateComponent`. The bottom most component will not have any child render callbacks, so at this point the `componentDidLoad` lifecycle events should start firing from bottom to top. + + - Fire off `componentDidLoad` lifecycle. + + - Fire off `componentDidRender` lifecycle. + + - Add `hydrated` css class signifying the component has finished loading. At this point this component has finished updating. + + - If the component has an ancestor component, then remove this component from its set of actively loading children in `s-al`. + + - After removing this component from the ancestor component's `s-al` set, if the set is now empty then fire the ancestor component's `s-init`. + + - Firing `s-init` on the ancestor component allows the ancestor to complete its first update and fire its own `componentDidLoad` lifecycle event, allowing for `componentDidLoad` lifecycles to fire bottom to top. + + - Fire all `componentOnReady` resolves. + + +- **Subsequent Updates**: All subsequent component updates and re-renders from within `updateComponent`. + + - Somehow `setValue` is triggered, either through a `Prop` or `State` update, or calling `forceUpdate()` on a component. If there is a change or a forced update, then `setValue` will add `updateComponent` to an async write task. + + - Fire `updateComponent` from async task queue. + + - Fire off `componentWillUpdate` lifecycle. + + - Fire off `componentWillRender` lifecycle. + + - Patch render. + + - Fire `postUpdateComponent`. + + - Fire off `componentDidUpdate` lifecycle. + + - Fire off `componentDidRender` lifecycle. + + + +## Property Descriptions + +`s-al`: A component's `Set` of child components that are actively loading. + +`s-init`: A function to be called by child components to finish initializing the component. + +`s-lr`: The component's lifecycle ready status. `true` if the component has finished its lifecycle update, falsy if it is actively updating and has not fired off either `componentWillLoad` or `componentWillUpdate`. + +`s-rc`: A component's array of child component render callbacks. After a component renders, it should then fire off all of its child component render callbacks. \ No newline at end of file diff --git a/packages/core/src/runtime/render.ts b/packages/core/src/runtime/render.ts new file mode 100644 index 00000000000..616c4e7f0df --- /dev/null +++ b/packages/core/src/runtime/render.ts @@ -0,0 +1,35 @@ +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); +} diff --git a/packages/core/src/runtime/runtime-constants.ts b/packages/core/src/runtime/runtime-constants.ts new file mode 100644 index 00000000000..8af3d699237 --- /dev/null +++ b/packages/core/src/runtime/runtime-constants.ts @@ -0,0 +1,86 @@ +/** + * Bit flags for recording various properties of VDom nodes + */ +export const enum VNODE_FLAGS { + /** + * Whether or not a vdom node is a slot reference + */ + isSlotReference = 1 << 0, + + /** + * Whether or not a slot element has fallback content + */ + isSlotFallback = 1 << 1, + + /** + * Whether or not an element is a host element + */ + isHost = 1 << 2, +} + +export const enum PROXY_FLAGS { + isElementConstructor = 1 << 0, + proxyState = 1 << 1, +} + +export const enum PLATFORM_FLAGS { + /** + * designates a node in the DOM as being actively moved by the runtime + */ + isTmpDisconnected = 1 << 0, + appLoaded = 1 << 1, + queueSync = 1 << 2, + + queueMask = appLoaded | queueSync, +} + +/** + * A (subset) of node types which are relevant for the Stencil runtime. These + * values are based on the values which can possibly be returned by the + * `.nodeType` property of a DOM node. See here for details: + * + * {@link https://dom.spec.whatwg.org/#ref-for-dom-node-nodetype%E2%91%A0} + */ +export const enum NODE_TYPE { + ElementNode = 1, + TextNode = 3, + CommentNode = 8, + DocumentNode = 9, + DocumentTypeNode = 10, + DocumentFragment = 11, +} + +export const CONTENT_REF_ID = 'r'; +export const ORG_LOCATION_ID = 'o'; +export const SLOT_NODE_ID = 's'; +export const TEXT_NODE_ID = 't'; +export const COMMENT_NODE_ID = 'c'; + +export const HYDRATE_ID = 's-id'; +export const HYDRATED_STYLE_ID = 'sty-id'; +export const HYDRATE_CHILD_ID = 'c-id'; +export const HYDRATED_CSS = '{visibility:hidden}.hydrated{visibility:inherit}'; + +export const STENCIL_DOC_DATA = '_stencilDocData'; +export const DEFAULT_DOC_DATA = { + hostIds: 0, + rootLevelIds: 0, + staticComponents: new Set(), +}; + +/** + * Constant for styles to be globally applied to `slot-fb` elements for pseudo-slot behavior. + * + * Two cascading rules must be used instead of a `:not()` selector due to Stencil browser + * support as of Stencil v4. + */ +export const SLOT_FB_CSS = 'slot-fb{display:contents}slot-fb[hidden]{display:none}'; + +export const XLINK_NS = 'http://www.w3.org/1999/xlink'; + +export const FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS = [ + 'formAssociatedCallback', + 'formResetCallback', + 'formDisabledCallback', + 'formStateRestoreCallback', +] as const; diff --git a/packages/core/src/runtime/set-value.ts b/packages/core/src/runtime/set-value.ts new file mode 100644 index 00000000000..b33c536d7a6 --- /dev/null +++ b/packages/core/src/runtime/set-value.ts @@ -0,0 +1,162 @@ +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); + } + } + } +}; diff --git a/packages/core/src/runtime/slot-polyfill-utils.ts b/packages/core/src/runtime/slot-polyfill-utils.ts new file mode 100644 index 00000000000..76f2d793443 --- /dev/null +++ b/packages/core/src/runtime/slot-polyfill-utils.ts @@ -0,0 +1,266 @@ +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 }; +} diff --git a/packages/core/src/runtime/styles.ts b/packages/core/src/runtime/styles.ts new file mode 100644 index 00000000000..69ec1778d97 --- /dev/null +++ b/packages/core/src/runtime/styles.ts @@ -0,0 +1,324 @@ +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) + 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. + */ + const existingStyleContainer: HTMLStyleElement = styleContainerNode.querySelector('style'); + if (existingStyleContainer) { + existingStyleContainer.textContent = style + existingStyleContainer.textContent; + } else { + (styleContainerNode as HTMLElement).prepend(styleElm); + } + } + } else { + styleContainerNode.append(styleElm); + } + } + + /** + * attach styles at the beginning of a shadow root node if we render shadow components + */ + if (cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) { + styleContainerNode.insertBefore(styleElm, null); + } + + // Add styles for `slot-fb` elements if we're using slots outside the Shadow DOM + if (cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation) { + styleElm.textContent += SLOT_FB_CSS; + } + + if (appliedStyles) { + appliedStyles.add(scopeId); + } + } + } else if (BUILD.constructableCSS) { + let appliedStyles = rootAppliedStyles.get(styleContainerNode); + if (!appliedStyles) { + rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Set())); + } + if (!appliedStyles.has(scopeId)) { + /** + * Constructable stylesheets can't be shared between windows, + * we need to create a new one for the current window if necessary + */ + const currentWindow = styleContainerNode.defaultView ?? styleContainerNode.ownerDocument.defaultView; + let stylesheet: CSSStyleSheet; + if (style.constructor === currentWindow.CSSStyleSheet) { + stylesheet = style; + } else { + stylesheet = new currentWindow.CSSStyleSheet(); + for (let i = 0; i < style.cssRules.length; i++) { + stylesheet.insertRule(style.cssRules[i].cssText, i); + } + } + /** + * > If the array needs to be modified, use in-place mutations like push(). + * https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets + */ + if (supportsMutableAdoptedStyleSheets) { + styleContainerNode.adoptedStyleSheets.push(stylesheet); + } else { + styleContainerNode.adoptedStyleSheets = [...styleContainerNode.adoptedStyleSheets, stylesheet]; + } + + appliedStyles.add(scopeId); + + // Remove SSR style element from shadow root now that adoptedStyleSheets is in use + // Only remove from shadow roots, not from document head (for scoped components) + if (BUILD.hydrateClientSide && 'host' in styleContainerNode) { + const ssrStyleElm = styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`); + if (ssrStyleElm) { + writeTask(() => ssrStyleElm.remove()); + } + } + } + } + } + return scopeId; +}; + +/** + * Add styles for a given component to the DOM, optionally handling 'scoped' + * encapsulation by adding an appropriate class name to the host element. + * + * @param hostRef the host reference for the component of interest + */ +export const attachStyles = (hostRef: d.HostRef) => { + const cmpMeta = hostRef.$cmpMeta$; + const elm = hostRef.$hostElement$; + const flags = cmpMeta.$flags$; + const endAttachStyles = createTime('attachStyles', cmpMeta.$tagName$); + const scopeId = addStyle( + BUILD.shadowDom && supportsShadow && elm.shadowRoot ? elm.shadowRoot : (elm.getRootNode() as ShadowRoot), + cmpMeta, + hostRef.$modeName$, + ); + + if ((BUILD.shadowDom || BUILD.scoped) && BUILD.cssAnnotations && flags & CMP_FLAGS.needsScopedEncapsulation) { + // only required when we're NOT using native shadow dom (slot) + // or this browser doesn't support native shadow dom + // and this host element was NOT created with SSR + // let's pick out the inner content for slot projection + // create a node to represent where the original + // content was first placed, which is useful later on + // DOM WRITE!! + elm['s-sc'] = scopeId; + elm.classList.add(scopeId + '-h'); + } + endAttachStyles(); +}; + +/** + * Get the scope ID for a given component + * + * @param cmp runtime metadata for the component of interest + * @param mode the current mode (optional) + * @returns a scope ID for the component of interest + */ +export const getScopeId = (cmp: d.ComponentRuntimeMeta, mode?: string) => + 'sc-' + (BUILD.mode && mode && cmp.$flags$ & CMP_FLAGS.hasMode ? cmp.$tagName$ + '-' + mode : cmp.$tagName$); + +/** + * Convert a 'scoped' CSS string to one appropriate for use in the shadow DOM. + * + * Given a 'scoped' CSS string that looks like this: + * + * ``` + * /*!@div*\/div.class-name { display: flex }; + * ``` + * + * Convert it to a 'shadow' appropriate string, like so: + * + * ``` + * /*!@div*\/div.class-name { display: flex } + * ─┬─ ────────┬──────── + * │ │ + * │ ┌─────────────────┘ + * ▼ ▼ + * div{ display: flex } + * ``` + * + * Note that forward-slashes in the above are escaped so they don't end the + * comment. + * + * @param css a CSS string to convert + * @returns the converted string + */ +export const convertScopedToShadow = (css: string) => css.replace(/\/\*!@([^\/]+)\*\/[^\{]+\{/g, '$1{'); + +/** + * Hydrate styles after SSR for components *not* using DSD. Convert 'scoped' styles to 'shadow' + * and add them to a constructable stylesheet. + */ +export const hydrateScopedToShadow = () => { + if (!win.document) { + return; + } + + const styles = win.document.querySelectorAll(`[${HYDRATED_STYLE_ID}]`); + let i = 0; + for (; i < styles.length; i++) { + registerStyle(styles[i].getAttribute(HYDRATED_STYLE_ID), convertScopedToShadow(styles[i].innerHTML), true); + } +}; + +declare global { + export interface CSSStyleSheet { + replaceSync(cssText: string): void; + replace(cssText: string): Promise; + } +} diff --git a/packages/core/src/runtime/tag-transform.ts b/packages/core/src/runtime/tag-transform.ts new file mode 100644 index 00000000000..09193901be8 --- /dev/null +++ b/packages/core/src/runtime/tag-transform.ts @@ -0,0 +1,27 @@ +import type * as d from '../declarations'; + +export let tagTransformer: d.TagTransformer | undefined = undefined; + +/** + * Transforms a tag name using the current tag transformer + * @param tag - the tag to transform e.g. `my-tag` + * @returns the transformed tag e.g. `new-my-tag` + */ +export function transformTag(tag: T): T { + if (!tagTransformer) return tag; + return tagTransformer(tag) as T; +} + +/** + * Sets the tag transformer to be used when rendering custom elements + * @param transformer the transformer function to use. Must return a string + */ +export function setTagTransformer(transformer: d.TagTransformer) { + if (tagTransformer) { + console.warn(` + A tagTransformer has already been set. + Overwriting it may lead to error and unexpected results if your components have already been defined. + `); + } + tagTransformer = transformer; +} diff --git a/packages/core/src/runtime/test/assets.spec.tsx b/packages/core/src/runtime/test/assets.spec.tsx new file mode 100644 index 00000000000..0e9afe68a20 --- /dev/null +++ b/packages/core/src/runtime/test/assets.spec.tsx @@ -0,0 +1,24 @@ +import { getAssetPath } from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; + +import { CmpAsset } from './fixtures/cmp-asset'; + +describe('assets', () => { + it('should load asset data', async () => { + const page = await newSpecPage({ + components: [CmpAsset], + html: ``, + }); + + expect(page.root).toEqualHtml(` + + + + + `); + }); + + it('getAssetPath is defined', async () => { + expect(getAssetPath).toBeDefined(); + }); +}); diff --git a/packages/core/src/runtime/test/attr-deserialize.spec.tsx b/packages/core/src/runtime/test/attr-deserialize.spec.tsx new file mode 100644 index 00000000000..e779023bd52 --- /dev/null +++ b/packages/core/src/runtime/test/attr-deserialize.spec.tsx @@ -0,0 +1,207 @@ +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); + }); +}); diff --git a/packages/core/src/runtime/test/attr-prop-prefix.spec.tsx b/packages/core/src/runtime/test/attr-prop-prefix.spec.tsx new file mode 100644 index 00000000000..22bc335afac --- /dev/null +++ b/packages/core/src/runtime/test/attr-prop-prefix.spec.tsx @@ -0,0 +1,409 @@ +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 }); + }); + }); +}); diff --git a/packages/core/src/runtime/test/attr.spec.tsx b/packages/core/src/runtime/test/attr.spec.tsx new file mode 100644 index 00000000000..58ca4276608 --- /dev/null +++ b/packages/core/src/runtime/test/attr.spec.tsx @@ -0,0 +1,435 @@ +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(` + + +
+ +
+
+ `); + }); + }); +}); diff --git a/packages/core/src/runtime/test/before-each.spec.tsx b/packages/core/src/runtime/test/before-each.spec.tsx new file mode 100644 index 00000000000..a1094348556 --- /dev/null +++ b/packages/core/src/runtime/test/before-each.spec.tsx @@ -0,0 +1,85 @@ +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`); + }); +}); diff --git a/packages/core/src/runtime/test/bootstrap-lazy.spec.tsx b/packages/core/src/runtime/test/bootstrap-lazy.spec.tsx new file mode 100644 index 00000000000..bbeb09a6890 --- /dev/null +++ b/packages/core/src/runtime/test/bootstrap-lazy.spec.tsx @@ -0,0 +1,63 @@ +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, + ); + }); +}); diff --git a/packages/core/src/runtime/test/client-hydrate-to-vdom.spec.tsx b/packages/core/src/runtime/test/client-hydrate-to-vdom.spec.tsx new file mode 100644 index 00000000000..1ac4f7039ae --- /dev/null +++ b/packages/core/src/runtime/test/client-hydrate-to-vdom.spec.tsx @@ -0,0 +1,86 @@ +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'); + }); +}); diff --git a/packages/core/src/runtime/test/component-class.spec.tsx b/packages/core/src/runtime/test/component-class.spec.tsx new file mode 100644 index 00000000000..007539ac07d --- /dev/null +++ b/packages/core/src/runtime/test/component-class.spec.tsx @@ -0,0 +1,29 @@ +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'); + }); +}); diff --git a/packages/core/src/runtime/test/component-error-handling.spec.tsx b/packages/core/src/runtime/test/component-error-handling.spec.tsx new file mode 100644 index 00000000000..58200e1b557 --- /dev/null +++ b/packages/core/src/runtime/test/component-error-handling.spec.tsx @@ -0,0 +1,86 @@ +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')); + }); + }); +}); diff --git a/packages/core/src/runtime/test/dom-extras.spec.tsx b/packages/core/src/runtime/test/dom-extras.spec.tsx new file mode 100644 index 00000000000..9422201d8e7 --- /dev/null +++ b/packages/core/src/runtime/test/dom-extras.spec.tsx @@ -0,0 +1,143 @@ +import { Component, h, Host } from '@stencil/core'; +import { newSpecPage, SpecPage } from '@stencil/core/testing'; + +import { patchPseudoShadowDom, patchSlottedNode } from '../../runtime/dom-extras'; + +describe('dom-extras - patches for non-shadow dom methods and accessors', () => { + let specPage: SpecPage; + + const nodeOrEleContent = (node: Node | Element) => { + return (node as Element)?.outerHTML || node?.nodeValue?.trim(); + }; + + beforeEach(async () => { + @Component({ + tag: 'cmp-a', + scoped: true, + }) + class CmpA { + render() { + return ( + + 'Shadow' first text node +
+
+ Second slot fallback text +
+
+ Default slot fallback text +
+
+ 'Shadow' last text node +
+ ); + } + } + + specPage = await newSpecPage({ + components: [CmpA], + html: ` + + Some default slot, slotted text + a default slot, slotted element +
+ a second slot, slotted element + nested element in the second slot +
+ `, + hydrateClientSide: true, + }); + + patchPseudoShadowDom(specPage.root); + }); + + it('patches `childNodes` to return only nodes that have been slotted', async () => { + const childNodes = specPage.root.childNodes; + + expect(nodeOrEleContent(childNodes[0])).toBe(`Some default slot, slotted text`); + expect(nodeOrEleContent(childNodes[1])).toBe(`a default slot, slotted element`); + expect(nodeOrEleContent(childNodes[2])).toBe(``); + expect(nodeOrEleContent(childNodes[3])).toBe( + `
a second slot, slotted element nested element in the second slot
`, + ); + + const innerChildNodes = specPage.root.__childNodes; + + expect(nodeOrEleContent(innerChildNodes[0])).toBe(``); + expect(nodeOrEleContent(innerChildNodes[1])).toBe(``); + expect(nodeOrEleContent(innerChildNodes[2])).toBe(``); + expect(nodeOrEleContent(innerChildNodes[3])).toBe(``); + expect(nodeOrEleContent(innerChildNodes[4])).toBe(``); + expect(nodeOrEleContent(innerChildNodes[5])).toBe(`'Shadow' first text node`); + }); + + it('patches `children` to return only elements that have been slotted', async () => { + const children = specPage.root.children; + + expect(nodeOrEleContent(children[0])).toBe(`a default slot, slotted element`); + expect(nodeOrEleContent(children[1])).toBe( + `
a second slot, slotted element nested element in the second slot
`, + ); + expect(nodeOrEleContent(children[2])).toBe(undefined); + }); + + it('patches `childElementCount` to only count elements that have been slotted', async () => { + expect(specPage.root.childElementCount).toBe(2); + }); + + it('patches `textContent` to only return slotted node text', async () => { + expect(specPage.root.textContent.replace(/\s+/g, ' ').trim()).toBe( + `Some default slot, slotted text a default slot, slotted element a second slot, slotted element nested element in the second slot`, + ); + }); + + it('firstChild', async () => { + expect(nodeOrEleContent(specPage.root.firstChild)).toBe(`Some default slot, slotted text`); + }); + + it('lastChild', async () => { + expect(nodeOrEleContent(specPage.root.lastChild)).toBe( + `
a second slot, slotted element nested element in the second slot
`, + ); + }); + + it('patches nextSibling / previousSibling accessors of slotted nodes', async () => { + specPage.root.childNodes.forEach((node: Node) => patchSlottedNode(node)); + expect(nodeOrEleContent(specPage.root.firstChild)).toBe('Some default slot, slotted text'); + expect(nodeOrEleContent(specPage.root.firstChild.nextSibling)).toBe('a default slot, slotted element'); + expect(nodeOrEleContent(specPage.root.firstChild.nextSibling.nextSibling)).toBe(``); + expect(nodeOrEleContent(specPage.root.firstChild.nextSibling.nextSibling.nextSibling)).toBe( + `
a second slot, slotted element nested element in the second slot
`, + ); + // back we go! + expect(nodeOrEleContent(specPage.root.firstChild.nextSibling.nextSibling.nextSibling.previousSibling)).toBe(``); + expect( + nodeOrEleContent(specPage.root.firstChild.nextSibling.nextSibling.nextSibling.previousSibling.previousSibling), + ).toBe(`a default slot, slotted element`); + expect( + nodeOrEleContent( + specPage.root.firstChild.nextSibling.nextSibling.nextSibling.previousSibling.previousSibling.previousSibling, + ), + ).toBe(`Some default slot, slotted text`); + }); + + it('patches nextElementSibling / previousElementSibling accessors of slotted nodes', async () => { + specPage.root.childNodes.forEach((node: Node) => patchSlottedNode(node)); + expect(nodeOrEleContent(specPage.root.children[0].nextElementSibling)).toBe( + '
a second slot, slotted element nested element in the second slot
', + ); + expect(nodeOrEleContent(specPage.root.children[0].nextElementSibling.previousElementSibling)).toBe( + 'a default slot, slotted element', + ); + }); + + it('patches parentNode of slotted nodes', async () => { + specPage.root.childNodes.forEach((node: Node) => patchSlottedNode(node)); + expect(specPage.root.children[0].parentNode.tagName).toBe('CMP-A'); + expect(specPage.root.children[1].parentNode.tagName).toBe('CMP-A'); + expect(specPage.root.childNodes[0].parentNode.tagName).toBe('CMP-A'); + expect(specPage.root.childNodes[1].parentNode.tagName).toBe('CMP-A'); + expect(specPage.root.children[0].__parentNode.tagName).toBe('DIV'); + expect(specPage.root.childNodes[0].__parentNode.tagName).toBe('DIV'); + }); +}); diff --git a/packages/core/src/runtime/test/element.spec.tsx b/packages/core/src/runtime/test/element.spec.tsx new file mode 100644 index 00000000000..12aed559580 --- /dev/null +++ b/packages/core/src/runtime/test/element.spec.tsx @@ -0,0 +1,32 @@ +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(` + + `); + }); +}); diff --git a/packages/core/src/runtime/test/event.spec.tsx b/packages/core/src/runtime/test/event.spec.tsx new file mode 100644 index 00000000000..42f28833b88 --- /dev/null +++ b/packages/core/src/runtime/test/event.spec.tsx @@ -0,0 +1,474 @@ +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 + `); + }); + }); +}); diff --git a/packages/core/src/runtime/test/extends-basic.spec.tsx b/packages/core/src/runtime/test/extends-basic.spec.tsx new file mode 100644 index 00000000000..c7a9ec3678a --- /dev/null +++ b/packages/core/src/runtime/test/extends-basic.spec.tsx @@ -0,0 +1,65 @@ +import { Component, h, Prop, Watch } from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; + +declare global { + namespace JSX { + interface IntrinsicElements { + [elemName: string]: any; + } + } +} + +describe('extends', () => { + it('renders a component that extends from a base class', async () => { + class Base { + baseProp = 'base'; + } + @Component({ tag: 'cmp-a' }) + class CmpA extends Base { + render() { + return `${this.baseProp}`; + } + } + + const page = await newSpecPage({ + components: [CmpA], + html: ``, + }); + + expect(page.root).toEqualHtml(` + base + `); + }); + + it('should call inherited watch methods when props change', async () => { + let called = 0; + + class BaseWatch { + @Prop() foo: string; + + @Watch('foo') + fooChanged() { + called++; + } + } + + @Component({ tag: 'extended-component' }) + class ExtendedComponent extends BaseWatch { + render() { + return
{this.foo}
; + } + } + + const { root } = await newSpecPage({ + components: [ExtendedComponent], + html: ``, + }); + + expect(called).toBe(0); + + root.foo = '1'; + + expect(called).toBe(1); + expect(root.foo).toBe('1'); + }); +}); diff --git a/packages/core/src/runtime/test/fetch.spec.tsx b/packages/core/src/runtime/test/fetch.spec.tsx new file mode 100644 index 00000000000..51e027c4102 --- /dev/null +++ b/packages/core/src/runtime/test/fetch.spec.tsx @@ -0,0 +1,189 @@ +import { Component, h, Host, Prop } from '@stencil/core'; +import { mockFetch, MockHeaders, MockResponse, newSpecPage } from '@stencil/core/testing'; + +describe('fetch', () => { + afterEach(() => { + mockFetch.reset(); + }); + + @Component({ + tag: 'cmp-a', + }) + class CmpA { + @Prop() data: string; + names: string[]; + text: string; + headers: string[]; + + async componentWillLoad() { + const url = `/${this.data}`; + const rsp = await fetch(url); + + this.headers = []; + rsp.headers.forEach((v, k) => { + this.headers.push(k + ': ' + v); + }); + + if (url.endsWith('.json')) { + const data = await rsp.json(); + this.text = null; + this.names = data.names; + } else { + this.text = await rsp.text(); + this.names = null; + } + } + render() { + return ( + +
    + {this.headers.map((n) => ( +
  • {n}
  • + ))} +
+ {this.names ? ( +
    + {this.names.map((n) => ( +
  • {n}
  • + ))} +
+ ) : null} + {this.text ?

{this.text}

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

+ 10:04 +

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

+ Not Found +

+
+ `); + }); + + it('global Request/Response/Headers should work', () => { + const headers = new Headers(); + headers.set('x-header', 'value'); + const request = new Request('http://testing.stenciljs.com/some-url', { + headers, + }); + expect(request.url).toBe('http://testing.stenciljs.com/some-url'); + expect(request.headers.get('x-header')).toBe('value'); + }); +}); diff --git a/packages/core/src/runtime/test/fixtures/cmp-a.css b/packages/core/src/runtime/test/fixtures/cmp-a.css new file mode 100644 index 00000000000..cef256784c7 --- /dev/null +++ b/packages/core/src/runtime/test/fixtures/cmp-a.css @@ -0,0 +1,4 @@ + +:host { + color: red; +} diff --git a/packages/core/src/runtime/test/fixtures/cmp-a.tsx b/packages/core/src/runtime/test/fixtures/cmp-a.tsx new file mode 100644 index 00000000000..34ea92464f6 --- /dev/null +++ b/packages/core/src/runtime/test/fixtures/cmp-a.tsx @@ -0,0 +1,117 @@ +import { Component, Event, EventEmitter, h, Listen, Method, Prop, State, Watch } from '@stencil/core'; + +import { format } from './utils'; + +@Component({ + tag: 'cmp-a', + styleUrl: 'cmp-a.css', + shadow: true, +}) +export class CmpA { + // ************************ + // * Property Definitions * + // ************************ + + /** + * The first name + */ + @Prop() first: string; + + /** + * The middle name + */ + @Prop() middle: string; + + /** + * The last name + */ + @Prop() last: string; + + // ************************ + // * State Definitions * + // ************************ + + @State() innerFirst: string; + @State() innerMiddle: string; + @State() innerLast: string; + + // ***************************** + // * Watch on Property Changes * + // ***************************** + + @Watch('first') + parseFirstProp(newValue: string) { + this.innerFirst = newValue ? newValue : ''; + } + @Watch('middle') + parseMiddleProp(newValue: string) { + this.innerMiddle = newValue ? newValue : ''; + } + @Watch('last') + parseLastProp(newValue: string) { + this.innerLast = newValue ? newValue : ''; + } + + // ********************* + // * Event Definitions * + // ********************* + + /** + * Emitted when the component Loads + */ + @Event() initevent: EventEmitter; + + // ******************************* + // * Listen to Event Definitions * + // ******************************* + + @Listen('testevent', { target: 'document' }) + handleTestEvent(event: CustomEvent) { + this.parseLastProp(event.detail.last ? event.detail.last : ''); + } + + // ********************** + // * Method Definitions * + // ********************** + + @Method() + init(): Promise { + return Promise.resolve(this._init()); + } + + // ********************************* + // * Internal Variable Definitions * + // ********************************* + + // ******************************* + // * Component Lifecycle Methods * + // ******************************* + + async componentWillLoad() { + await this.init(); + } + + // ****************************** + // * Private Method Definitions * + // ****************************** + + private async _init(): Promise { + this.parseFirstProp(this.first ? this.first : ''); + this.parseMiddleProp(this.middle ? this.middle : ''); + this.parseLastProp(this.last ? this.last : ''); + this.initevent.emit({ init: true }); + return; + } + + private getText(): string { + return format(this.innerFirst, this.innerMiddle, this.innerLast); + } + + // ************************* + // * Rendering JSX Element * + // ************************* + + render() { + return
Hello, World! I'm {this.getText()}
; + } +} diff --git a/packages/core/src/runtime/test/fixtures/cmp-asset.tsx b/packages/core/src/runtime/test/fixtures/cmp-asset.tsx new file mode 100644 index 00000000000..48dc7e9e492 --- /dev/null +++ b/packages/core/src/runtime/test/fixtures/cmp-asset.tsx @@ -0,0 +1,17 @@ +import { Component, getAssetPath, h, Host, Prop } from '@stencil/core'; + +@Component({ + tag: 'cmp-asset', +}) +export class CmpAsset { + @Prop() icon: string; + + render() { + return ( + + + + + ); + } +} diff --git a/packages/core/src/runtime/test/fixtures/utils.ts b/packages/core/src/runtime/test/fixtures/utils.ts new file mode 100644 index 00000000000..4fb9a8b2855 --- /dev/null +++ b/packages/core/src/runtime/test/fixtures/utils.ts @@ -0,0 +1,3 @@ +export function format(first: string, middle: string, last: string): string { + return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : ''); +} diff --git a/packages/core/src/runtime/test/globals.spec.tsx b/packages/core/src/runtime/test/globals.spec.tsx new file mode 100644 index 00000000000..44c49e9ee6d --- /dev/null +++ b/packages/core/src/runtime/test/globals.spec.tsx @@ -0,0 +1,102 @@ +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(); + }); + }); +}); diff --git a/packages/core/src/runtime/test/host.spec.tsx b/packages/core/src/runtime/test/host.spec.tsx new file mode 100644 index 00000000000..ca2e42bbb20 --- /dev/null +++ b/packages/core/src/runtime/test/host.spec.tsx @@ -0,0 +1,101 @@ +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