@nutin/cli 1.3.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +106 -9
- package/README.md +17 -35
- package/bin/add-feature.mjs +9 -0
- package/bin/cli.mjs +8 -0
- package/bin/create-app.mjs +5 -4
- package/bin/update-app.mjs +9 -0
- package/lib/index.mjs +76 -25
- package/lib/src/add-feature/feature-adder.mjs +83 -0
- package/lib/src/add-feature/feature-context-builder.mjs +25 -0
- package/lib/src/add-feature/nutin-config-updater.mjs +29 -0
- package/lib/src/add-feature/package-json-updater.mjs +49 -0
- package/lib/src/common/feature-registry.mjs +11 -0
- package/lib/src/common/file-generator.mjs +159 -0
- package/lib/src/common/meta-bootstrap-prompt.mjs +33 -0
- package/lib/src/common/package-data.mjs +10 -0
- package/lib/src/common/package-json-helper.mjs +103 -0
- package/lib/src/common/project-meta.mjs +54 -0
- package/lib/src/create-app/context-builder.mjs +21 -0
- package/lib/src/{git-manager.mjs → create-app/git-manager.mjs} +2 -2
- package/lib/src/create-app/json-manager.mjs +73 -0
- package/lib/src/{prompts.mjs → create-app/new-app-prompt.mjs} +6 -6
- package/lib/src/{project-generator.mjs → create-app/project-generator.mjs} +10 -8
- package/lib/src/create-app/success-message.mjs +13 -0
- package/lib/src/update-app/conflict-report-writer.mjs +31 -0
- package/lib/src/update-app/fetch-old-templates.mjs +61 -0
- package/lib/src/update-app/project-updater.mjs +142 -0
- package/lib/src/update-app/template-differ.mjs +124 -0
- package/lib/src/update-app/update-context-builder.mjs +25 -0
- package/lib/src/update-app/update-summary-printer.mjs +22 -0
- package/lib/src/update-app/version-compare.mjs +12 -0
- package/lib/src/utils/print.mjs +36 -0
- package/lib/src/utils/promise-exec-alias.mjs +4 -0
- package/package.json +17 -12
- package/templates/base/.gitignore.hbs +6 -1
- package/templates/base/AGENTS.md.hbs +168 -0
- package/templates/base/GETTING_STARTED.md.hbs +199 -0
- package/templates/base/README.md.hbs +8 -17
- package/templates/base/config/languages.json.hbs +6 -0
- package/templates/base/config/seo.json.hbs +12 -0
- package/templates/base/nutin.config.js.hbs +44 -0
- package/templates/base/src/app/components/index.ts.hbs +1 -2
- package/templates/base/src/app/globals.d.ts.hbs +3 -19
- package/templates/base/src/{libs/guards → app}/guards.ts.hbs +1 -5
- package/templates/base/src/app/main.ts.hbs +6 -26
- package/templates/base/src/app/services/index.ts.hbs +1 -1
- package/templates/base/src/app/views/home/home.view.html.hbs +7 -0
- package/templates/base/src/app/views/home/home.view.ts.hbs +2 -57
- package/templates/base/src/app/views/index.ts.hbs +1 -0
- package/templates/base/src/app/views/not-found/not-found.view.html.hbs +8 -0
- package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +5 -38
- package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +158 -0
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +68 -35
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +124 -0
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +9 -8
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +96 -0
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +6 -3
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +151 -0
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +8 -2
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +88 -0
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +62 -0
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +89 -0
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +4 -1
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +142 -0
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +67 -22
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +183 -0
- package/templates/base/src/core/base-classes/component/component.test.js.hbs +85 -0
- package/templates/base/src/core/base-classes/component/component.ts.hbs +24 -33
- package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +58 -0
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +75 -0
- package/templates/base/src/core/base-classes/service/service.test.js.hbs +150 -0
- package/templates/base/src/core/base-classes/service/service.ts.hbs +26 -20
- package/templates/base/src/core/base-classes/view/view.test.js.hbs +71 -0
- package/templates/base/src/core/base-classes/view/view.ts.hbs +24 -49
- package/templates/base/src/core/config.ts.hbs +10 -0
- package/templates/base/src/core/index.ts.hbs +0 -3
- package/templates/base/src/core/internals.d.ts.hbs +27 -0
- package/templates/base/src/core/nutin-config.d.ts.hbs +4 -0
- package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +67 -3
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +9 -4
- package/templates/base/src/core/services/event-bus/index.ts.hbs +3 -0
- package/templates/base/src/core/services/event-bus/lifecycle.facade.test.js.hbs +123 -0
- package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +61 -0
- package/templates/base/src/core/services/event-bus/navigation.facade.test.js.hbs +46 -0
- package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +25 -0
- package/templates/base/src/core/services/globals/globals.test.js.hbs +187 -0
- package/templates/base/src/core/services/globals/globals.ts.hbs +104 -0
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +25 -0
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +20 -0
- package/templates/base/src/core/services/http-client/http-client.test.js.hbs +73 -2
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +4 -1
- package/templates/base/src/core/services/i18n/i18n.test.js.hbs +270 -0
- package/templates/{features/i18n → base}/src/core/services/i18n/i18n.ts.hbs +22 -4
- package/templates/base/src/core/services/index.ts.hbs +5 -6
- package/templates/base/src/core/services/pipe-registry/pipe-registry.test.js.hbs +60 -1
- package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +1 -1
- package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +128 -0
- package/templates/base/src/{libs/pipes → core/services/pipe-registry}/pipes.ts.hbs +8 -5
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +257 -6
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +56 -29
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +27 -0
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +83 -17
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +14 -38
- package/templates/base/src/core/services/router/router.test.js.hbs +304 -2
- package/templates/base/src/core/services/router/router.ts.hbs +47 -12
- package/templates/base/src/index.html.hbs +2 -6
- package/templates/base/src/styles/_styles.scss.hbs +102 -43
- package/templates/base/src/styles/base/_variables.scss.hbs +1 -86
- package/templates/base/src/styles/main.scss.hbs +1 -18
- package/templates/base/src/styles/tailwind.css.hbs +2 -0
- package/templates/base/tools/builder/builder.config.js.hbs +40 -0
- package/templates/base/tools/builder/builder.js.hbs +24 -26
- package/templates/base/tools/builder/core/app/binary-extensions.test.js.hbs +16 -0
- package/templates/base/tools/builder/core/app/compile-ts.js.hbs +16 -0
- package/templates/base/tools/builder/core/{copy-static.js.hbs → app/copy-static.js.hbs} +27 -34
- package/templates/base/tools/builder/core/app/validate-routes.js.hbs +77 -0
- package/templates/base/tools/builder/core/finalize-build.js.hbs +16 -7
- package/templates/base/tools/builder/core/{add-tags.js.hbs → html-index/add-tags.js.hbs} +2 -2
- package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +56 -0
- package/templates/base/tools/builder/core/{validate-html.js.hbs → html-index/validate-html.js.hbs} +5 -11
- package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +152 -0
- package/templates/base/tools/builder/core/html-templates/minify-html.js.hbs +15 -0
- package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +8 -0
- package/templates/{features/i18n/tools/builder/core → base/tools/builder/core/i18n}/build-i18n.js.hbs +6 -17
- package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +37 -0
- package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +30 -0
- package/templates/base/tools/builder/core/{hash-files.js.hbs → prod-bundle/hash-files.js.hbs} +14 -11
- package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +51 -0
- package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +16 -0
- package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +128 -0
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +43 -0
- package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +99 -0
- package/templates/base/tools/builder/core/seo/i18n-resolution.js.hbs +66 -0
- package/templates/base/tools/builder/core/seo/route-output.js.hbs +64 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +54 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +132 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +95 -0
- package/templates/base/tools/builder/core/styles/sass.js.hbs +40 -0
- package/templates/base/tools/builder/core/styles/tailwind.js.hbs +84 -0
- package/templates/base/tools/dev/dev-serve.js.hbs +26 -18
- package/templates/base/tools/dev/serve.js.hbs +11 -1
- package/templates/base/tools/dev/watcher.js.hbs +14 -6
- package/templates/base/tools/generator/generator.js.hbs +44 -47
- package/templates/base/tools/generator/handle-file.js.hbs +17 -44
- package/templates/base/tools/generator/handle-file.test.js.hbs +72 -0
- package/templates/base/tools/generator/templates/{component.template.js.hbs → component/component.template.js.hbs} +3 -2
- package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +23 -0
- package/templates/base/tools/generator/templates/html/html.template.js.hbs +2 -0
- package/templates/base/tools/generator/templates/html/html.template.test.js.hbs +7 -0
- package/templates/base/tools/generator/templates/index.js.hbs +7 -12
- package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +8 -0
- package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +17 -0
- package/templates/base/tools/generator/templates/scss/scss.template.js.hbs +1 -0
- package/templates/base/tools/generator/templates/scss/scss.template.test.js.hbs +7 -0
- package/templates/base/tools/generator/templates/service/service.template.js.hbs +17 -0
- package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +22 -0
- package/templates/base/tools/generator/templates/test/test.template.js.hbs +10 -0
- package/templates/base/tools/generator/templates/test/test.template.test.js.hbs +27 -0
- package/templates/base/tools/generator/templates/{view.template.js.hbs → view/view.template.js.hbs} +4 -3
- package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +24 -0
- package/templates/base/tools/testin-nutin/core/coverage/collect-coverage.js.hbs +39 -0
- package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.js.hbs +115 -0
- package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.test.js.hbs +74 -0
- package/templates/base/tools/testin-nutin/core/coverage/write-summary-report.js.hbs +54 -0
- package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +44 -0
- package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/assertion-lib.js.hbs +62 -24
- package/templates/base/tools/testin-nutin/core/globals/clock.js.hbs +206 -0
- package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/jsdom-setup.js.hbs +19 -6
- package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/register-test-globals.js.hbs +13 -4
- package/templates/base/tools/testin-nutin/core/globals/silence-console.js.hbs +23 -0
- package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/spyon.js.hbs +2 -0
- package/templates/base/tools/testin-nutin/core/index.js.hbs +9 -0
- package/templates/base/tools/testin-nutin/core/printer.js.hbs +85 -0
- package/templates/base/tools/testin-nutin/core/queue/queue.test.js.hbs +46 -0
- package/templates/base/tools/testin-nutin/core/queue/test-discovery.test.js.hbs +46 -0
- package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +154 -0
- package/templates/{features/testinNutin → base/tools}/testin-nutin/core/tests/assertions.test.js.hbs +27 -0
- package/templates/base/tools/testin-nutin/core/tests/clock.test.js.hbs +118 -0
- package/templates/base/tools/testin-nutin/core/tests/globals-each-all.test.js.hbs +28 -0
- package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/create-mock-method.js.hbs +2 -1
- package/templates/base/tools/testin-nutin/mocks/create-mock-method.test.js.hbs +64 -0
- package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-event-bus.js.hbs +3 -2
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +66 -0
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +26 -0
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +61 -0
- package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +29 -0
- package/templates/base/tools/testin-nutin/runner.js.hbs +100 -0
- package/templates/base/tools/testin-nutin/watch-tests.js.hbs +37 -0
- package/templates/base/tools/utils/error-exit.js.hbs +8 -0
- package/templates/base/tools/utils/format.js.hbs +10 -10
- package/templates/base/tools/utils/format.test.js.hbs +45 -0
- package/templates/base/tools/utils/get-files-recursive.js.hbs +6 -1
- package/templates/base/tools/utils/get-files-recursive.test.js.hbs +49 -0
- package/templates/base/tools/utils/get-rel-to-core.test.js.hbs +18 -0
- package/templates/base/tools/utils/index.js.hbs +1 -3
- package/templates/base/tools/utils/languages.js.hbs +15 -0
- package/templates/base/tools/utils/languages.test.js.hbs +16 -0
- package/templates/base/tools/utils/print.js.hbs +19 -13
- package/templates/base/tools/utils/print.test.js.hbs +60 -0
- package/templates/base/tools/utils/prompt-boolean.js.hbs +7 -0
- package/templates/base/tools/utils/prompt-boolean.test.js.hbs +54 -0
- package/templates/base/tools/utils/run-command.js.hbs +4 -2
- package/templates/base/tools/utils/run-command.test.js.hbs +19 -0
- package/templates/base/tools/utils/run-script.js.hbs +14 -3
- package/templates/base/tools/utils/run-script.test.js.hbs +40 -0
- package/templates/features/{deployHelper → docker}/.dockerignore.hbs +4 -4
- package/templates/features/docker/tools/docker/Dockerfile.template.hbs +51 -0
- package/templates/features/{deployHelper/tools/deployment/nginx.conf.hbs → docker/tools/docker/nginx.conf.template} +4 -2
- package/templates/features/docker/tools/docker/scripts/docker-build.js.hbs +20 -0
- package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +48 -0
- package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +52 -0
- package/templates/features/docker/tools/docker/scripts/docker-run.js.hbs +28 -0
- package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +23 -0
- package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +60 -0
- package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +30 -0
- package/lib/src/context-builder.mjs +0 -40
- package/lib/src/file-generator.mjs +0 -109
- package/lib/src/json-generator.mjs +0 -31
- package/lib/src/package-manager.mjs +0 -139
- package/lib/src/print.mjs +0 -32
- package/lib/src/utils.mjs +0 -18
- package/lib/src/version.mjs +0 -1
- package/templates/base/builder.config.js.hbs +0 -36
- package/templates/base/src/app/components/common/anchor/anchor.component.ts.hbs +0 -32
- package/templates/base/src/app/components/common/button/button.component.ts.hbs +0 -40
- package/templates/base/src/app/dom.test.js.hbs +0 -13
- package/templates/base/src/app/services/example/example.service.ts.hbs +0 -18
- package/templates/base/src/app/views/home/home.view.test.js.hbs +0 -26
- package/templates/base/src/core/services/store/store.test.js.hbs +0 -8
- package/templates/base/src/core/services/store/store.ts.hbs +0 -39
- package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.test.js.hbs +0 -0
- package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +0 -113
- package/templates/base/src/core/utils/button-manager/button-manager.utils.test.js.hbs +0 -249
- package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +0 -131
- package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +0 -50
- package/templates/base/src/core/utils/index.ts.hbs +0 -2
- package/templates/base/src/libs/index.ts.hbs +0 -4
- package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +0 -197
- package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +0 -75
- package/templates/base/src/libs/popover/popover.ts.hbs +0 -138
- package/templates/base/src/libs/snackbar/snackbar.ts.hbs +0 -75
- package/templates/base/src/styles/base/_functions.scss.hbs +0 -9
- package/templates/base/src/styles/core/_anchors.scss.hbs +0 -10
- package/templates/base/src/styles/core/_index.scss.hbs +0 -6
- package/templates/base/src/styles/core/_popover.scss.hbs +0 -96
- package/templates/base/src/styles/core/_snackbar.scss.hbs +0 -50
- package/templates/base/tools/builder/core/esbuild.js.hbs +0 -25
- package/templates/base/tools/builder/core/minify-html.js.hbs +0 -60
- package/templates/base/tools/builder/core/sass.js.hbs +0 -22
- package/templates/base/tools/builder/core/tsc.js.hbs +0 -12
- package/templates/base/tools/generator/templates/service.template.js.hbs +0 -17
- package/templates/base/tools/utils/export-flags.js.hbs +0 -6
- package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +0 -56
- package/templates/features/deployHelper/tools/deployment/Dockerfile.hbs +0 -30
- package/templates/features/i18n/src/app/views/home/locales/en.json.hbs +0 -9
- package/templates/features/i18n/src/app/views/not-found/locales/en.json +0 -8
- package/templates/features/i18n/src/app/views/not-found/locales/fr.json +0 -8
- package/templates/features/i18n/src/core/config.ts.hbs +0 -3
- package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +0 -125
- package/templates/features/i18n/tools/generator/templates/json.template.js.hbs +0 -8
- package/templates/features/i18n/tools/utils/languages.js.hbs +0 -7
- package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +0 -9
- package/templates/features/stylinNutin/src/styles/base/_mixins.scss.hbs +0 -372
- package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -3
- package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +0 -254
- package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
- package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +0 -4
- package/templates/features/template/src/app/views/home/home.view.html.hbs +0 -56
- package/templates/features/template/src/app/views/not-found/not-found.view.html.hbs +0 -19
- package/templates/features/template/tools/builder/core/merge-templates.js.hbs +0 -49
- package/templates/features/template/tools/generator/templates/html.template.js.hbs +0 -2
- package/templates/features/testinNutin/testin-nutin/core/index.js.hbs +0 -5
- package/templates/features/testinNutin/testin-nutin/core/src/utils/print.js.hbs +0 -64
- package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +0 -64
- package/templates/features/testinNutin/testin-nutin/core/tests/globals-each-all.test.js.hbs +0 -28
- package/templates/features/testinNutin/testin-nutin/mocks/mock-store.js.hbs +0 -38
- package/templates/features/testinNutin/testin-nutin/runner.js.hbs +0 -36
- package/templates/features/testinNutin/testin-nutin/watch-tests.js.hbs +0 -21
- package/templates/features/testinNutin/testin-nutin.config.js.hbs +0 -14
- /package/lib/src/{template-compiler.mjs → common/template-compiler.mjs} +0 -0
- /package/templates/{features/i18n → base}/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +0 -0
- /package/templates/{features/i18n → base}/src/core/services/i18n/languages.ts.hbs +0 -0
- /package/templates/base/tools/builder/{variables → core/app}/binary-extensions.js.hbs +0 -0
- /package/templates/base/tools/builder/core/{paths.js.hbs → app/paths.js.hbs} +0 -0
- /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/queue.js.hbs +0 -0
- /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/test-discovery.js.hbs +0 -0
- /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-http-client.js.hbs +0 -0
- /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-i18n.js.hbs +0 -0
- /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-router.js.hbs +0 -0
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { print
|
|
4
|
-
{
|
|
5
|
-
import { jsonTemplate } from './templates/index.js';
|
|
6
|
-
{{/if}}
|
|
7
|
-
{{#if stylinNutin}}
|
|
8
|
-
import { scssTemplate } from './templates/index.js';
|
|
9
|
-
{{/if}}
|
|
3
|
+
import { print, LANGUAGES } from '../utils/index.js';
|
|
4
|
+
import { localeTemplate } from './templates/index.js';
|
|
10
5
|
|
|
11
6
|
export function generateFile({
|
|
12
7
|
name,
|
|
@@ -16,7 +11,7 @@ export function generateFile({
|
|
|
16
11
|
extension = 'ts',
|
|
17
12
|
}) {
|
|
18
13
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
19
|
-
const template = templateFn(name, targetPath);
|
|
14
|
+
const template = templateFn(name, targetPath, suffix);
|
|
20
15
|
const filePath = `${targetPath}/${name.kebab}.${suffix}.${extension}`;
|
|
21
16
|
|
|
22
17
|
if (fs.existsSync(filePath)) {
|
|
@@ -42,51 +37,29 @@ export function appendToIndex({ name, targetPath, suffix }) {
|
|
|
42
37
|
|
|
43
38
|
try {
|
|
44
39
|
fs.appendFileSync(indexFilePath, lineToAppend, 'utf8');
|
|
45
|
-
print.
|
|
40
|
+
print.gray(`${suffix}s/index.ts updated.`);
|
|
46
41
|
} catch (err) {
|
|
47
|
-
|
|
42
|
+
throw new Error(`Failed to update ${indexFilePath}: ${err.message}`, { cause: err });
|
|
48
43
|
}
|
|
49
44
|
}
|
|
50
45
|
|
|
51
|
-
{
|
|
52
|
-
export function generateJson({ targetPath, name }) {
|
|
46
|
+
export function generateLocalesJson({ targetPath, name, isView }) {
|
|
53
47
|
const localesDir = `${targetPath}/locales`;
|
|
54
48
|
fs.mkdirSync(localesDir, { recursive: true });
|
|
55
49
|
|
|
56
|
-
const template =
|
|
50
|
+
const template = localeTemplate(name, isView);
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
export function generateStylesheet({ name, suffix }) {
|
|
67
|
-
const template = scssTemplate();
|
|
68
|
-
const stylesPath = path.join('src', 'styles', `${suffix}s`);
|
|
69
|
-
const filePath = path.join(stylesPath, `_${name.kebab}.${suffix}.scss`);
|
|
70
|
-
const indexPath = path.join(stylesPath, `_index.scss`);
|
|
71
|
-
|
|
72
|
-
if (!fs.existsSync(stylesPath) || !fs.existsSync(indexPath)) {
|
|
73
|
-
print.boldError('Non existing path');
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
if (fs.existsSync(filePath)) {
|
|
77
|
-
print.boldError('A file with this name already exists');
|
|
78
|
-
process.exit(1);
|
|
52
|
+
const failedLangs = [];
|
|
53
|
+
for (const lang of LANGUAGES) {
|
|
54
|
+
try {
|
|
55
|
+
fs.writeFileSync(`${localesDir}/${lang}.json`, template);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
print.error(`Failed to write locale file for "${lang}": ${err.message}`);
|
|
58
|
+
failedLangs.push(lang);
|
|
59
|
+
}
|
|
79
60
|
}
|
|
80
61
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const lineToAppend = `@forward "${name.kebab}.${suffix}";\n`;
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
fs.appendFileSync(indexPath, lineToAppend, 'utf8');
|
|
87
|
-
} catch (err) {
|
|
88
|
-
print.error(`Error appending line: ${err}`);
|
|
62
|
+
if (failedLangs.length) {
|
|
63
|
+
throw new Error(`Failed to generate locale file(s) for: ${failedLangs.join(', ')}`);
|
|
89
64
|
}
|
|
90
65
|
}
|
|
91
|
-
|
|
92
|
-
{{/if}}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { generateFile, appendToIndex } from './handle-file.js';
|
|
5
|
+
|
|
6
|
+
describe('handle-file', () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
let originalCwd;
|
|
9
|
+
let exitSpy;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nutin-handle-file-'));
|
|
13
|
+
originalCwd = process.cwd();
|
|
14
|
+
exitSpy = spyOn(process, 'exit').andCallFake(() => {});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
exitSpy.restore();
|
|
19
|
+
process.chdir(originalCwd);
|
|
20
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('generateFile writes the template output to <targetPath>/<name>.<suffix>.<extension>', () => {
|
|
24
|
+
const targetPath = path.join(tmpDir, 'components', 'widget');
|
|
25
|
+
const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
|
|
26
|
+
|
|
27
|
+
generateFile({ name, targetPath, templateFn: (n) => `content for ${n.kebab}`, suffix: 'component' });
|
|
28
|
+
|
|
29
|
+
const filePath = path.join(targetPath, 'widget.component.ts');
|
|
30
|
+
expect(fs.existsSync(filePath)).toBeTruthy();
|
|
31
|
+
expect(fs.readFileSync(filePath, 'utf8')).toBe('content for widget');
|
|
32
|
+
expect(exitSpy.callCount).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('generateFile respects a custom extension', () => {
|
|
36
|
+
const targetPath = path.join(tmpDir, 'services', 'widget');
|
|
37
|
+
const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
|
|
38
|
+
|
|
39
|
+
generateFile({ name, targetPath, templateFn: () => 'x', suffix: 'service', extension: 'js' });
|
|
40
|
+
|
|
41
|
+
expect(fs.existsSync(path.join(targetPath, 'widget.service.js'))).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('generateFile calls process.exit(1) instead of overwriting an existing file', () => {
|
|
45
|
+
const targetPath = path.join(tmpDir, 'components', 'widget');
|
|
46
|
+
const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
|
|
47
|
+
const templateFn = () => 'fixed content';
|
|
48
|
+
|
|
49
|
+
generateFile({ name, targetPath, templateFn, suffix: 'component' });
|
|
50
|
+
// the second call hits the already-exists guard, which prints via console.error
|
|
51
|
+
silenceConsole('error', () => generateFile({ name, targetPath, templateFn, suffix: 'component' }));
|
|
52
|
+
|
|
53
|
+
expect(exitSpy.callCount).toBe(1);
|
|
54
|
+
expect(exitSpy.lastCall).toEqual([1]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('appendToIndex appends an export line to the barrel three segments up from targetPath', () => {
|
|
58
|
+
process.chdir(tmpDir);
|
|
59
|
+
const basePath = path.join('src', 'app', 'components');
|
|
60
|
+
fs.mkdirSync(basePath, { recursive: true });
|
|
61
|
+
fs.writeFileSync(path.join(basePath, 'index.ts'), "export * from './existing.js';\n");
|
|
62
|
+
|
|
63
|
+
const targetPath = path.join('src', 'app', 'components', 'widget');
|
|
64
|
+
const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
|
|
65
|
+
|
|
66
|
+
// appendToIndex prints a "index.ts updated" log via console.info
|
|
67
|
+
silenceConsole('info', () => appendToIndex({ name, targetPath, suffix: 'component' }));
|
|
68
|
+
|
|
69
|
+
const indexContent = fs.readFileSync(path.join(basePath, 'index.ts'), 'utf8');
|
|
70
|
+
expect(indexContent).toBe("export * from './existing.js';\nexport * from './widget/widget.component.js';\n");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { getRelToCore } from "
|
|
1
|
+
import { getRelToCore } from "../../../utils/get-rel-to-core.js";
|
|
2
2
|
|
|
3
3
|
export const componentTemplate = (name, targetPath) => {
|
|
4
4
|
const relToCore = getRelToCore(targetPath);
|
|
5
|
+
const htmlTemplate = '__TEMPLATE_PLACEHOLDER__';
|
|
5
6
|
|
|
6
7
|
return `import { Component } from '${relToCore}';
|
|
7
8
|
|
|
8
|
-
const templateFn = () => {
|
|
9
|
+
const templateFn = () => \`${htmlTemplate}\`;
|
|
9
10
|
|
|
10
11
|
export class ${name.pascal}Component extends Component {
|
|
11
12
|
constructor(mountTarget: HTMLElement) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { componentTemplate } from './component.template.js';
|
|
2
|
+
|
|
3
|
+
describe('componentTemplate', () => {
|
|
4
|
+
it('renders a component class importing Component via the relative core path', () => {
|
|
5
|
+
const name = { pascal: 'Widget' };
|
|
6
|
+
const targetPath = 'src/app/components/widget';
|
|
7
|
+
|
|
8
|
+
// The generator always leaves the html body as a placeholder — the builder
|
|
9
|
+
// swaps in the .html sibling's content at build time.
|
|
10
|
+
expect(componentTemplate(name, targetPath)).toBe(
|
|
11
|
+
`import { Component } from '../../../core/index.js';
|
|
12
|
+
|
|
13
|
+
const templateFn = () => \`__TEMPLATE_PLACEHOLDER__\`;
|
|
14
|
+
|
|
15
|
+
export class WidgetComponent extends Component {
|
|
16
|
+
constructor(mountTarget: HTMLElement) {
|
|
17
|
+
super({templateFn, mountTarget});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
`
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
export * from './component.template.js';
|
|
2
|
-
export * from './view.template.js';
|
|
3
|
-
export * from './service.template.js';
|
|
4
|
-
|
|
5
|
-
export * from './
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export * from './json.template.js';
|
|
9
|
-
{{/if}}
|
|
10
|
-
{{#if stylinNutin}}
|
|
11
|
-
export * from './scss.template.js';
|
|
12
|
-
{{/if}}
|
|
1
|
+
export * from './component/component.template.js';
|
|
2
|
+
export * from './view/view.template.js';
|
|
3
|
+
export * from './service/service.template.js';
|
|
4
|
+
export * from './locale/locale.template.js';
|
|
5
|
+
export * from './scss/scss.template.js';
|
|
6
|
+
export * from './html/html.template.js';
|
|
7
|
+
export * from './test/test.template.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { localeTemplate } from './locale.template.js';
|
|
2
|
+
|
|
3
|
+
describe('localeTemplate', () => {
|
|
4
|
+
it('renders a locale JSON stub with a "default" key', () => {
|
|
5
|
+
expect(localeTemplate({ pascal: 'Widget', capitalized: 'Widget' })).toBe('{\n "default": "Widget works !"\n}\n');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('does not include a "title" key when isView is false', () => {
|
|
9
|
+
expect(localeTemplate({ pascal: 'Widget', capitalized: 'Widget' }, false)).toBe('{\n "default": "Widget works !"\n}\n');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('includes a top-level "title" key seeded from the capitalized name when isView is true', () => {
|
|
13
|
+
expect(localeTemplate({ pascal: 'Widget', capitalized: 'Widget' }, true)).toBe(
|
|
14
|
+
'{\n "title": "Widget",\n "default": "Widget works !"\n}\n'
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const scssTemplate = () => `\n`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getRelToCore } from "../../../utils/get-rel-to-core.js";
|
|
2
|
+
|
|
3
|
+
export const serviceTemplate = (name, targetPath) => {
|
|
4
|
+
const relToCore = getRelToCore(targetPath);
|
|
5
|
+
|
|
6
|
+
return `import { Service } from '${relToCore}';
|
|
7
|
+
|
|
8
|
+
export class ${name.pascal}Service extends Service<${name.pascal}Service> {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ${name.camel}Service = ${name.pascal}Service.getInstance();
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { serviceTemplate } from './service.template.js';
|
|
2
|
+
|
|
3
|
+
describe('serviceTemplate', () => {
|
|
4
|
+
it('renders a Service subclass with a singleton export, importing Service via the relative core path', () => {
|
|
5
|
+
const name = { pascal: 'Widget', camel: 'widget' };
|
|
6
|
+
const targetPath = 'src/app/components/widget';
|
|
7
|
+
|
|
8
|
+
expect(serviceTemplate(name, targetPath)).toBe(
|
|
9
|
+
`import { Service } from '../../../core/index.js';
|
|
10
|
+
|
|
11
|
+
export class WidgetService extends Service<WidgetService> {
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const widgetService = WidgetService.getInstance();
|
|
19
|
+
`
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const testTemplate = (name, targetPath, suffix) => {
|
|
2
|
+
const type = suffix.charAt(0).toUpperCase() + suffix.slice(1);
|
|
3
|
+
|
|
4
|
+
return `import { ${name.pascal}${type} } from '#root/dist/${targetPath}/${name.kebab}.${suffix}.js';
|
|
5
|
+
|
|
6
|
+
describe('${name.pascal}${type}', () => {
|
|
7
|
+
it.todo('Write tests for ${name.pascal}${type}');
|
|
8
|
+
});
|
|
9
|
+
`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { testTemplate } from './test.template.js';
|
|
2
|
+
|
|
3
|
+
describe('testTemplate', () => {
|
|
4
|
+
it('renders an it.todo stub importing the built output via #root/dist', () => {
|
|
5
|
+
const name = { pascal: 'Widget', kebab: 'widget' };
|
|
6
|
+
const targetPath = 'src/app/components/widget';
|
|
7
|
+
|
|
8
|
+
expect(testTemplate(name, targetPath, 'component')).toBe(
|
|
9
|
+
`import { WidgetComponent } from '#root/dist/src/app/components/widget/widget.component.js';
|
|
10
|
+
|
|
11
|
+
describe('WidgetComponent', () => {
|
|
12
|
+
it.todo('Write tests for WidgetComponent');
|
|
13
|
+
});
|
|
14
|
+
`
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('capitalizes only the first letter of the suffix for the "Type" suffix on the class name', () => {
|
|
19
|
+
const name = { pascal: 'Home', kebab: 'home' };
|
|
20
|
+
const targetPath = 'src/app/views/home';
|
|
21
|
+
|
|
22
|
+
const result = testTemplate(name, targetPath, 'view');
|
|
23
|
+
|
|
24
|
+
expect(result).toContain('HomeView');
|
|
25
|
+
expect(result).toContain("it.todo('Write tests for HomeView');");
|
|
26
|
+
});
|
|
27
|
+
});
|
package/templates/base/tools/generator/templates/{view.template.js.hbs → view/view.template.js.hbs}
RENAMED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { getRelToCore } from "
|
|
1
|
+
import { getRelToCore } from "../../../utils/get-rel-to-core.js";
|
|
2
2
|
|
|
3
3
|
export const viewTemplate = (name, targetPath) => {
|
|
4
4
|
const relToCore = getRelToCore(targetPath);
|
|
5
|
+
const htmlTemplate = '__TEMPLATE_PLACEHOLDER__';
|
|
5
6
|
|
|
6
7
|
return `import { View } from '${relToCore}';
|
|
7
8
|
|
|
8
|
-
const template = {
|
|
9
|
+
const template = \`${htmlTemplate}\`;
|
|
9
10
|
|
|
10
11
|
export class ${name.pascal}View extends View {
|
|
11
12
|
constructor() {
|
|
12
|
-
super({template});
|
|
13
|
+
super({ template, viewName: '${name.kebab}' });
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { viewTemplate } from './view.template.js';
|
|
2
|
+
|
|
3
|
+
describe('viewTemplate', () => {
|
|
4
|
+
it('renders a view class importing View via the relative core path', () => {
|
|
5
|
+
const name = { kebab: 'widget', pascal: 'Widget' };
|
|
6
|
+
const targetPath = 'src/app/components/widget';
|
|
7
|
+
|
|
8
|
+
// The generator always leaves the html body as a placeholder — the builder
|
|
9
|
+
// swaps in the .html sibling's content at build time.
|
|
10
|
+
expect(viewTemplate(name, targetPath)).toBe(
|
|
11
|
+
`import { View } from '../../../core/index.js';
|
|
12
|
+
|
|
13
|
+
const template = \`__TEMPLATE_PLACEHOLDER__\`;
|
|
14
|
+
|
|
15
|
+
export class WidgetView extends View {
|
|
16
|
+
constructor() {
|
|
17
|
+
super({ template, viewName: 'widget' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
`
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Session } from 'node:inspector/promises';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { errorExit } from '../../../utils/index.js';
|
|
5
|
+
|
|
6
|
+
let session;
|
|
7
|
+
|
|
8
|
+
export async function startCoverage() {
|
|
9
|
+
try {
|
|
10
|
+
session = new Session();
|
|
11
|
+
session.connect();
|
|
12
|
+
await session.post('Profiler.enable');
|
|
13
|
+
await session.post('Profiler.startPreciseCoverage', { callCount: true, detailed: true });
|
|
14
|
+
} catch (err) {
|
|
15
|
+
errorExit(err, 'coverage/start');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function stopCoverage(scopedDirs) {
|
|
20
|
+
let result;
|
|
21
|
+
try {
|
|
22
|
+
({ result } = await session.post('Profiler.takePreciseCoverage'));
|
|
23
|
+
await session.post('Profiler.stopPreciseCoverage');
|
|
24
|
+
await session.post('Profiler.disable');
|
|
25
|
+
session.disconnect();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
errorExit(err, 'coverage/stop');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result
|
|
31
|
+
.filter(entry => entry.url.startsWith('file://'))
|
|
32
|
+
.map(entry => ({ ...entry, filePath: fileURLToPath(entry.url) }))
|
|
33
|
+
.filter(({ filePath }) => {
|
|
34
|
+
const rel = path.relative(process.cwd(), filePath);
|
|
35
|
+
if (rel.includes(`node_modules${path.sep}`)) return false;
|
|
36
|
+
if (rel.endsWith('.test.js')) return false;
|
|
37
|
+
return scopedDirs.some(dir => rel.startsWith(dir + path.sep));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { errorExit } from '../../../utils/index.js';
|
|
4
|
+
|
|
5
|
+
// V8 ranges within a script are always disjoint or strictly nested, never partially
|
|
6
|
+
// overlapping. The smallest range containing an offset holds the true execution count
|
|
7
|
+
// for that offset - e.g. "this function ran 5x but this one branch inside it ran 0x".
|
|
8
|
+
function countAt(offset, ranges) {
|
|
9
|
+
let best = null;
|
|
10
|
+
for (const range of ranges) {
|
|
11
|
+
if (offset < range.startOffset || offset >= range.endOffset) continue;
|
|
12
|
+
if (!best || (range.endOffset - range.startOffset) < (best.endOffset - best.startOffset)) {
|
|
13
|
+
best = range;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return best ? best.count : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildLineStarts(sourceText) {
|
|
20
|
+
const starts = [0];
|
|
21
|
+
for (let i = 0; i < sourceText.length; i++) {
|
|
22
|
+
if (sourceText[i] === '\n') starts.push(i + 1);
|
|
23
|
+
}
|
|
24
|
+
return starts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function offsetToLine(offset, lineStarts) {
|
|
28
|
+
let lo = 0, hi = lineStarts.length - 1;
|
|
29
|
+
while (lo < hi) {
|
|
30
|
+
const mid = (lo + hi + 1) >> 1;
|
|
31
|
+
if (lineStarts[mid] <= offset) lo = mid; else hi = mid - 1;
|
|
32
|
+
}
|
|
33
|
+
return lo + 1; // 1-indexed
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// V8 sometimes reports an empty functionName (e.g. accessors, unnamed callbacks).
|
|
37
|
+
// Fall back to reading the source at the function's own start offset instead of a
|
|
38
|
+
// meaningless placeholder - covers `constructor(`, `get foo(`, method names, etc.
|
|
39
|
+
// Only treat a leading identifier as a real name when it's immediately followed by
|
|
40
|
+
// `(` (a param list) - otherwise it's just an arrow function's bare parameter (e.g.
|
|
41
|
+
// `c => ...`), which isn't a name at all, so fall through to a source snippet instead.
|
|
42
|
+
function deriveLabel(sourceText, startOffset) {
|
|
43
|
+
const snippet = sourceText.slice(startOffset, startOffset + 60);
|
|
44
|
+
const named = snippet.match(/^\s*(?:get |set )?([A-Za-z_$][\w$]*)\s*\(/);
|
|
45
|
+
if (named) return named[1];
|
|
46
|
+
return snippet.trim().split('\n')[0].slice(0, 30) || '<anonymous>';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function computeFileCoverage({ functions }, sourceText) {
|
|
50
|
+
const allRanges = functions.flatMap(fn => fn.ranges);
|
|
51
|
+
const lineStarts = buildLineStarts(sourceText);
|
|
52
|
+
|
|
53
|
+
// Line coverage: sample the count at each line's first non-whitespace character.
|
|
54
|
+
// Not statement/branch-accurate, but simple and good enough for a lightweight report.
|
|
55
|
+
let offset = 0;
|
|
56
|
+
let totalLines = 0;
|
|
57
|
+
let coveredLines = 0;
|
|
58
|
+
const uncoveredLines = [];
|
|
59
|
+
for (const line of sourceText.split('\n')) {
|
|
60
|
+
const firstChar = line.search(/\S/);
|
|
61
|
+
if (firstChar !== -1) {
|
|
62
|
+
totalLines++;
|
|
63
|
+
if (countAt(offset + firstChar, allRanges) > 0) coveredLines++;
|
|
64
|
+
else uncoveredLines.push(offsetToLine(offset + firstChar, lineStarts));
|
|
65
|
+
}
|
|
66
|
+
offset += line.length + 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Function coverage: every functions[] entry except the whole-script pseudo-function,
|
|
70
|
+
// identified by its first range spanning the entire file.
|
|
71
|
+
const realFunctions = functions.filter(fn => {
|
|
72
|
+
const [topRange] = fn.ranges;
|
|
73
|
+
return !(topRange.startOffset === 0 && topRange.endOffset === sourceText.length);
|
|
74
|
+
});
|
|
75
|
+
const coveredFunctions = realFunctions.filter(fn => fn.ranges[0].count > 0).length;
|
|
76
|
+
const uncoveredFunctions = realFunctions
|
|
77
|
+
.filter(fn => fn.ranges[0].count === 0)
|
|
78
|
+
.map(fn => ({
|
|
79
|
+
name: fn.functionName || deriveLabel(sourceText, fn.ranges[0].startOffset),
|
|
80
|
+
line: offsetToLine(fn.ranges[0].startOffset, lineStarts),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// Branch coverage: every range beyond a function's own top-level range (index 0)
|
|
84
|
+
// is a nested conditional block V8 already tracked while collecting detailed
|
|
85
|
+
// coverage - if/else arms, ternary branches, switch cases, logical short-circuits,
|
|
86
|
+
// loop bodies. No AST parsing needed, just reusing data already being collected.
|
|
87
|
+
const allBranchRanges = functions.flatMap(fn => fn.ranges.slice(1));
|
|
88
|
+
const coveredBranches = allBranchRanges.filter(r => r.count > 0).length;
|
|
89
|
+
const uncoveredBranches = allBranchRanges
|
|
90
|
+
.filter(r => r.count === 0)
|
|
91
|
+
.map(r => offsetToLine(r.startOffset, lineStarts))
|
|
92
|
+
.sort((a, b) => a - b);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
totalLines, coveredLines, uncoveredLines,
|
|
96
|
+
linesPct: totalLines ? (coveredLines / totalLines) * 100 : 100,
|
|
97
|
+
totalBranches: allBranchRanges.length, coveredBranches, uncoveredBranches,
|
|
98
|
+
branchesPct: allBranchRanges.length ? (coveredBranches / allBranchRanges.length) * 100 : 100,
|
|
99
|
+
totalFunctions: realFunctions.length, coveredFunctions, uncoveredFunctions,
|
|
100
|
+
functionsPct: realFunctions.length ? (coveredFunctions / realFunctions.length) * 100 : 100,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildCoverageReport(scopedResults) {
|
|
105
|
+
return scopedResults.map(entry => {
|
|
106
|
+
let sourceText;
|
|
107
|
+
try {
|
|
108
|
+
sourceText = fs.readFileSync(entry.filePath, 'utf-8');
|
|
109
|
+
} catch (err) {
|
|
110
|
+
errorExit(err, 'coverage/compute-coverage');
|
|
111
|
+
}
|
|
112
|
+
const coverage = computeFileCoverage(entry, sourceText);
|
|
113
|
+
return { file: path.relative(process.cwd(), entry.filePath), ...coverage };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { computeFileCoverage } from './compute-coverage.js';
|
|
2
|
+
|
|
3
|
+
describe('computeFileCoverage', () => {
|
|
4
|
+
it('reports 100% when every function and line ran', () => {
|
|
5
|
+
const sourceText = 'function covered() {\n return 1;\n}\n';
|
|
6
|
+
const scriptResult = {
|
|
7
|
+
functions: [
|
|
8
|
+
{ functionName: '', ranges: [{ startOffset: 0, endOffset: sourceText.length, count: 1 }] },
|
|
9
|
+
{ functionName: 'covered', ranges: [{ startOffset: 0, endOffset: sourceText.length - 1, count: 3 }] },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const { linesPct, functionsPct, branchesPct } = computeFileCoverage(scriptResult, sourceText);
|
|
14
|
+
expect(linesPct).toEqual(100);
|
|
15
|
+
expect(functionsPct).toEqual(100);
|
|
16
|
+
expect(branchesPct).toEqual(100); // no nested ranges anywhere -> defaults to 100
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('reports 0% for a function that never ran', () => {
|
|
20
|
+
const sourceText = 'function uncovered() {\n return 1;\n}\n';
|
|
21
|
+
const scriptResult = {
|
|
22
|
+
functions: [
|
|
23
|
+
{ functionName: '', ranges: [{ startOffset: 0, endOffset: sourceText.length, count: 1 }] },
|
|
24
|
+
{ functionName: 'uncovered', ranges: [{ startOffset: 0, endOffset: sourceText.length - 1, count: 0 }] },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const { linesPct, functionsPct } = computeFileCoverage(scriptResult, sourceText);
|
|
29
|
+
expect(linesPct).toEqual(0);
|
|
30
|
+
expect(functionsPct).toEqual(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('lets a nested 0-count branch override its enclosing covered range', () => {
|
|
34
|
+
const sourceText = 'function branchy(flag) {\n if (flag) {\n return 1;\n }\n return 2;\n}\n';
|
|
35
|
+
const branchStart = sourceText.indexOf('return 1');
|
|
36
|
+
const branchEnd = sourceText.indexOf(';', branchStart) + 1;
|
|
37
|
+
|
|
38
|
+
const scriptResult = {
|
|
39
|
+
functions: [
|
|
40
|
+
{ functionName: '', ranges: [{ startOffset: 0, endOffset: sourceText.length, count: 1 }] },
|
|
41
|
+
{
|
|
42
|
+
functionName: 'branchy',
|
|
43
|
+
ranges: [
|
|
44
|
+
{ startOffset: 0, endOffset: sourceText.length - 1, count: 5 },
|
|
45
|
+
{ startOffset: branchStart, endOffset: branchEnd, count: 0 },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const { linesPct, totalBranches, coveredBranches, branchesPct } = computeFileCoverage(scriptResult, sourceText);
|
|
52
|
+
// every line covered except the never-taken "return 1;" branch
|
|
53
|
+
expect(linesPct).toEqual(5 / 6 * 100);
|
|
54
|
+
expect(totalBranches).toEqual(1);
|
|
55
|
+
expect(coveredBranches).toEqual(0);
|
|
56
|
+
expect(branchesPct).toEqual(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('derives a name from the source when V8 reports an empty functionName', () => {
|
|
60
|
+
const sourceText = 'class Foo {\n constructor(x) {\n this.x = x;\n }\n}\n';
|
|
61
|
+
const constructorStart = sourceText.indexOf('constructor');
|
|
62
|
+
const constructorEnd = sourceText.indexOf('}', constructorStart) + 1;
|
|
63
|
+
|
|
64
|
+
const scriptResult = {
|
|
65
|
+
functions: [
|
|
66
|
+
{ functionName: '', ranges: [{ startOffset: 0, endOffset: sourceText.length, count: 1 }] },
|
|
67
|
+
{ functionName: '', ranges: [{ startOffset: constructorStart, endOffset: constructorEnd, count: 0 }] },
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const { uncoveredFunctions } = computeFileCoverage(scriptResult, sourceText);
|
|
72
|
+
expect(uncoveredFunctions).toEqual([{ name: 'constructor', line: 2 }]);
|
|
73
|
+
});
|
|
74
|
+
});
|