@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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import * as fsExtra from 'fs-extra';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { print } from '../utils/print.mjs';
|
|
5
|
+
import { PACKAGE_VERSION as packageVersion, META_FILE_NAME } from '../common/package-data.mjs';
|
|
6
|
+
import { readProjectMeta, updateProjectMeta } from '../common/project-meta.mjs';
|
|
7
|
+
import { fetchOldTemplates } from './fetch-old-templates.mjs';
|
|
8
|
+
import { parseVersion, compareVersions } from './version-compare.mjs';
|
|
9
|
+
import { bootstrapProjectMeta } from '../common/meta-bootstrap-prompt.mjs';
|
|
10
|
+
import { printUpdateSummary } from './update-summary-printer.mjs';
|
|
11
|
+
import { writeConflictReport } from './conflict-report-writer.mjs';
|
|
12
|
+
import { UpdateContextBuilder } from './update-context-builder.mjs';
|
|
13
|
+
import { TemplateDiffer } from './template-differ.mjs';
|
|
14
|
+
|
|
15
|
+
const fs = fsExtra.default;
|
|
16
|
+
const REPORT_FILE_NAME = 'NUTIN-UPDATE-REPORT.md';
|
|
17
|
+
|
|
18
|
+
export class ProjectUpdater {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.contextBuilder = new UpdateContextBuilder();
|
|
21
|
+
this.differ = new TemplateDiffer();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async updateProject(projectPath, options = {}) {
|
|
25
|
+
const { yes = false, from } = options;
|
|
26
|
+
const meta = await this.resolveMeta(projectPath);
|
|
27
|
+
|
|
28
|
+
if (this.isUpToDate(meta)) {
|
|
29
|
+
print.boldSuccess(`✅ Already up to date (nutin v${packageVersion}).`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.assertUpdatable(meta);
|
|
33
|
+
|
|
34
|
+
print.boldHead(`\n🔄 Updating ${projectPath} from nutin v${meta.version} to v${packageVersion}...\n`);
|
|
35
|
+
|
|
36
|
+
const { templatesRoot: oldTemplatesRoot, cleanup } = await fetchOldTemplates(meta.version, { from });
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const changeSet = await this.computeChangeSet(projectPath, meta, oldTemplatesRoot);
|
|
40
|
+
await this.applyChangeSet(projectPath, meta, changeSet, { yes });
|
|
41
|
+
} finally {
|
|
42
|
+
await cleanup();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async resolveMeta(projectPath) {
|
|
47
|
+
const meta = (await readProjectMeta(projectPath)) ?? (await bootstrapProjectMeta(projectPath));
|
|
48
|
+
this.assertValidVersion(meta.version);
|
|
49
|
+
return meta;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// meta.version ends up interpolated into a shell command in fetchOldTemplates
|
|
53
|
+
// (npm pack), so it must be strictly numeric before anything downstream uses it —
|
|
54
|
+
// .nutin-meta.json is a plain project file that could be hand-edited or come from
|
|
55
|
+
// an untrusted clone.
|
|
56
|
+
assertValidVersion(version) {
|
|
57
|
+
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`${META_FILE_NAME} has an invalid version "${version}" — expected a version in major.minor.patch format, e.g. "1.3.1". ` +
|
|
60
|
+
`Fix or delete ${META_FILE_NAME} and try again.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
isUpToDate(meta) {
|
|
66
|
+
return compareVersions(meta.version, packageVersion) === 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
assertUpdatable(meta) {
|
|
70
|
+
const comparison = compareVersions(meta.version, packageVersion);
|
|
71
|
+
if (comparison > 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`This project records nutin v${meta.version}, but the installed CLI is v${packageVersion}. ` +
|
|
74
|
+
'Install the latest nutin with "npm i -g @nutin/cli" before running nutin-update.',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (parseVersion(meta.version).major !== parseVersion(packageVersion).major) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`nutin-update only handles minor/patch updates. Project is on v${meta.version}, installed CLI is ` +
|
|
80
|
+
`v${packageVersion} — that's a major version change. Please migrate manually (see the changelog).`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async computeChangeSet(projectPath, meta, oldTemplatesRoot) {
|
|
86
|
+
const { oldContext, newContext } = await this.contextBuilder.buildContexts(projectPath, meta);
|
|
87
|
+
return this.differ.diff(projectPath, oldTemplatesRoot, oldContext, newContext);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async applyChangeSet(projectPath, meta, changeSet, { yes }) {
|
|
91
|
+
const { toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated } = changeSet;
|
|
92
|
+
printUpdateSummary({ toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated });
|
|
93
|
+
|
|
94
|
+
if (toUpdate.length === 0 && toAdd.length === 0 && conflicts.length === 0) {
|
|
95
|
+
print.boldSuccess('\n✅ Nothing to update.');
|
|
96
|
+
await updateProjectMeta(projectPath, { version: packageVersion });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!yes && (toUpdate.length > 0 || toAdd.length > 0)) {
|
|
101
|
+
if (!process.stdin.isTTY) {
|
|
102
|
+
print.warn('\n⚠️ Non-interactive shell detected — skipping the confirmation prompt.');
|
|
103
|
+
print.boldInfo('Re-run with --yes to apply these updates automatically. No files were changed.');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { proceed } = await inquirer.prompt([
|
|
108
|
+
{
|
|
109
|
+
type: 'confirm',
|
|
110
|
+
name: 'proceed',
|
|
111
|
+
message: `\nApply ${toUpdate.length + toAdd.length} safe file update(s)?`,
|
|
112
|
+
default: true,
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
if (!proceed) {
|
|
116
|
+
print.boldInfo('Aborted — no files were changed.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const { relPath, newEntry } of [...toUpdate, ...toAdd]) {
|
|
122
|
+
const outputPath = path.join(projectPath, relPath);
|
|
123
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
124
|
+
await fs.writeFile(outputPath, newEntry.content);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (conflicts.length > 0) {
|
|
128
|
+
await writeConflictReport(projectPath, conflicts, meta.version, packageVersion);
|
|
129
|
+
print.boldError(`\n⚠️ ${conflicts.length} file(s) you modified need manual attention — see ${REPORT_FILE_NAME}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await updateProjectMeta(projectPath, { version: packageVersion });
|
|
133
|
+
|
|
134
|
+
print.boldSuccess(`\n✅ Updated to nutin v${packageVersion}.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const projectUpdater = new ProjectUpdater();
|
|
139
|
+
|
|
140
|
+
export async function updateProject(projectPath, options = {}) {
|
|
141
|
+
return projectUpdater.updateProject(projectPath, options);
|
|
142
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import * as fsExtra from 'fs-extra';
|
|
3
|
+
import { FileGenerator } from '../common/file-generator.mjs';
|
|
4
|
+
|
|
5
|
+
const fs = fsExtra.default;
|
|
6
|
+
|
|
7
|
+
function contentEquals(a, b) {
|
|
8
|
+
if (Buffer.isBuffer(a) || Buffer.isBuffer(b)) {
|
|
9
|
+
return Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
|
|
10
|
+
}
|
|
11
|
+
return a === b;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Paths the user is expected to hand-edit right after `create-app` — their generated
|
|
15
|
+
// content is just placeholder/starter scaffolding, so nutin-update must never rewrite
|
|
16
|
+
// or conflict-flag them.
|
|
17
|
+
const FULLY_EXCLUDED_PATTERNS = [
|
|
18
|
+
/^config\/[^/]+\.json$/,
|
|
19
|
+
/^src\/app(\/|$)/,
|
|
20
|
+
/^src\/assets(\/|$)/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Like FULLY_EXCLUDED_PATTERNS, but only for files that already existed before this
|
|
24
|
+
// update — a brand new file introduced by a newer template version should still be added.
|
|
25
|
+
const ADD_ONLY_PATTERNS = [
|
|
26
|
+
/^src\/styles(\/|$)/,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function toPosixPath(relPath) {
|
|
30
|
+
return relPath.split(path.sep).join('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function excludeUserOwnedPaths(oldTree, newTree) {
|
|
34
|
+
for (const relPath of new Set([...oldTree.keys(), ...newTree.keys()])) {
|
|
35
|
+
const posixPath = toPosixPath(relPath);
|
|
36
|
+
const fullyExcluded = FULLY_EXCLUDED_PATTERNS.some((re) => re.test(posixPath));
|
|
37
|
+
const preexistingAddOnly = ADD_ONLY_PATTERNS.some((re) => re.test(posixPath)) && oldTree.has(relPath);
|
|
38
|
+
|
|
39
|
+
if (fullyExcluded || preexistingAddOnly) {
|
|
40
|
+
oldTree.delete(relPath);
|
|
41
|
+
newTree.delete(relPath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class TemplateDiffer {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.fileGenerator = new FileGenerator();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async diff(projectPath, oldTemplatesRoot, oldContext, newContext) {
|
|
52
|
+
const newTemplatesRoot = this.fileGenerator.getTemplatesRoot();
|
|
53
|
+
|
|
54
|
+
const [oldTree, newTree] = await Promise.all([
|
|
55
|
+
this.fileGenerator.collectTemplateTree(oldTemplatesRoot, oldContext),
|
|
56
|
+
this.fileGenerator.collectTemplateTree(newTemplatesRoot, newContext),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
excludeUserOwnedPaths(oldTree, newTree);
|
|
60
|
+
|
|
61
|
+
const toUpdate = [];
|
|
62
|
+
const toAdd = [];
|
|
63
|
+
const conflicts = [];
|
|
64
|
+
const unknown = [];
|
|
65
|
+
const removedByUser = [];
|
|
66
|
+
const noLongerGenerated = [];
|
|
67
|
+
|
|
68
|
+
for (const relPath of new Set([...oldTree.keys(), ...newTree.keys()])) {
|
|
69
|
+
const oldEntry = oldTree.get(relPath);
|
|
70
|
+
const newEntry = newTree.get(relPath);
|
|
71
|
+
|
|
72
|
+
if (oldEntry && newEntry && contentEquals(oldEntry.content, newEntry.content)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const outputPath = path.join(projectPath, relPath);
|
|
77
|
+
const existsOnDisk = await fs.pathExists(outputPath);
|
|
78
|
+
|
|
79
|
+
if (!oldEntry) {
|
|
80
|
+
// Brand new file introduced in the new version — nothing to conflict with,
|
|
81
|
+
// unless something else already occupies that path
|
|
82
|
+
if (!existsOnDisk) {
|
|
83
|
+
toAdd.push({ relPath, newEntry });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const actualContent = newEntry.isBinary
|
|
88
|
+
? await fs.readFile(outputPath)
|
|
89
|
+
: await fs.readFile(outputPath, 'utf8');
|
|
90
|
+
if (!contentEquals(actualContent, newEntry.content)) {
|
|
91
|
+
unknown.push({ relPath });
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!newEntry) {
|
|
97
|
+
// Nutin no longer generates this file; leave it alone either way,
|
|
98
|
+
// but warn if it's still on disk since it may now be stale/broken.
|
|
99
|
+
if (existsOnDisk) {
|
|
100
|
+
noLongerGenerated.push({ relPath });
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!existsOnDisk) {
|
|
106
|
+
removedByUser.push({ relPath });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const actualContent = oldEntry.isBinary
|
|
111
|
+
? await fs.readFile(outputPath)
|
|
112
|
+
: await fs.readFile(outputPath, 'utf8');
|
|
113
|
+
|
|
114
|
+
if (contentEquals(actualContent, oldEntry.content)) {
|
|
115
|
+
toUpdate.push({ relPath, newEntry });
|
|
116
|
+
} else if (!contentEquals(actualContent, newEntry.content)) {
|
|
117
|
+
// Differs from both baselines — a real conflict.
|
|
118
|
+
conflicts.push({ relPath, actualContent, newEntry });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import * as fsExtra from 'fs-extra';
|
|
3
|
+
import { getCiCommand, detectPackageManager } from '../common/package-json-helper.mjs';
|
|
4
|
+
import { PACKAGE_VERSION as packageVersion } from '../common/package-data.mjs';
|
|
5
|
+
|
|
6
|
+
const fs = fsExtra.default;
|
|
7
|
+
|
|
8
|
+
export class UpdateContextBuilder {
|
|
9
|
+
async buildContexts(projectPath, meta) {
|
|
10
|
+
const packageJson = await fs.readJSON(path.join(projectPath, 'package.json'));
|
|
11
|
+
const packageManager = meta.packageManager ?? (await detectPackageManager(projectPath));
|
|
12
|
+
|
|
13
|
+
const baseContext = {
|
|
14
|
+
projectName: packageJson.name,
|
|
15
|
+
packageManager,
|
|
16
|
+
...meta.features,
|
|
17
|
+
ciCommand: getCiCommand(packageManager),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
oldContext: { ...baseContext, version: meta.version },
|
|
22
|
+
newContext: { ...baseContext, version: packageVersion },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { print } from '../utils/print.mjs';
|
|
2
|
+
|
|
3
|
+
export function printUpdateSummary({ toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated }) {
|
|
4
|
+
print.section('\nSummary:');
|
|
5
|
+
print.info(` ${toUpdate.length} file(s) will be updated (untouched since generation)`);
|
|
6
|
+
print.info(` ${toAdd.length} new file(s) will be added`);
|
|
7
|
+
if (conflicts.length > 0) {
|
|
8
|
+
print.boldError(` ${conflicts.length} file(s) you modified need manual attention`);
|
|
9
|
+
}
|
|
10
|
+
if (noLongerGenerated.length > 0) {
|
|
11
|
+
print.boldError(` ${noLongerGenerated.length} file(s) nutin no longer generates — left in place, review manually:`);
|
|
12
|
+
noLongerGenerated.forEach(({ relPath }) => print.section(` - ${relPath}`));
|
|
13
|
+
}
|
|
14
|
+
if (unknown.length > 0) {
|
|
15
|
+
print.section(` ${unknown.length} file(s) have no baseline to compare against — left untouched:`);
|
|
16
|
+
unknown.forEach(({ relPath }) => print.section(` - ${relPath}`));
|
|
17
|
+
}
|
|
18
|
+
if (removedByUser.length > 0) {
|
|
19
|
+
print.section(` ${removedByUser.length} file(s) nutin used to generate but you removed — not recreated:`);
|
|
20
|
+
removedByUser.forEach(({ relPath }) => print.section(` - ${relPath}`));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function parseVersion(version) {
|
|
2
|
+
const [major, minor, patch] = version.split('.').map((n) => parseInt(n, 10) || 0);
|
|
3
|
+
return { major, minor, patch };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function compareVersions(a, b) {
|
|
7
|
+
const va = parseVersion(a);
|
|
8
|
+
const vb = parseVersion(b);
|
|
9
|
+
if (va.major !== vb.major) return va.major - vb.major;
|
|
10
|
+
if (va.minor !== vb.minor) return va.minor - vb.minor;
|
|
11
|
+
return va.patch - vb.patch;
|
|
12
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const chalk = {
|
|
2
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
3
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
4
|
+
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
5
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
6
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
7
|
+
purple: (s) => `\x1b[35m${s}\x1b[0m`,
|
|
8
|
+
gray: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
9
|
+
boldGreen: (s) => `\x1b[1;32m${s}\x1b[0m`,
|
|
10
|
+
boldRed: (s) => `\x1b[1;31m${s}\x1b[0m`,
|
|
11
|
+
boldBlue: (s) => `\x1b[1;34m${s}\x1b[0m`,
|
|
12
|
+
boldYellow: (s) => `\x1b[1;33m${s}\x1b[0m`,
|
|
13
|
+
boldCyan: (s) => `\x1b[1;36m${s}\x1b[0m`,
|
|
14
|
+
boldPurple: (s) => `\x1b[1;35m${s}\x1b[0m`,
|
|
15
|
+
boldGray: (s) => `\x1b[1;90m${s}\x1b[0m`,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const print = {
|
|
19
|
+
head: (s) => console.info(`${chalk.purple(s)}`),
|
|
20
|
+
boldHead : (s) => console.info(`${chalk.boldPurple(s)}`),
|
|
21
|
+
section: (s) => console.info(`${chalk.yellow(s)}`),
|
|
22
|
+
boldSection: (s) => console.info(`${chalk.boldYellow(s)}`),
|
|
23
|
+
info: (s) => console.info(`${chalk.green(s)}`),
|
|
24
|
+
boldInfo: (s) => console.info(`${chalk.boldGreen(s)}`),
|
|
25
|
+
success: (s) => console.log(`${chalk.cyan(s)}`),
|
|
26
|
+
boldSuccess: (s) => console.log(`${chalk.boldCyan(s)}`),
|
|
27
|
+
gray: (s) => console.info(`${chalk.gray(s)}`),
|
|
28
|
+
boldGray: (s) => console.info(`${chalk.boldGray(s)}`),
|
|
29
|
+
blue: (s) => console.info(`${chalk.blue(s)}`),
|
|
30
|
+
boldBlue: (s) => console.info(`${chalk.boldBlue(s)}`),
|
|
31
|
+
error: (s) => console.error(`${chalk.red(s)}`),
|
|
32
|
+
boldError: (s) => console.error(`${chalk.boldRed(s)}`),
|
|
33
|
+
grayError: (s) => console.error(`${chalk.boldGray(s)}`),
|
|
34
|
+
warn: (s) => console.warn(`${chalk.yellow(s)}`),
|
|
35
|
+
debug: (s) => console.debug(`${chalk.gray(s)}`),
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nutin/cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "The structure of a framework. The freedom of vanilla.",
|
|
5
5
|
"preferGlobal": true,
|
|
6
6
|
"main": "lib/index.mjs",
|
|
7
7
|
"bin": {
|
|
8
|
+
"cli": "./bin/cli.mjs",
|
|
9
|
+
"nutin": "./bin/cli.mjs",
|
|
8
10
|
"create-nutin-app": "./bin/create-app.mjs",
|
|
9
|
-
"nutin-new": "./bin/create-app.mjs"
|
|
11
|
+
"nutin-new": "./bin/create-app.mjs",
|
|
12
|
+
"nutin-add": "./bin/add-feature.mjs",
|
|
13
|
+
"nutin-update": "./bin/update-app.mjs"
|
|
10
14
|
},
|
|
11
15
|
"scripts": {
|
|
12
16
|
"patch": "npm version patch -m 'CI/CD: Bump version to %s'",
|
|
@@ -16,24 +20,25 @@
|
|
|
16
20
|
"publish": "npm publish --access public"
|
|
17
21
|
},
|
|
18
22
|
"keywords": [
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"Customizable"
|
|
23
|
+
"Nutin",
|
|
24
|
+
"Vanilla",
|
|
25
|
+
"Framework",
|
|
26
|
+
"Freedom",
|
|
27
|
+
"Structure",
|
|
28
|
+
"Ownership"
|
|
26
29
|
],
|
|
27
30
|
"author": "Loïs Kouninef <loiskouninef@gmail.com>",
|
|
28
31
|
"license": "MIT",
|
|
29
32
|
"dependencies": {
|
|
30
33
|
"commander": "^9.0.0",
|
|
34
|
+
"diff": "^9.0.0",
|
|
31
35
|
"fs-extra": "^11.0.0",
|
|
32
36
|
"handlebars": "^4.7.8",
|
|
33
|
-
"inquirer": "^9.0.0"
|
|
37
|
+
"inquirer": "^9.0.0",
|
|
38
|
+
"tar": "^7.5.22"
|
|
34
39
|
},
|
|
35
40
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
41
|
+
"node": ">=22"
|
|
37
42
|
},
|
|
38
43
|
"repository": {
|
|
39
44
|
"type": "git",
|
|
@@ -7,6 +7,11 @@ pnpm-debug.log*
|
|
|
7
7
|
|
|
8
8
|
/dist/
|
|
9
9
|
/dist-build/
|
|
10
|
+
/coverage/
|
|
11
|
+
|
|
12
|
+
# Generated by "validate-docker" (run via docker:build) from tools/docker/*.template — only present when the docker feature is added
|
|
13
|
+
/tools/docker/Dockerfile
|
|
14
|
+
/tools/docker/nginx.conf
|
|
10
15
|
|
|
11
16
|
.DS_Store
|
|
12
17
|
.DS_Store?
|
|
@@ -40,4 +45,4 @@ Desktop.ini
|
|
|
40
45
|
.vscode-test
|
|
41
46
|
|
|
42
47
|
tmp/
|
|
43
|
-
temp/
|
|
48
|
+
temp/
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Working on this project
|
|
2
|
+
|
|
3
|
+
This project was generated with Nutin {{version}}.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
app/ application code (components, views, services)
|
|
10
|
+
core/ Nutin framework internals — see "Nutin" section before editing
|
|
11
|
+
styles/ project-level Sass
|
|
12
|
+
config/ i18n (languages.json) and SEO configuration
|
|
13
|
+
tools/ builder, dev server, generator, testin-nutin runner, docker (optional)
|
|
14
|
+
nutin.config.js build/feature config — see below
|
|
15
|
+
.nutin-meta.json Nutin version, package manager, enabled features (source of truth for what's installed)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Conventions
|
|
19
|
+
|
|
20
|
+
- Favor small, composable components.
|
|
21
|
+
- One `View` = one route.
|
|
22
|
+
- Import everything from barrel files (`core/index.js`, `components/index.js`, `views/index.js`, `services/index.js`) — do not deep-import.
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
- Serve: `{{packageManager}} run serve` (build + serve), `{{packageManager}} run serve:only` (no rebuild), `{{packageManager}} run dev` (watch mode).
|
|
27
|
+
- Build: `{{packageManager}} run build` (dev) or `{{packageManager}} run build:prod` (production).
|
|
28
|
+
- Generate: `{{packageManager}} run generate ELEMENT ELEMENT_NAME` (`ELEMENT` = `component`/`view`/`service`, supports nested paths, updates the nearest `index.ts` barrel).
|
|
29
|
+
- Test: `{{packageManager}} run testin-nutin` (build + run), `{{packageManager}} run testin-nutin:only` (no rebuild), `{{packageManager}} run testin-nutin:watch`.
|
|
30
|
+
|
|
31
|
+
## Base classes
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Service<T> — app-wide singleton (router, event bus, i18n, http client, pipe registry, app-specific services)
|
|
35
|
+
BaseComponent<T> — render/destroy engine, data-* parsing, children
|
|
36
|
+
├── Component — reusable, non-routed UI; config/props + data-bind
|
|
37
|
+
└── View — one per route; mounted/unmounted by the Router
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `render()`: `onBeforeRender()` → sanitize + set `innerHTML` → mount children (`data-component`/`data-catalog`) → parse `data-i18n`/`data-pipe` → bind `data-event` listeners → `onAfterRender()`. Runs in full on every `render()` call — no virtual DOM/diffing.
|
|
41
|
+
- `destroy()`: unbinds events, unsubscribes bus listeners, destroys children, removes the element.
|
|
42
|
+
- `Service`: direct `new` throws — always use `MyService.getInstance()` (or the exported const). Auto-binds all methods to the instance. `registerCleanup(fn)` for teardown; `Service.destroyAll()` runs on `beforeunload`.
|
|
43
|
+
- Overridable hooks (all no-op by default): `onBeforeRender`, `onAfterRender`, `onBeforeDestroy`, `onAfterDestroy`, `generateTemplate()`, `registerChildren()`. `View` adds router-only `onEnter()`/`onExit()`.
|
|
44
|
+
|
|
45
|
+
## Children components
|
|
46
|
+
|
|
47
|
+
- Override `registerChildren(): ComponentConfig[]` to declare children — matched against `[data-component="selector"]` elements in the rendered template. ComponentConfig: `{ selector: 'my-selector', factory: (el) => new MyComponent(el, {...} )}`.
|
|
48
|
+
- For repeated lists, use the inherited `createCatalogComponents({ items, elementName, selector, component })` inside `registerChildren()` instead of hand-writing one entry per item — targets `[data-catalog="selector"]`.
|
|
49
|
+
|
|
50
|
+
## `data-*` attributes
|
|
51
|
+
|
|
52
|
+
| Attribute | Purpose |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `data-event="event:handler[:args]"` | Declarative event binding, see below. |
|
|
55
|
+
| `data-i18n="key"` | Sets translated text/placeholder via `I18nService`. |
|
|
56
|
+
| `data-pipe="name[:args]\|name2..."` | Pipes value/text through registered pipes, chainable with `\|`. |
|
|
57
|
+
| `data-pipe-source="..."` | Overrides the raw value fed into `data-pipe`. |
|
|
58
|
+
| `data-optional` | Removes the element post-render if it ends up empty (tag-specific check: `src`/`value`/`textContent`). |
|
|
59
|
+
| `data-component="selector"` | Child component mount point, from `registerChildren()`. |
|
|
60
|
+
| `data-catalog="selector"` | Repeated-children mount point, from `createCatalogComponents()`. |
|
|
61
|
+
| `data-bind="propKey"` | `Component`-only two-way field binding; read back with `getValues()`. |
|
|
62
|
+
|
|
63
|
+
## DOM events binding
|
|
64
|
+
|
|
65
|
+
Handlers are (re-)bound on every `render()`, not at build time. Always use private methods prefixed with underscore:
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<div data-event="event:_myFunction:@value,@id">
|
|
69
|
+
```
|
|
70
|
+
```ts
|
|
71
|
+
private _myFunction(value: string, id: string) { doStuff() }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Tokens: `@value`, `@id`, `@class`, `@textContent`, `@event`, `@target`, `@attr:name`, `@dataset:key`, plus string/number literals.
|
|
75
|
+
|
|
76
|
+
## Core services
|
|
77
|
+
|
|
78
|
+
- `AppEventBus` — `subscribe`/`once`/`emit`/`off`. Inside a component, prefer `this.listen(event, cb)` (auto-unsubscribes on `destroy()`) or `this.listenToRenderEvents([event1, event2, ...])` (re-runs `this.render()` whenever any of the given events fire) over calling the bus directly. Typed facades — each `on*` subscriber returns a closure you call directly to unsubscribe (`const off = Navigation.onNavigate(cb); ...; off();`), no need to keep the original callback reference around for `off()`:
|
|
79
|
+
- `Navigation` — `navigateTo(path)`/`onNavigate`, `reload()`/`onReload`.
|
|
80
|
+
- `Lifecycle` — `viewMount(viewName)`/`onViewMount`, `viewUnmount(viewName)`/`onViewUnmount`, plus generic `beforeRender`/`afterRender`/`beforeDestroy`/`afterDestroy`.
|
|
81
|
+
- `AppRouter(routes)` — call once in `main.ts`; routes support `:param`/`:param?` and per-route guards.
|
|
82
|
+
- `registerGlobals({ before?, after? })` — each entry is `{ component, id }` (a bare class, constructed internally); mounts them outside `<main id="app">` (`before`/`after` control placement relative to it), stamping each root element with `id`. `hideGlobals(ids)`/`revealGlobals(ids)` (same module) toggle visibility by those ids from any component or view. Call once in `main.ts`.
|
|
83
|
+
- `AppHttpClient` — `get/post/put/patch/delete<T>(endpoint, data?, config?)`; non-OK responses throw `HttpError`.
|
|
84
|
+
- `I18nService` — `translate(key, fallback?)`, `setCurrentLanguage(lang)`; language list comes from `config/languages.json`.
|
|
85
|
+
- `AppPipeRegistry` — `register(name, fn)` / `apply(name, value, args)`; backs `data-pipe`.
|
|
86
|
+
|
|
87
|
+
## Styling
|
|
88
|
+
|
|
89
|
+
- Sass is the primary styling system; Tailwind is optional (`nutin.config.js`'s `tailwind` flag).
|
|
90
|
+
- Naming convention: `.my-element__my-class`.
|
|
91
|
+
|
|
92
|
+
## Pipes
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
// currency
|
|
96
|
+
AppPipeRegistry.register('currency');
|
|
97
|
+
|
|
98
|
+
// date
|
|
99
|
+
AppPipeRegistry.register('date');
|
|
100
|
+
|
|
101
|
+
// number (toFixed)
|
|
102
|
+
AppPipeRegistry.register('number');
|
|
103
|
+
|
|
104
|
+
// string pipes
|
|
105
|
+
AppPipeRegistry.register('uppercase');
|
|
106
|
+
AppPipeRegistry.register('lowercase');
|
|
107
|
+
AppPipeRegistry.register('capitalize');
|
|
108
|
+
AppPipeRegistry.register('capitalizeAll');
|
|
109
|
+
AppPipeRegistry.register('truncate');
|
|
110
|
+
|
|
111
|
+
// utility pipes
|
|
112
|
+
AppPipeRegistry.register('default');
|
|
113
|
+
AppPipeRegistry.register('json');
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
### `nutin.config.js`
|
|
119
|
+
|
|
120
|
+
Central options/tools config (see the inline comments for details):
|
|
121
|
+
- `i18n`, `tailwind`, `generateSEO`, `dockerPorts` — top-level feature toggles (`dockerPorts` only applies once the docker feature is added).
|
|
122
|
+
- `builder.{sass.paths, esbuild}` — build pipeline behavior.
|
|
123
|
+
- `testinNutin.{includeFramework, includeTools, includeApp, jsdomOptions}` — test discovery/environment.
|
|
124
|
+
|
|
125
|
+
### `config/seo.json`
|
|
126
|
+
|
|
127
|
+
Consumed by builder (production only) to generate static HTML per route. Unused if `nutin.config.js -> generateSEO` is disabled.
|
|
128
|
+
|
|
129
|
+
### `config/languages.json`
|
|
130
|
+
|
|
131
|
+
Consumed by i18nService and builder. Unused if i18n is disabled.
|
|
132
|
+
|
|
133
|
+
## Security
|
|
134
|
+
|
|
135
|
+
`SecurityHelper.sanitizeTemplate()` runs on every `render()` before `innerHTML` is set — `trustLevel` (`'strict' | 'normal' | 'trusted'`, default `'normal'`) controls how much is stripped (`<script>`/inline handlers always; `<iframe>`/`javascript:`/`data:` under `strict` only). Use `SecurityHelper.escapeHtml()` when manually interpolating strings into templates.
|
|
136
|
+
|
|
137
|
+
## Testing (testin-nutin)
|
|
138
|
+
|
|
139
|
+
Nutin's own hand-built test runner + jsdom - not Jest. `describe`/`it` only (**no `test()` alias**), `beforeAll`/`beforeEach`/`afterEach`/`afterAll` (one of each per suite, no stacking - inside `describe` statement, not top-level). All globals — no imports needed in test files.
|
|
140
|
+
|
|
141
|
+
- Matchers: `toBe`, `toEqual`, `toContain`, `toBeTruthy`/`toBeFalsy`, `toBeDefined`/`toBeUndefined`, `toBeInstanceOf`, `toHaveBeenCalled`/`toHaveBeenCalledWith`, `toBeLessThan`/`toBeGreaterThan`, `toThrow` — each has a `.not` counterpart.
|
|
142
|
+
- `spyOn(obj, 'method')` returns `.calls`/`.callCount`/`.lastCall`, `.andCallFake(fn)`/`.andReturn(v)`, `.restore()` — unlike mocks, calls pass through by default.
|
|
143
|
+
- `tools/testin-nutin/mocks/` has ready-made `MockEventBus`/`MockHttpClient`/`MockI18n`/`MockRouter` for constructor injection.
|
|
144
|
+
- `useFakeTimers()`/`useRealTimers()`/`advanceTimersByTime(ms)`/`runAllTimers()` for time-dependent code.
|
|
145
|
+
- Test discovery/scope controlled by `nutin.config.js`'s `testinNutin.includeApp`/`includeFramework`.
|
|
146
|
+
|
|
147
|
+
## Nutin
|
|
148
|
+
|
|
149
|
+
Nutin provides the project tooling, build pipeline and generated structure. Its source code lives in `src/core/`.
|
|
150
|
+
|
|
151
|
+
Tests are an important source of documentation. They describe expected behavior and should be consulted before modifying existing functionality.
|
|
152
|
+
Before changing existing functionality, inspect the corresponding tests.
|
|
153
|
+
When adding behavior, add or update tests.
|
|
154
|
+
|
|
155
|
+
When modifying Nutin `core/` files:
|
|
156
|
+
|
|
157
|
+
1. Explore the alternatives: is modifying Nutin core files the best approach long-term? Always ask for validation.
|
|
158
|
+
2. Check the corresponding Nutin documentation and tests.
|
|
159
|
+
3. Preserve existing conventions.
|
|
160
|
+
4. Inspect existing implementations before creating new abstractions.
|
|
161
|
+
5. Reuse existing utilities/helpers where appropriate.
|
|
162
|
+
6. Do not rewrite generated configuration unnecessarily.
|
|
163
|
+
7. Keep changes focused on the requested feature.
|
|
164
|
+
8. Run tests.
|
|
165
|
+
|
|
166
|
+
## Documentation
|
|
167
|
+
|
|
168
|
+
[Nutin Documentation](https://github.com/LoisKOUNINEF/nutin/tree/main/docs)
|