@nutin/cli 1.3.0 → 2.0.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 +122 -8
- package/README.md +21 -37
- 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 +81 -30
- 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/create-app/new-app-prompt.mjs +51 -0
- package/lib/src/{project-generator.mjs → create-app/project-generator.mjs} +15 -13
- package/lib/src/create-app/success-message.mjs +14 -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 +136 -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 +169 -0
- package/templates/base/GETTING_STARTED.md.hbs +227 -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 +51 -0
- package/templates/base/src/app/components/index.ts.hbs +1 -2
- package/templates/base/src/app/globals.d.ts.hbs +3 -15
- package/templates/base/src/{libs/guards → app}/guards.ts.hbs +1 -5
- package/templates/base/src/app/main.ts.hbs +7 -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 +1 -56
- 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 +4 -37
- 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 +81 -31
- 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 +38 -23
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +74 -0
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +1 -1
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +145 -0
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +41 -1
- 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 +13 -14
- 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 +82 -0
- 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 +27 -40
- 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/config.helper.ts.hbs +17 -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 +74 -0
- package/templates/base/src/core/base-classes/view/view.ts.hbs +17 -41
- package/templates/base/src/core/config.ts.hbs +7 -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 +149 -0
- package/templates/base/src/core/services/globals/globals.ts.hbs +95 -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 +43 -0
- 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 +60 -9
- 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 +149 -6
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +42 -30
- 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 +13 -70
- package/templates/base/src/core/services/router/router.test.js.hbs +226 -2
- package/templates/base/src/core/services/router/router.ts.hbs +40 -9
- 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 -85
- 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 +64 -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 +4 -0
- package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +7 -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} +3 -2
- 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 -35
- 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 -31
- package/lib/src/prompts.mjs +0 -123
- package/lib/src/utils.mjs +0 -18
- package/lib/src/version.mjs +0 -1
- package/templates/base/builder.config.js.hbs +0 -35
- 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 -85
- 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 -108
- 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/popover.ts.hbs +0 -145
- package/templates/base/src/libs/snackbar/snackbar.ts.hbs +0 -72
- package/templates/base/src/styles/base/_functions.scss.hbs +0 -9
- package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
- 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 -240
- 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
|
@@ -12,20 +12,26 @@ export const chalk = {
|
|
|
12
12
|
boldYellow: (s) => `\x1b[1;33m${s}\x1b[0m`,
|
|
13
13
|
boldCyan: (s) => `\x1b[1;36m${s}\x1b[0m`,
|
|
14
14
|
boldPurple: (s) => `\x1b[1;35m${s}\x1b[0m`,
|
|
15
|
+
boldGray: (s) => `\x1b[1;90m${s}\x1b[0m`,
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export const print = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
boldInfo: (s) => console.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
boldWarn: (s) => console.warn(`${chalk.yellow(s)}`),
|
|
36
|
+
debug: (s) => console.debug(`${chalk.gray(s)}`),
|
|
31
37
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { print, chalk } from './print.js';
|
|
2
|
+
|
|
3
|
+
describe('print', () => {
|
|
4
|
+
let infoSpy;
|
|
5
|
+
let logSpy;
|
|
6
|
+
let errorSpy;
|
|
7
|
+
let warnSpy;
|
|
8
|
+
let debugSpy;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
infoSpy = spyOn(console, 'info').andCallFake(() => {});
|
|
12
|
+
logSpy = spyOn(console, 'log').andCallFake(() => {});
|
|
13
|
+
errorSpy = spyOn(console, 'error').andCallFake(() => {});
|
|
14
|
+
warnSpy = spyOn(console, 'warn').andCallFake(() => {});
|
|
15
|
+
debugSpy = spyOn(console, 'debug').andCallFake(() => {});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
infoSpy.restore();
|
|
20
|
+
logSpy.restore();
|
|
21
|
+
errorSpy.restore();
|
|
22
|
+
warnSpy.restore();
|
|
23
|
+
debugSpy.restore();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('info() writes to console.info, colorized green', () => {
|
|
27
|
+
print.info('hello');
|
|
28
|
+
expect(infoSpy.callCount).toBe(1);
|
|
29
|
+
expect(infoSpy.lastCall).toEqual([chalk.green('hello')]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('success() writes to console.log, colorized cyan', () => {
|
|
33
|
+
print.success('done');
|
|
34
|
+
expect(logSpy.callCount).toBe(1);
|
|
35
|
+
expect(logSpy.lastCall).toEqual([chalk.cyan('done')]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('error() writes to console.error, colorized red', () => {
|
|
39
|
+
print.error('oops');
|
|
40
|
+
expect(errorSpy.callCount).toBe(1);
|
|
41
|
+
expect(errorSpy.lastCall).toEqual([chalk.red('oops')]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('warn() writes to console.warn, colorized yellow', () => {
|
|
45
|
+
print.warn('careful');
|
|
46
|
+
expect(warnSpy.callCount).toBe(1);
|
|
47
|
+
expect(warnSpy.lastCall).toEqual([chalk.yellow('careful')]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('debug() writes to console.debug, colorized gray', () => {
|
|
51
|
+
print.debug('trace');
|
|
52
|
+
expect(debugSpy.callCount).toBe(1);
|
|
53
|
+
expect(debugSpy.lastCall).toEqual([chalk.gray('trace')]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('chalk wraps text in the matching ANSI escape codes', () => {
|
|
57
|
+
expect(chalk.red('x')).toBe('\x1b[31mx\x1b[0m');
|
|
58
|
+
expect(chalk.boldGreen('x')).toBe('\x1b[1;32mx\x1b[0m');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -7,9 +7,16 @@ export const promptBoolean = (question) => {
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
return new Promise((resolve) => {
|
|
10
|
+
let answered = false;
|
|
11
|
+
|
|
10
12
|
rl.question(`${question} (y/n): `, (answer) => {
|
|
13
|
+
answered = true;
|
|
11
14
|
rl.close();
|
|
12
15
|
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
13
16
|
});
|
|
17
|
+
|
|
18
|
+
rl.on('close', () => {
|
|
19
|
+
if (!answered) resolve(false);
|
|
20
|
+
});
|
|
14
21
|
});
|
|
15
22
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { promptBoolean } from './prompt-boolean.js';
|
|
3
|
+
|
|
4
|
+
describe('promptBoolean', () => {
|
|
5
|
+
let createInterfaceSpy;
|
|
6
|
+
let answer;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
createInterfaceSpy = spyOn(readline, 'createInterface').andCallFake(() => ({
|
|
10
|
+
question: (_question, callback) => callback(answer),
|
|
11
|
+
close: () => {},
|
|
12
|
+
on: () => {},
|
|
13
|
+
}));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
createInterfaceSpy.restore();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('resolves true for "y"', async () => {
|
|
21
|
+
answer = 'y';
|
|
22
|
+
expect(await promptBoolean('continue?')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('resolves true for "yes", case-insensitively', async () => {
|
|
26
|
+
answer = 'YES';
|
|
27
|
+
expect(await promptBoolean('continue?')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('resolves false for "n"', async () => {
|
|
31
|
+
answer = 'n';
|
|
32
|
+
expect(await promptBoolean('continue?')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('resolves false for anything else', async () => {
|
|
36
|
+
answer = 'maybe';
|
|
37
|
+
expect(await promptBoolean('continue?')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('resolves false instead of hanging if stdin closes before an answer', async () => {
|
|
41
|
+
createInterfaceSpy.restore();
|
|
42
|
+
let closeHandler;
|
|
43
|
+
createInterfaceSpy = spyOn(readline, 'createInterface').andCallFake(() => ({
|
|
44
|
+
question: () => {},
|
|
45
|
+
close: () => {},
|
|
46
|
+
on: (event, handler) => { if (event === 'close') closeHandler = handler; },
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const resultPromise = promptBoolean('continue?');
|
|
50
|
+
closeHandler();
|
|
51
|
+
|
|
52
|
+
expect(await resultPromise).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
|
|
3
|
-
export function runCommand(command, args = []) {
|
|
3
|
+
export function runCommand(command, args = [], options = {}) {
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
|
-
const child = spawn(command, args, { stdio: 'inherit', shell: true });
|
|
5
|
+
const child = spawn([command, ...args].join(' '), { stdio: 'inherit', shell: true, ...options });
|
|
6
|
+
|
|
7
|
+
child.on('error', (err) => reject(err));
|
|
6
8
|
|
|
7
9
|
child.on('close', (code) => {
|
|
8
10
|
if (code === 0) resolve();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { runCommand } from './run-command.js';
|
|
2
|
+
|
|
3
|
+
describe('runCommand', () => {
|
|
4
|
+
it('resolves when the child process exits with code 0', async () => {
|
|
5
|
+
const result = await runCommand('node', ['-e', 'process.exitCode=0']);
|
|
6
|
+
expect(result).toBe(undefined);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('rejects with the exit code when the child process exits non-zero', async () => {
|
|
10
|
+
let error;
|
|
11
|
+
try {
|
|
12
|
+
await runCommand('node', ['-e', 'process.exitCode=7']);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
error = err;
|
|
15
|
+
}
|
|
16
|
+
expect(error).toBeDefined();
|
|
17
|
+
expect(error.message).toContain('7');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -4,17 +4,28 @@ import process from 'process';
|
|
|
4
4
|
import { print } from './index.js';
|
|
5
5
|
|
|
6
6
|
export function runScript(path, message) {
|
|
7
|
-
print.boldSection(
|
|
7
|
+
if (!message) print.boldSection(`Running ${path}...`);
|
|
8
|
+
else print.boldSection(`${message}`);
|
|
8
9
|
|
|
9
|
-
const args = [resolve(path)];
|
|
10
|
+
const args = [resolve(path), '--silent'];
|
|
10
11
|
|
|
11
|
-
const result = spawnSync('node', args,
|
|
12
|
+
const result = spawnSync('node', args,
|
|
12
13
|
{
|
|
13
14
|
stdio: 'inherit',
|
|
14
15
|
env: { ...process.env },
|
|
15
16
|
}
|
|
16
17
|
);
|
|
17
18
|
|
|
19
|
+
if (result.error) {
|
|
20
|
+
print.boldError(`\nScript ${path} could not be started: ${result.error.message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (result.signal) {
|
|
25
|
+
print.boldError(`\nScript ${path} was terminated by signal ${result.signal}.`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
if (result.status !== 0) {
|
|
19
30
|
print.boldError(`\nScript ${path} failed.`);
|
|
20
31
|
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { runScript } from './run-script.js';
|
|
5
|
+
|
|
6
|
+
describe('runScript', () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
let exitSpy;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nutin-run-script-'));
|
|
12
|
+
exitSpy = spyOn(process, 'exit').andCallFake(() => {});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
exitSpy.restore();
|
|
17
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('does not call process.exit when the script exits 0', () => {
|
|
21
|
+
const scriptPath = path.join(tmpDir, 'ok.js');
|
|
22
|
+
fs.writeFileSync(scriptPath, '// no-op, exits 0 by default\n');
|
|
23
|
+
|
|
24
|
+
// runScript prints its status message via console.info as part of its real behavior
|
|
25
|
+
silenceConsole('info', () => runScript(scriptPath, 'running ok script'));
|
|
26
|
+
|
|
27
|
+
expect(exitSpy.callCount).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('calls process.exit with the script status when it exits non-zero', () => {
|
|
31
|
+
const scriptPath = path.join(tmpDir, 'fail.js');
|
|
32
|
+
fs.writeFileSync(scriptPath, 'process.exitCode = 3;\n');
|
|
33
|
+
|
|
34
|
+
// runScript prints its status message and a failure message via console.info/console.error
|
|
35
|
+
silenceConsole(['info', 'error'], () => runScript(scriptPath, 'running failing script'));
|
|
36
|
+
|
|
37
|
+
expect(exitSpy.callCount).toBe(1);
|
|
38
|
+
expect(exitSpy.lastCall).toEqual([3]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
ARG NODE_VERSION=22.23.2
|
|
2
|
+
ARG ALPINE_VERSION=3.20
|
|
3
|
+
ARG PORT="__PORTS_PLACEHOLDER__"
|
|
4
|
+
|
|
5
|
+
FROM node:${NODE_VERSION}-alpine AS builder
|
|
6
|
+
WORKDIR /usr/src/app
|
|
7
|
+
RUN corepack enable
|
|
8
|
+
{{#if (eq packageManager "npm")}}
|
|
9
|
+
COPY package.json package-lock.json* .
|
|
10
|
+
{{/if}}
|
|
11
|
+
{{#if (eq packageManager "pnpm")}}
|
|
12
|
+
COPY package.json pnpm-lock.yaml .
|
|
13
|
+
{{/if}}
|
|
14
|
+
{{#if (eq packageManager "yarn")}}
|
|
15
|
+
COPY package.json yarn.lock .
|
|
16
|
+
{{/if}}
|
|
17
|
+
{{#if (eq packageManager "bun")}}
|
|
18
|
+
COPY package.json bun.lock* bun.lockb* .
|
|
19
|
+
RUN npm install -g bun
|
|
20
|
+
{{/if}}
|
|
21
|
+
{{#if (eq packageManager "pnpm")}}
|
|
22
|
+
RUN {{ciCommand}} || pnpm approve-builds --all
|
|
23
|
+
{{else}}
|
|
24
|
+
RUN {{ciCommand}}
|
|
25
|
+
{{/if}}
|
|
26
|
+
COPY tsconfig.json .
|
|
27
|
+
COPY ./src ./src
|
|
28
|
+
COPY ./public ./public
|
|
29
|
+
COPY ./nutin.config.js .
|
|
30
|
+
COPY ./config ./config
|
|
31
|
+
COPY ./tools/builder ./tools/builder
|
|
32
|
+
COPY ./tools/docker ./tools/docker
|
|
33
|
+
COPY ./tools/utils ./tools/utils
|
|
34
|
+
RUN {{packageManager}} run build:prod
|
|
35
|
+
|
|
36
|
+
FROM alpine:${ALPINE_VERSION}
|
|
37
|
+
ARG PORT
|
|
38
|
+
ENV PORT=${PORT}
|
|
39
|
+
|
|
40
|
+
RUN apk add --no-cache nginx nginx-mod-http-brotli
|
|
41
|
+
|
|
42
|
+
RUN rm -f /etc/nginx/http.d/default.conf
|
|
43
|
+
COPY --from=builder /usr/src/app/tools/docker/nginx.conf /etc/nginx/http.d/default.conf
|
|
44
|
+
COPY --from=builder /usr/src/app/dist/src /usr/share/nginx/html
|
|
45
|
+
|
|
46
|
+
EXPOSE ${PORT}
|
|
47
|
+
|
|
48
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
49
|
+
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:__PRIMARY_PORT_PLACEHOLDER__/health || exit 1
|
|
50
|
+
|
|
51
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
@@ -30,14 +30,16 @@ map $uri $csp_policy {
|
|
|
30
30
|
# ---------------------------
|
|
31
31
|
gzip on;
|
|
32
32
|
gzip_static on;
|
|
33
|
-
gzip_vary on;
|
|
34
33
|
gzip_proxied any;
|
|
35
34
|
|
|
35
|
+
brotli on;
|
|
36
|
+
brotli_static on;
|
|
37
|
+
|
|
36
38
|
server {
|
|
37
39
|
# ---------------------------
|
|
38
40
|
# LISTEN PORTS
|
|
39
41
|
# ---------------------------
|
|
40
|
-
|
|
42
|
+
__PORTS_PLACEHOLDER__
|
|
41
43
|
server_name _;
|
|
42
44
|
|
|
43
45
|
# ---------------------------
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { print, runScript } from '../../utils/index.js';
|
|
7
|
+
|
|
8
|
+
const { name: projectName } = JSON.parse(readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
|
|
9
|
+
|
|
10
|
+
runScript(path.join(process.cwd(), 'tools', 'docker', 'scripts', 'validate-docker.js'), 'Validating Docker configuration...');
|
|
11
|
+
|
|
12
|
+
const args = ['build', '-t', projectName, '-f', 'tools/docker/Dockerfile', '.'];
|
|
13
|
+
print.boldSection(`Running: docker ${args.join(' ')}`);
|
|
14
|
+
|
|
15
|
+
const child = spawn('docker', args, { stdio: 'inherit' });
|
|
16
|
+
child.on('error', (err) => {
|
|
17
|
+
print.boldError(`Failed to start docker: ${err.message}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
child.on('exit', (code) => process.exit(code ?? 1));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Registered/user port range only — excludes privileged ports (<1024, may need
|
|
2
|
+
// elevated host privileges to bind) and the OS ephemeral range (>=49152, reserved
|
|
3
|
+
// for outgoing/dynamic connections, not meant for fixed service ports).
|
|
4
|
+
const MIN_PORT = 1024;
|
|
5
|
+
const MAX_PORT = 49151;
|
|
6
|
+
|
|
7
|
+
export function validateDockerPorts(rawPorts) {
|
|
8
|
+
if (rawPorts === undefined) {
|
|
9
|
+
throw new Error('nutin.config.js: "dockerPorts" not found. It must be an array of integers (e.g. [9090, 6969])');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!Array.isArray(rawPorts)) {
|
|
13
|
+
throw new Error('nutin.config.js: "dockerPorts" must be an array of integers (e.g. [9090, 6969]).');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (rawPorts.length === 0) {
|
|
17
|
+
throw new Error('nutin.config.js: "dockerPorts" must contain at least one port (e.g. [9090]).');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < rawPorts.length; i++) {
|
|
23
|
+
const port = rawPorts[i];
|
|
24
|
+
|
|
25
|
+
if (typeof port !== 'number' || !Number.isInteger(port)) {
|
|
26
|
+
throw new Error(`nutin.config.js: "dockerPorts[${i}]" must be an integer; ports must be numbers, e.g. 9090 not "9090".`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (port < MIN_PORT) {
|
|
30
|
+
throw new Error(`nutin.config.js: "dockerPorts[${i}]" (${port}) is a privileged port (<1024) — use a registered port between ${MIN_PORT} and ${MAX_PORT}.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (port > MAX_PORT) {
|
|
34
|
+
throw new Error(`nutin.config.js: "dockerPorts[${i}]" (${port}) is in the OS ephemeral port range (49152-65535) — use a registered port between ${MIN_PORT} and ${MAX_PORT}.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (port < MIN_PORT || port > MAX_PORT) {
|
|
38
|
+
throw new Error(`nutin.config.js: "dockerPorts[${i}]" (${port}) is outside of port range (${MIN_PORT} / ${MAX_PORT} .`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (seen.has(port)) {
|
|
42
|
+
throw new Error(`nutin.config.js: "dockerPorts" contains duplicate port ${port} — each port must be listed once.`);
|
|
43
|
+
}
|
|
44
|
+
seen.add(port);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return rawPorts;
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { validateDockerPorts } from './docker-ports.js';
|
|
2
|
+
|
|
3
|
+
describe('validateDockerPorts', () => {
|
|
4
|
+
it('throws when dockerPorts is not set', () => {
|
|
5
|
+
expect(() => validateDockerPorts(undefined)).toThrow('"dockerPorts" not found');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('throws when dockerPorts is not an array', () => {
|
|
9
|
+
expect(() => validateDockerPorts(9090)).toThrow('must be an array of integers');
|
|
10
|
+
expect(() => validateDockerPorts('9090')).toThrow('must be an array of integers');
|
|
11
|
+
expect(() => validateDockerPorts({ port: 9090 })).toThrow('must be an array of integers');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('throws when dockerPorts is empty', () => {
|
|
15
|
+
expect(() => validateDockerPorts([])).toThrow('must contain at least one port');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('throws when an entry is not an integer', () => {
|
|
19
|
+
expect(() => validateDockerPorts(['8080'])).toThrow('must be an integer');
|
|
20
|
+
expect(() => validateDockerPorts([8080.5])).toThrow('must be an integer');
|
|
21
|
+
expect(() => validateDockerPorts([NaN])).toThrow('must be an integer');
|
|
22
|
+
expect(() => validateDockerPorts([true])).toThrow('must be an integer');
|
|
23
|
+
expect(() => validateDockerPorts([null])).toThrow('must be an integer');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('throws for privileged ports below 1024', () => {
|
|
27
|
+
expect(() => validateDockerPorts([80])).toThrow('privileged port');
|
|
28
|
+
expect(() => validateDockerPorts([1023])).toThrow('privileged port');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('throws for ephemeral ports at or above 49152', () => {
|
|
32
|
+
expect(() => validateDockerPorts([49152])).toThrow('ephemeral port range');
|
|
33
|
+
expect(() => validateDockerPorts([60000])).toThrow('ephemeral port range');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('throws for duplicate ports', () => {
|
|
37
|
+
expect(() => validateDockerPorts([8080, 8080])).toThrow('duplicate port');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('identifies the invalid entry by index in the error message', () => {
|
|
41
|
+
expect(() => validateDockerPorts([8080, 80])).toThrow('dockerPorts[1]');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('accepts the registered-port range boundaries', () => {
|
|
45
|
+
expect(validateDockerPorts([1024])).toEqual([1024]);
|
|
46
|
+
expect(validateDockerPorts([49151])).toEqual([49151]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns a valid multi-port array unchanged', () => {
|
|
50
|
+
expect(validateDockerPorts([8080, 8081, 3000])).toEqual([8080, 8081, 3000]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { print, errorExit } from '../../utils/index.js';
|
|
7
|
+
import nutinConfig from '../../../nutin.config.js';
|
|
8
|
+
import { validateDockerPorts } from './docker-ports.js';
|
|
9
|
+
|
|
10
|
+
const { name: projectName } = JSON.parse(readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
|
|
11
|
+
|
|
12
|
+
let ports;
|
|
13
|
+
try {
|
|
14
|
+
ports = validateDockerPorts(nutinConfig.dockerPorts);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
errorExit(err.message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const portArgs = ports.flatMap((port) => ['-p', `${port}:${port}`]);
|
|
20
|
+
const args = ['run', ...portArgs, `${projectName}:latest`];
|
|
21
|
+
print.boldSection(`Running: docker ${args.join(' ')}`);
|
|
22
|
+
|
|
23
|
+
const child = spawn('docker', args, { stdio: 'inherit' });
|
|
24
|
+
child.on('error', (err) => {
|
|
25
|
+
print.boldError(`Failed to start docker: ${err.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
|
28
|
+
child.on('exit', (code) => process.exit(code ?? 1));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function renderFromTemplate(dockerDir, templateName, outputName, replacements) {
|
|
5
|
+
const templatePath = path.join(dockerDir, templateName);
|
|
6
|
+
const outputPath = path.join(dockerDir, outputName);
|
|
7
|
+
|
|
8
|
+
let content;
|
|
9
|
+
try {
|
|
10
|
+
content = await readFile(templatePath, 'utf-8');
|
|
11
|
+
} catch {
|
|
12
|
+
throw new Error(`tools/docker/${templateName} not found — re-run "nutin-add docker" or restore the file.`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for (const [placeholder, value] of replacements) {
|
|
16
|
+
if (!content.includes(placeholder)) {
|
|
17
|
+
throw new Error(`tools/docker/${templateName} is missing the "${placeholder}" token — validate-docker has nothing to substitute. Restore the token or re-run "nutin-add docker".`);
|
|
18
|
+
}
|
|
19
|
+
content = content.replace(placeholder, value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await writeFile(outputPath, content);
|
|
23
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { renderFromTemplate } from './render-docker-template.js';
|
|
5
|
+
|
|
6
|
+
describe('renderFromTemplate', () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nutin-render-docker-template-'));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('substitutes a single placeholder and writes the output file', async () => {
|
|
18
|
+
fs.writeFileSync(path.join(tmpDir, 'nginx.conf.template'), '__PORTS_PLACEHOLDER__;\n');
|
|
19
|
+
|
|
20
|
+
await renderFromTemplate(tmpDir, 'nginx.conf.template', 'nginx.conf', [
|
|
21
|
+
['__PORTS_PLACEHOLDER__', 'listen 8080;'],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const output = fs.readFileSync(path.join(tmpDir, 'nginx.conf'), 'utf-8');
|
|
25
|
+
expect(output).toBe('listen 8080;;\n');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('substitutes multiple placeholders in one call', async () => {
|
|
29
|
+
fs.writeFileSync(
|
|
30
|
+
path.join(tmpDir, 'Dockerfile.template'),
|
|
31
|
+
'ARG PORT="__PORTS_PLACEHOLDER__"\nCMD wget http://127.0.0.1:__PRIMARY_PORT_PLACEHOLDER__/health\n'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
await renderFromTemplate(tmpDir, 'Dockerfile.template', 'Dockerfile', [
|
|
35
|
+
['__PORTS_PLACEHOLDER__', '8080 8081 3000'],
|
|
36
|
+
['__PRIMARY_PORT_PLACEHOLDER__', '8080'],
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const output = fs.readFileSync(path.join(tmpDir, 'Dockerfile'), 'utf-8');
|
|
40
|
+
expect(output).toBe('ARG PORT="8080 8081 3000"\nCMD wget http://127.0.0.1:8080/health\n');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('throws when the template file does not exist', async () => {
|
|
44
|
+
await expect(() =>
|
|
45
|
+
renderFromTemplate(tmpDir, 'missing.template', 'missing', [['__X__', 'y']])
|
|
46
|
+
).toThrow('missing.template');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('throws when a placeholder token is missing from the template, without writing the output', async () => {
|
|
50
|
+
fs.writeFileSync(path.join(tmpDir, 'nginx.conf.template'), 'listen 9090;\n');
|
|
51
|
+
|
|
52
|
+
await expect(() =>
|
|
53
|
+
renderFromTemplate(tmpDir, 'nginx.conf.template', 'nginx.conf', [
|
|
54
|
+
['__PORTS_PLACEHOLDER__', 'listen 8080;'],
|
|
55
|
+
])
|
|
56
|
+
).toThrow('__PORTS_PLACEHOLDER__');
|
|
57
|
+
|
|
58
|
+
expect(fs.existsSync(path.join(tmpDir, 'nginx.conf'))).toBeFalsy();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { errorExit } from '../../utils/index.js';
|
|
3
|
+
import nutinConfig from '../../../nutin.config.js';
|
|
4
|
+
import { validateDockerPorts } from './docker-ports.js';
|
|
5
|
+
import { renderFromTemplate } from './render-docker-template.js';
|
|
6
|
+
|
|
7
|
+
const DOCKER_DIR = path.join(process.cwd(), 'tools', 'docker');
|
|
8
|
+
const PLACEHOLDER = '__PORTS_PLACEHOLDER__';
|
|
9
|
+
|
|
10
|
+
async function validateDocker() {
|
|
11
|
+
let ports;
|
|
12
|
+
try {
|
|
13
|
+
ports = validateDockerPorts(nutinConfig.dockerPorts);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
errorExit(err.message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await renderFromTemplate(DOCKER_DIR, 'Dockerfile.template', 'Dockerfile', [
|
|
19
|
+
[PLACEHOLDER, ports.join(' ')],
|
|
20
|
+
['__PRIMARY_PORT_PLACEHOLDER__', String(ports[0])],
|
|
21
|
+
]).catch((err) => errorExit(err.message));
|
|
22
|
+
|
|
23
|
+
await renderFromTemplate(DOCKER_DIR, 'nginx.conf.template', 'nginx.conf', [
|
|
24
|
+
[PLACEHOLDER, ports.map((port) => `listen ${port};`).join('\n ')],
|
|
25
|
+
]).catch((err) => errorExit(err.message));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
validateDocker().catch((err) => {
|
|
29
|
+
errorExit(err, 'validate-docker');
|
|
30
|
+
});
|