@nutin/cli 1.3.1 → 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 +89 -9
- package/README.md +20 -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 +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 -19
- 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 +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 +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 +7 -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 +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 +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 +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 +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 +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 +149 -6
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +40 -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 +226 -2
- package/templates/base/src/core/services/router/router.ts.hbs +40 -8
- 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 +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 -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,152 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getFilesRecursive, print, pascalCased, capitalized } from '../../../utils/index.js';
|
|
4
|
+
import { PATHS } from '../app/paths.js';
|
|
5
|
+
import { minifyHTML } from './minify-html.js';
|
|
6
|
+
|
|
7
|
+
const PLACEHOLDER = '__TEMPLATE_PLACEHOLDER__';
|
|
8
|
+
const TARGET_EXTENSIONS = ['.ts', '.js'];
|
|
9
|
+
const TEMPLATE_REGEX = /const\s+(?:template|templateFn)\s*=?\s*(?:\(.*?\)\s*=>\s*)?`([\s\S]*?)`/;
|
|
10
|
+
|
|
11
|
+
// A component/view's template can live inline in its .ts/.js file or in a
|
|
12
|
+
// colocated .html sibling merged in via `__TEMPLATE_PLACEHOLDER__` — resolved
|
|
13
|
+
// automatically per file rather than through a global config flag:
|
|
14
|
+
// external + placeholder -> merge external in
|
|
15
|
+
// external + real inline markup -> conflict, fail
|
|
16
|
+
// no external + real inline markup -> minify inline
|
|
17
|
+
// no external + still the placeholder -> nothing to use, fail
|
|
18
|
+
export async function resolveTemplates() {
|
|
19
|
+
const htmlFiles = await getFilesRecursive(PATHS.tempApp, 'html');
|
|
20
|
+
const resolvedPaths = new Set();
|
|
21
|
+
const failures = [];
|
|
22
|
+
|
|
23
|
+
for (const htmlPath of htmlFiles) {
|
|
24
|
+
await resolveExternalTemplate(htmlPath, resolvedPaths, failures);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await resolveRemainingTemplates(resolvedPaths, failures);
|
|
28
|
+
|
|
29
|
+
if (failures.length) {
|
|
30
|
+
throw new Error(`Failed to resolve ${failures.length} template(s).`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Normally a view/component has exactly one sibling here — `.js` in dev (tsc
|
|
35
|
+
// output), `.ts` in prod (esbuild bundles straight from source, tsc runs
|
|
36
|
+
// --noEmit there). Checking whichever exist(s) rather than assuming one, so
|
|
37
|
+
// this can't silently regress again if that dev/prod split ever changes.
|
|
38
|
+
async function resolveExternalTemplate(htmlPath, resolvedPaths, failures) {
|
|
39
|
+
const htmlFilename = path.basename(htmlPath);
|
|
40
|
+
|
|
41
|
+
let htmlContent;
|
|
42
|
+
try {
|
|
43
|
+
htmlContent = await fs.readFile(htmlPath, 'utf-8');
|
|
44
|
+
} catch (err) {
|
|
45
|
+
print.warn(`Failed to read "${htmlFilename}". ${err.message}`);
|
|
46
|
+
failures.push(htmlFilename);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const minifiedHtml = await minifyHTML(htmlContent);
|
|
51
|
+
let mergedCount = 0;
|
|
52
|
+
let conflictCount = 0;
|
|
53
|
+
|
|
54
|
+
for (const extension of TARGET_EXTENSIONS) {
|
|
55
|
+
const codePath = htmlPath.replace(/\.html$/, extension);
|
|
56
|
+
|
|
57
|
+
let content;
|
|
58
|
+
try {
|
|
59
|
+
content = await fs.readFile(codePath, 'utf-8');
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err.code === 'ENOENT') continue;
|
|
62
|
+
print.warn(`Cannot update "${path.basename(codePath)}". ${err.message}`);
|
|
63
|
+
failures.push(path.basename(codePath));
|
|
64
|
+
resolvedPaths.add(codePath);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
resolvedPaths.add(codePath);
|
|
69
|
+
|
|
70
|
+
if (content.includes(PLACEHOLDER)) {
|
|
71
|
+
await fs.writeFile(codePath, content.replace(PLACEHOLDER, minifiedHtml));
|
|
72
|
+
mergedCount++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
print.warn(describeConflict(codePath));
|
|
77
|
+
failures.push(path.basename(codePath));
|
|
78
|
+
conflictCount++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (mergedCount > 0) {
|
|
82
|
+
await fs.unlink(htmlPath);
|
|
83
|
+
} else if (mergedCount + conflictCount === 0) {
|
|
84
|
+
print.warn(describeOrphanHtml(htmlPath));
|
|
85
|
+
failures.push(htmlFilename);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function resolveRemainingTemplates(resolvedPaths, failures) {
|
|
90
|
+
const codeFiles = await getFilesRecursive(PATHS.tempApp, TARGET_EXTENSIONS);
|
|
91
|
+
|
|
92
|
+
for (const codePath of codeFiles) {
|
|
93
|
+
if (resolvedPaths.has(codePath)) continue;
|
|
94
|
+
|
|
95
|
+
const content = await fs.readFile(codePath, 'utf-8');
|
|
96
|
+
const match = content.match(TEMPLATE_REGEX);
|
|
97
|
+
if (!match) continue;
|
|
98
|
+
|
|
99
|
+
const [full, body] = match;
|
|
100
|
+
|
|
101
|
+
if (body === PLACEHOLDER) {
|
|
102
|
+
print.warn(describeMissingTemplate(codePath));
|
|
103
|
+
failures.push(path.basename(codePath));
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const minified = await minifyHTML(body);
|
|
108
|
+
await fs.writeFile(codePath, content.replace(full, full.replace(body, minified)));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function describeCodeFile(codePath) {
|
|
113
|
+
const filename = path.basename(codePath);
|
|
114
|
+
const basename = filename.replace(/\.(ts|js)$/, '');
|
|
115
|
+
const lastDot = basename.lastIndexOf('.');
|
|
116
|
+
const namePart = lastDot === -1 ? basename : basename.slice(0, lastDot);
|
|
117
|
+
const suffix = lastDot === -1 ? '' : basename.slice(lastDot + 1);
|
|
118
|
+
const kind = suffix ? capitalized(suffix) : 'Component';
|
|
119
|
+
const className = `${pascalCased(namePart)}${kind}`;
|
|
120
|
+
|
|
121
|
+
return { filename, basename, kind, className };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function describeConflict(codePath) {
|
|
125
|
+
const { filename, basename, kind, className } = describeCodeFile(codePath);
|
|
126
|
+
|
|
127
|
+
return `${kind} "${className}" has both an inline template and an external template.
|
|
128
|
+
|
|
129
|
+
Use either:
|
|
130
|
+
${filename}
|
|
131
|
+
or:
|
|
132
|
+
${filename} (template placeholder) + ${basename}.html
|
|
133
|
+
Remove one of the template definitions.`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function describeMissingTemplate(codePath) {
|
|
137
|
+
const { filename, basename, kind, className } = describeCodeFile(codePath);
|
|
138
|
+
|
|
139
|
+
return `${kind} "${className}" has a template placeholder but no external template.
|
|
140
|
+
|
|
141
|
+
Expected:
|
|
142
|
+
${filename} (template placeholder) + ${basename}.html
|
|
143
|
+
Add ${basename}.html, or replace the placeholder in ${filename} with inline markup.`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function describeOrphanHtml(htmlPath) {
|
|
147
|
+
const htmlFilename = path.basename(htmlPath);
|
|
148
|
+
const basename = htmlFilename.replace(/\.html$/, '');
|
|
149
|
+
|
|
150
|
+
return `"${htmlFilename}" has no matching component or view (expected ${basename}.ts or ${basename}.js).
|
|
151
|
+
Remove ${htmlFilename}, or add ${basename}.ts with a template placeholder to merge it into.`;
|
|
152
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { minify } from 'html-minifier-terser';
|
|
2
|
+
|
|
3
|
+
export async function minifyHTML(html) {
|
|
4
|
+
return minify(html, {
|
|
5
|
+
collapseWhitespace: true,
|
|
6
|
+
removeComments: true,
|
|
7
|
+
removeRedundantAttributes: true,
|
|
8
|
+
removeScriptTypeAttributes: true,
|
|
9
|
+
removeStyleLinkTypeAttributes: true,
|
|
10
|
+
useShortDoctype: true,
|
|
11
|
+
keepClosingSlash: true,
|
|
12
|
+
minifyCSS: true,
|
|
13
|
+
minifyJS: true,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { PATHS } from './paths.js';
|
|
3
|
+
import { getFilesRecursive, print, errorExit } from '../../../utils/index.js';
|
|
4
|
+
import { PATHS } from '../app/paths.js';
|
|
6
5
|
|
|
7
6
|
const destDir = path.join(PATHS.tempSource, 'locales');
|
|
8
7
|
|
|
@@ -50,7 +49,7 @@ async function removeJsonFiles(directory) {
|
|
|
50
49
|
try {
|
|
51
50
|
await fs.promises.access(directory);
|
|
52
51
|
} catch {
|
|
53
|
-
print.
|
|
52
|
+
print.grayError(`Directory ${directory} does not exist, skipping cleanup`);
|
|
54
53
|
return;
|
|
55
54
|
}
|
|
56
55
|
|
|
@@ -60,19 +59,13 @@ async function removeJsonFiles(directory) {
|
|
|
60
59
|
if (file.includes(`${path.sep}locales${path.sep}`)) {
|
|
61
60
|
try {
|
|
62
61
|
await fs.promises.unlink(file);
|
|
63
|
-
if (isVerbose) print.info(`Deleted locale JSON file: ${file}`);
|
|
64
62
|
} catch (err) {
|
|
65
63
|
print.error(`Failed to remove ${file}: ${err.message}`);
|
|
66
64
|
}
|
|
67
|
-
}
|
|
68
|
-
else if (isVerbose) {
|
|
69
|
-
print.info(`Skipping non-locale JSON file: ${file}`);
|
|
70
65
|
}
|
|
71
66
|
}
|
|
72
67
|
|
|
73
68
|
await removeEmptyDirectories(directory);
|
|
74
|
-
|
|
75
|
-
if (isVerbose) print.boldInfo(`JSON files cleanup complete.`);
|
|
76
69
|
}
|
|
77
70
|
|
|
78
71
|
async function removeEmptyDirectories(directory) {
|
|
@@ -102,10 +95,9 @@ async function removeEmptyDirectories(directory) {
|
|
|
102
95
|
const updatedItems = await fs.promises.readdir(directory);
|
|
103
96
|
if (updatedItems.length === 0) {
|
|
104
97
|
await fs.promises.rmdir(directory, { recursive: false });
|
|
105
|
-
if (isVerbose) print.info(`📁 Removed empty directory: ${directory}`);
|
|
106
98
|
}
|
|
107
99
|
} catch (err) {
|
|
108
|
-
print.
|
|
100
|
+
print.grayError(`Failed to remove empty directory ${directory}: ${err.message}`);
|
|
109
101
|
}
|
|
110
102
|
}
|
|
111
103
|
}
|
|
@@ -114,19 +106,16 @@ async function mergeJson() {
|
|
|
114
106
|
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
115
107
|
|
|
116
108
|
const localeFilesMap = findLocaleFiles();
|
|
117
|
-
|
|
118
109
|
for (const [locale, files] of Object.entries(localeFilesMap)) {
|
|
119
110
|
const combined = combineJsonFiles(files);
|
|
120
111
|
fs.writeFileSync(
|
|
121
112
|
`${destDir}/${locale}.json`,
|
|
122
113
|
JSON.stringify(combined, null, 2)
|
|
123
114
|
);
|
|
124
|
-
if (isVerbose) print.boldInfo(`Json files for locale ${locale} merged successfully`);
|
|
125
115
|
}
|
|
126
|
-
removeJsonFiles(PATHS.tempApp);
|
|
116
|
+
await removeJsonFiles(PATHS.tempApp);
|
|
127
117
|
}
|
|
128
118
|
|
|
129
119
|
mergeJson().catch((err) => {
|
|
130
|
-
|
|
131
|
-
exit(1);
|
|
120
|
+
errorExit(err)
|
|
132
121
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { gzipSync, brotliCompressSync, constants } from 'zlib';
|
|
3
|
+
import { print, getFilesRecursive, errorExit } from '../../../utils/index.js';
|
|
4
|
+
import { PATHS } from '../app/paths.js';
|
|
5
|
+
|
|
6
|
+
function compressStaticAssets() {
|
|
7
|
+
const files = getFilesRecursive(
|
|
8
|
+
PATHS.tempSource,
|
|
9
|
+
['.js', '.css', '.json', '.svg', '.ttf', '.otf', '.eot']
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const content = readFileSync(file);
|
|
14
|
+
|
|
15
|
+
const gzipped = gzipSync(content, {
|
|
16
|
+
level: 9,
|
|
17
|
+
memLevel: 9,
|
|
18
|
+
windowBits: 15
|
|
19
|
+
});
|
|
20
|
+
writeFileSync(`${file}.gz`, gzipped);
|
|
21
|
+
|
|
22
|
+
const brotlied = brotliCompressSync(content, {
|
|
23
|
+
params: {
|
|
24
|
+
[constants.BROTLI_PARAM_QUALITY]: 11,
|
|
25
|
+
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_GENERIC,
|
|
26
|
+
[constants.BROTLI_PARAM_SIZE_HINT]: content.length
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
writeFileSync(`${file}.br`, brotlied);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
compressStaticAssets()
|
|
35
|
+
} catch(err) {
|
|
36
|
+
errorExit(err, 'compress-files');
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import esbuild from 'esbuild';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { print, errorExit } from '../../../utils/index.js';
|
|
4
|
+
import { PATHS } from '../app/paths.js';
|
|
5
|
+
import { builderConfig } from '../../builder.config.js';
|
|
6
|
+
|
|
7
|
+
const ENTRY_FILE = path.join(PATHS.tempApp, 'main.ts');
|
|
8
|
+
const OUT_FILE = path.join(PATHS.tempSource, 'bundle.js');
|
|
9
|
+
|
|
10
|
+
async function build() {
|
|
11
|
+
await esbuild.build({
|
|
12
|
+
...builderConfig.esbuild,
|
|
13
|
+
platform: 'browser',
|
|
14
|
+
format: 'esm',
|
|
15
|
+
legalComments: 'none',
|
|
16
|
+
loader: {
|
|
17
|
+
'.json': 'json',
|
|
18
|
+
},
|
|
19
|
+
define: {
|
|
20
|
+
'process.env.NODE_ENV': '"production"',
|
|
21
|
+
},
|
|
22
|
+
entryPoints: [ENTRY_FILE],
|
|
23
|
+
outfile: OUT_FILE,
|
|
24
|
+
keepNames: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
build().catch((err) => {
|
|
29
|
+
errorExit(err, 'esbuild');
|
|
30
|
+
});
|
package/templates/base/tools/builder/core/{hash-files.js.hbs → prod-bundle/hash-files.js.hbs}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
|
-
import { PATHS } from '
|
|
5
|
-
import { print,
|
|
4
|
+
import { PATHS } from '../app/paths.js';
|
|
5
|
+
import { print, getFilesRecursive, errorExit } from '../../../utils/index.js';
|
|
6
6
|
|
|
7
7
|
function addHashToFiles(distDir) {
|
|
8
8
|
const files = getFilesRecursive(distDir, ['.js', '.css']);
|
|
@@ -24,10 +24,6 @@ function addHashToFiles(distDir) {
|
|
|
24
24
|
const newRel = path.relative(distDir, newPath).replace(/\\/g, '/');
|
|
25
25
|
|
|
26
26
|
fileMap[oldRel] = newRel;
|
|
27
|
-
|
|
28
|
-
if (isVerbose) {
|
|
29
|
-
print.info(`Renamed: ${oldRel} → ${newRel}`);
|
|
30
|
-
}
|
|
31
27
|
});
|
|
32
28
|
|
|
33
29
|
updateHtmlReferences(path.join(distDir, 'index.html'), fileMap);
|
|
@@ -38,15 +34,22 @@ function hashFile(filePath) {
|
|
|
38
34
|
return crypto.createHash('sha256').update(content).digest('hex').substring(0, 8);
|
|
39
35
|
}
|
|
40
36
|
|
|
37
|
+
function escapeRegExp(str) {
|
|
38
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
41
|
function updateHtmlReferences(htmlPath, fileMap) {
|
|
42
42
|
let html = fs.readFileSync(htmlPath, 'utf8');
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
Object.entries(fileMap).forEach(([oldName, newName]) => {
|
|
45
|
-
html = html.replace(new RegExp(oldName, 'g'), newName);
|
|
45
|
+
html = html.replace(new RegExp(escapeRegExp(oldName), 'g'), newName);
|
|
46
46
|
});
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
fs.writeFileSync(htmlPath, html);
|
|
49
|
-
if (isVerbose) print.info('Updated index.html references');
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
try {
|
|
52
|
+
addHashToFiles(PATHS.tempSource)
|
|
53
|
+
} catch(err) {
|
|
54
|
+
errorExit(err, 'hash-files');
|
|
55
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PATHS } from '../app/paths.js';
|
|
4
|
+
import { print } from '../../../utils/index.js';
|
|
5
|
+
|
|
6
|
+
export function generateRobotsTxt() {
|
|
7
|
+
const seoConfigPath = path.join(PATHS.temp, 'config', 'seo.json');
|
|
8
|
+
const seoConfig = JSON.parse(fs.readFileSync(seoConfigPath, 'utf-8'));
|
|
9
|
+
const baseUrl = seoConfig.baseUrl.replace(/\/$/, '');
|
|
10
|
+
const routes = seoConfig.routes ?? [];
|
|
11
|
+
const disallowBots = seoConfig.disallowBots ?? [];
|
|
12
|
+
|
|
13
|
+
// Paths disallowed for all bots (disallow: true)
|
|
14
|
+
const globalDisallows = routes
|
|
15
|
+
.filter(r => r.disallow === true)
|
|
16
|
+
.map(r => `Disallow: ${r.path}`);
|
|
17
|
+
|
|
18
|
+
// Paths disallowed per named bot: { botName -> [path, ...] }
|
|
19
|
+
const perBotDisallows = {};
|
|
20
|
+
for (const r of routes) {
|
|
21
|
+
if (!Array.isArray(r.disallow)) continue;
|
|
22
|
+
for (const bot of r.disallow) {
|
|
23
|
+
(perBotDisallows[bot] ??= []).push(r.path);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Bots that are fully blocked contribute an empty-path disallow
|
|
27
|
+
for (const bot of disallowBots) {
|
|
28
|
+
perBotDisallows[bot] ??= [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const lines = [
|
|
32
|
+
'User-agent: *',
|
|
33
|
+
'Allow: /',
|
|
34
|
+
...globalDisallows,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (const [bot, paths] of Object.entries(perBotDisallows)) {
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(`User-agent: ${bot}`);
|
|
40
|
+
if (paths.length > 0) {
|
|
41
|
+
for (const p of paths) lines.push(`Disallow: ${p}`);
|
|
42
|
+
} else {
|
|
43
|
+
lines.push('Disallow: /');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
lines.push('', `Sitemap: ${baseUrl}/sitemap.xml`);
|
|
48
|
+
|
|
49
|
+
const outputPath = path.join(PATHS.tempSource, 'robots.txt');
|
|
50
|
+
fs.writeFileSync(outputPath, lines.join('\n') + '\n', 'utf-8');
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { print, errorExit } from '../../../utils/index.js';
|
|
2
|
+
import { generateSeoHtml } from './generate-seo-html.js';
|
|
3
|
+
import { generateRobotsTxt } from './generate-robots-txt.js';
|
|
4
|
+
import { generateSitemapXml } from './generate-sitemap-xml.js';
|
|
5
|
+
|
|
6
|
+
async function generateSeoFiles() {
|
|
7
|
+
await generateSeoHtml();
|
|
8
|
+
generateRobotsTxt();
|
|
9
|
+
generateSitemapXml();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await generateSeoFiles();
|
|
14
|
+
} catch (err) {
|
|
15
|
+
errorExit(err, 'generate-seo-files');
|
|
16
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PATHS } from '../app/paths.js';
|
|
4
|
+
import { print, errorExit } from '../../../utils/index.js';
|
|
5
|
+
import { buildSsrBundle, cleanupSsrBundle } from './ssr/ssr-bundle.js';
|
|
6
|
+
import { renderRoute } from './ssr/ssr-render.js';
|
|
7
|
+
import { builderConfig } from '../../builder.config.js';
|
|
8
|
+
import { resolveLocaleValue, valueForLangWithFallback, collectI18nSeoIssues } from './i18n-resolution.js';
|
|
9
|
+
import { segmentsOf, validateMockParams, writeRouteHtml, warnForRoutesMissingSeoConfig } from './route-output.js';
|
|
10
|
+
|
|
11
|
+
async function processRoute(template, baseUrl, defaultLanguage, languages, route, bundleUrl) {
|
|
12
|
+
validateMockParams(route);
|
|
13
|
+
|
|
14
|
+
const routeSuffix = route.path === '/' ? '' : route.path;
|
|
15
|
+
|
|
16
|
+
if (builderConfig.i18n) {
|
|
17
|
+
for (const lang of languages) {
|
|
18
|
+
const title = valueForLangWithFallback(route.title, lang, defaultLanguage);
|
|
19
|
+
const description = valueForLangWithFallback(route.description, lang, defaultLanguage);
|
|
20
|
+
const ogImage = valueForLangWithFallback(route.ogImage, lang, defaultLanguage);
|
|
21
|
+
|
|
22
|
+
const pageUrl = `${baseUrl}/${lang}${routeSuffix}`;
|
|
23
|
+
const body = await renderRoute({
|
|
24
|
+
bundleUrl,
|
|
25
|
+
appRoutesKey: route.path,
|
|
26
|
+
mockParams: route.mockParams,
|
|
27
|
+
mockFetch: route.mockFetch,
|
|
28
|
+
lang,
|
|
29
|
+
pageUrl,
|
|
30
|
+
i18nEnabled: builderConfig.i18n,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await writeRouteHtml({
|
|
34
|
+
template, lang, title, description, pageUrl, ogImage, body,
|
|
35
|
+
outputSegments: [lang, ...segmentsOf(routeSuffix)],
|
|
36
|
+
routePath: route.path,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
const title = resolveLocaleValue(route.title, defaultLanguage);
|
|
41
|
+
const description = resolveLocaleValue(route.description, defaultLanguage);
|
|
42
|
+
const ogImage = resolveLocaleValue(route.ogImage, defaultLanguage);
|
|
43
|
+
|
|
44
|
+
if (!title || !description) {
|
|
45
|
+
errorExit(`Missing title or description for route "${route.path}" in seo.json`, 'generate-seo-html');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const pageUrl = `${baseUrl}${routeSuffix}`;
|
|
49
|
+
const body = await renderRoute({
|
|
50
|
+
bundleUrl,
|
|
51
|
+
appRoutesKey: route.path,
|
|
52
|
+
mockParams: route.mockParams,
|
|
53
|
+
mockFetch: route.mockFetch,
|
|
54
|
+
lang: defaultLanguage,
|
|
55
|
+
pageUrl,
|
|
56
|
+
i18nEnabled: builderConfig.i18n,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await writeRouteHtml({
|
|
60
|
+
template, lang: defaultLanguage, title, description, pageUrl, ogImage, body,
|
|
61
|
+
outputSegments: segmentsOf(routeSuffix),
|
|
62
|
+
routePath: route.path,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function generateSeoHtml() {
|
|
68
|
+
const templatePath = path.join(PATHS.tempSource, 'index.html');
|
|
69
|
+
const template = fs.readFileSync(templatePath, 'utf-8');
|
|
70
|
+
|
|
71
|
+
if (!template.includes('</head>')) {
|
|
72
|
+
errorExit('index.html is missing a </head> tag — cannot inject SEO tags', 'generate-seo-html');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const seoConfigPath = path.join(PATHS.temp, 'config', 'seo.json');
|
|
76
|
+
const seoConfig = JSON.parse(fs.readFileSync(seoConfigPath, 'utf-8'));
|
|
77
|
+
const baseUrl = seoConfig.baseUrl.replace(/\/$/, '');
|
|
78
|
+
|
|
79
|
+
const languagesConfigPath = path.join(PATHS.temp, 'config', 'languages.json');
|
|
80
|
+
const { languages, defaultLanguage } = JSON.parse(fs.readFileSync(languagesConfigPath, 'utf-8'));
|
|
81
|
+
|
|
82
|
+
if (builderConfig.i18n) {
|
|
83
|
+
const { fallbacks, missing, noLocalization } = collectI18nSeoIssues(seoConfig.routes, languages, defaultLanguage);
|
|
84
|
+
|
|
85
|
+
if (noLocalization.length > 0) {
|
|
86
|
+
const lines = noLocalization
|
|
87
|
+
.map(({ path, field }) => ` - route "${path}": "${field}"`)
|
|
88
|
+
.join('\n');
|
|
89
|
+
print.warn(
|
|
90
|
+
`[generate-seo-html] i18n is enabled, but these routes have no language values in seo.json:\n${lines}\n\n` +
|
|
91
|
+
`Add per-language objects (e.g. { "en": ..., "fr": ... }) under the matching route's ` +
|
|
92
|
+
`"title"/"description" in config/seo.json to localize this content, or ignore this ` +
|
|
93
|
+
`warning if sharing the same content across every language is intentional.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (fallbacks.length > 0) {
|
|
98
|
+
const lines = fallbacks
|
|
99
|
+
.map(({ path, field, lang, sourceLang }) => ` - route "${path}": "${field}.${lang}" not set — using "${field}.${sourceLang}"`)
|
|
100
|
+
.join('\n');
|
|
101
|
+
print.error(`[generate-seo-html] Using defaultLanguage fallback for missing i18n content:\n${lines}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (missing.length > 0) {
|
|
105
|
+
const lines = missing
|
|
106
|
+
.map(({ path, field, lang }) => ` - route "${path}": missing "${field}.${lang}" (no language has a value for this field)`)
|
|
107
|
+
.join('\n');
|
|
108
|
+
|
|
109
|
+
errorExit(
|
|
110
|
+
`config/seo.json is missing required content:\n${lines}\n\n` +
|
|
111
|
+
`Add these keys under the matching route's "title"/"description" in config/seo.json. ` +
|
|
112
|
+
`Configured languages (config/languages.json): ${languages.join(', ')}.`
|
|
113
|
+
, 'generate-seo-html'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const bundleUrl = await buildSsrBundle();
|
|
120
|
+
await warnForRoutesMissingSeoConfig(bundleUrl, seoConfig.routes, defaultLanguage, baseUrl);
|
|
121
|
+
|
|
122
|
+
for (const route of seoConfig.routes) {
|
|
123
|
+
await processRoute(template, baseUrl, defaultLanguage, languages, route, bundleUrl);
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
cleanupSsrBundle();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PATHS } from '../app/paths.js';
|
|
4
|
+
import { print } from '../../../utils/index.js';
|
|
5
|
+
import { builderConfig } from '../../builder.config.js';
|
|
6
|
+
|
|
7
|
+
function collectUrls(baseUrl, routes, languages) {
|
|
8
|
+
const urls = [];
|
|
9
|
+
|
|
10
|
+
for (const route of routes) {
|
|
11
|
+
const routeSuffix = route.path === '/' ? '' : route.path;
|
|
12
|
+
|
|
13
|
+
if (builderConfig.i18n) {
|
|
14
|
+
for (const lang of languages) {
|
|
15
|
+
urls.push(`${baseUrl}/${lang}${routeSuffix}/`);
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
urls.push(`${baseUrl}${routeSuffix}/`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return urls;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildSitemapXml(urls) {
|
|
26
|
+
const urlEntries = urls.map(loc => ` <url>\n <loc>${loc}</loc>\n </url>`).join('\n');
|
|
27
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urlEntries}\n</urlset>\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function generateSitemapXml() {
|
|
31
|
+
const seoConfigPath = path.join(PATHS.temp, 'config', 'seo.json');
|
|
32
|
+
const seoConfig = JSON.parse(fs.readFileSync(seoConfigPath, 'utf-8'));
|
|
33
|
+
const baseUrl = seoConfig.baseUrl.replace(/\/$/, '');
|
|
34
|
+
|
|
35
|
+
const languagesConfigPath = path.join(PATHS.temp, 'config', 'languages.json');
|
|
36
|
+
const { languages } = JSON.parse(fs.readFileSync(languagesConfigPath, 'utf-8'));
|
|
37
|
+
|
|
38
|
+
const urls = collectUrls(baseUrl, seoConfig.routes, languages);
|
|
39
|
+
const xml = buildSitemapXml(urls);
|
|
40
|
+
|
|
41
|
+
const outputPath = path.join(PATHS.tempSource, 'sitemap.xml');
|
|
42
|
+
fs.writeFileSync(outputPath, xml, 'utf-8');
|
|
43
|
+
}
|