@nutin/cli 2.1.0 → 3.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 +322 -0
- package/README.md +14 -6
- package/bin/add-feature.mjs +1 -1
- package/bin/cli.mjs +1 -1
- package/bin/create-app.mjs +1 -1
- package/bin/update-app.mjs +1 -1
- package/lib/index.mjs +6 -2
- package/lib/src/add-feature/base-template-patcher.mjs +139 -0
- package/lib/src/add-feature/feature-adder.mjs +20 -9
- package/lib/src/add-feature/feature-context-builder.mjs +1 -0
- package/lib/src/add-feature/nutin-config-updater.mjs +34 -8
- package/lib/src/add-feature/package-json-updater.mjs +7 -9
- package/lib/src/common/feature-registry.mjs +4 -0
- package/lib/src/common/file-generator.mjs +25 -3
- package/lib/src/common/git.mjs +31 -0
- package/lib/src/common/meta-bootstrap-prompt.mjs +28 -2
- package/lib/src/common/package-json-helper.mjs +14 -4
- package/lib/src/common/project-meta.mjs +2 -2
- package/lib/src/common/template-compiler.mjs +2 -0
- package/lib/src/common/transpile-ts.mjs +94 -0
- package/lib/src/create-app/context-builder.mjs +1 -0
- package/lib/src/create-app/git-manager.mjs +12 -2
- package/lib/src/create-app/json-manager.mjs +21 -18
- package/lib/src/create-app/new-app-prompt.mjs +4 -2
- package/lib/src/create-app/success-message.mjs +1 -1
- package/lib/src/update-app/conflict-report-writer.mjs +33 -21
- package/lib/src/update-app/json-drift.mjs +54 -0
- package/lib/src/update-app/project-updater.mjs +123 -14
- package/lib/src/update-app/template-differ.mjs +5 -5
- package/lib/src/update-app/three-way-merge.mjs +42 -0
- package/lib/src/update-app/update-context-builder.mjs +1 -0
- package/lib/src/update-app/update-summary-printer.mjs +10 -2
- package/package.json +12 -11
- package/templates/base/.gitignore.hbs +1 -0
- package/templates/base/AGENTS.md.hbs +45 -43
- package/templates/base/GETTING_STARTED.md.hbs +32 -23
- package/templates/base/README.md.hbs +7 -4
- package/templates/base/nutin.config.js.hbs +1 -1
- package/templates/base/src/app/components/.gitkeep +0 -0
- package/templates/base/src/app/globals.d.ts.hbs +6 -3
- package/templates/base/src/app/guards.ts.hbs +1 -3
- package/templates/base/src/app/main.ts.hbs +2 -9
- package/templates/base/src/app/routes.ts.hbs +9 -3
- package/templates/base/src/app/services/.gitkeep +0 -0
- package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
- package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
- package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +307 -21
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +105 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +136 -62
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +101 -51
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
- package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
- package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
- package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
- package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
- package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
- package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
- package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
- package/templates/base/src/core/index.ts.hbs +4 -2
- package/templates/base/src/core/internals.d.ts.hbs +199 -20
- package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
- package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
- package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
- package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
- package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
- package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
- package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
- package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
- package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +217 -4
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +210 -65
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
- package/templates/base/src/core/services/router/router.test.js.hbs +321 -0
- package/templates/base/src/core/services/router/router.ts.hbs +122 -54
- package/templates/base/src/index.html.hbs +1 -0
- package/templates/base/src/styles/_styles.scss.hbs +1 -113
- package/templates/base/src/styles/main.scss.hbs +1 -1
- package/templates/base/tools/builder/builder.config.js.hbs +6 -0
- package/templates/base/tools/builder/builder.js.hbs +26 -8
- package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
- package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
- package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
- package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
- package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
- package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
- package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
- package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
- package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
- package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
- package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
- package/templates/base/tools/builder/core/html-index/add-tags.js.hbs +1 -4
- package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +3 -3
- package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
- package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
- package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
- package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
- package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
- package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
- package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
- package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
- package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +25 -8
- package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
- package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
- package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
- package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +61 -15
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
- package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +48 -18
- package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
- package/templates/base/tools/builder/core/seo/route-output.js.hbs +26 -8
- package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
- package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
- package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +26 -9
- package/templates/base/tools/builder/core/styles/sass.js.hbs +7 -5
- package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
- package/templates/base/tools/dev/dev-serve.js.hbs +71 -7
- package/templates/base/tools/dev/serve.js.hbs +152 -22
- package/templates/base/tools/dev/watcher.js.hbs +75 -28
- package/templates/base/tools/generator/generator.js.hbs +23 -18
- package/templates/base/tools/generator/handle-file.js.hbs +4 -24
- package/templates/base/tools/generator/handle-file.test.js.hbs +4 -23
- package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
- package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
- package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
- package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
- package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
- package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
- package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
- package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
- package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
- package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
- package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
- package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
- package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
- package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
- package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
- package/templates/base/tools/utils/build-lock.js.hbs +97 -0
- package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
- package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
- package/templates/base/tools/utils/format.js.hbs +1 -1
- package/templates/base/tools/utils/index.js.hbs +3 -1
- package/templates/base/tools/utils/print.js.hbs +2 -2
- package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
- package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
- package/templates/base/tools/utils/run-command.js.hbs +30 -1
- package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
- package/templates/base/tools/utils/run-script.js.hbs +2 -2
- package/templates/features/docker/tools/docker/Dockerfile.template.hbs +5 -2
- package/templates/features/docker/tools/docker/nginx.conf.template +19 -2
- package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
- package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
- package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
- package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
- package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
- package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +29 -0
- package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
- package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
- package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
- package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
- package/templates/base/src/app/components/index.ts.hbs +0 -1
- package/templates/base/src/app/services/index.ts.hbs +0 -1
- package/templates/base/src/app/views/index.ts.hbs +0 -3
- /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
|
@@ -1,35 +1,42 @@
|
|
|
1
|
-
import { Service } from "../../
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { Service } from "../../base-classes/service/service.js";
|
|
2
|
+
import * as HttpBuilder from "./helpers/http-builder.helper.js";
|
|
3
|
+
import * as HttpManager from "./helpers/http-manager.helper.js";
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export { path } from "./helpers/http-builder.helper.js";
|
|
6
|
+
export { HttpError } from "./helpers/http-manager.helper.js";
|
|
6
7
|
|
|
7
8
|
export class HttpClient extends Service<HttpClient> {
|
|
8
9
|
private _baseUrl: string;
|
|
9
10
|
private _defaultHeaders: Record<string, string>;
|
|
11
|
+
private _trustedAPIs: string[];
|
|
10
12
|
|
|
11
13
|
private _requestInterceptors: Array<(url: string, options: RequestInit) => void> = [];
|
|
12
14
|
private _responseInterceptors: Array<(response: Response) => void> = [];
|
|
13
15
|
|
|
14
|
-
constructor(
|
|
16
|
+
constructor(
|
|
17
|
+
baseUrl: string = '',
|
|
18
|
+
defaultHeaders: Record<string, string> = {},
|
|
19
|
+
options: Nutin.HttpClientOptions = {}
|
|
20
|
+
) {
|
|
15
21
|
super();
|
|
16
22
|
this._baseUrl = baseUrl;
|
|
17
|
-
this._defaultHeaders = {
|
|
23
|
+
this._defaultHeaders = { ...defaultHeaders };
|
|
24
|
+
this._trustedAPIs = [...(options.trustedAPIs ?? [])];
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
public get<T = unknown>(endpoint: string, config?:
|
|
27
|
+
public get<T = unknown>(endpoint: string, config?: Nutin.RequestConfig): Promise<T> {
|
|
21
28
|
return this.request<T>('GET', endpoint, null, config);
|
|
22
29
|
}
|
|
23
|
-
public post<T = unknown>(endpoint: string, data?: unknown, config?:
|
|
30
|
+
public post<T = unknown>(endpoint: string, data?: unknown, config?: Nutin.RequestConfig): Promise<T> {
|
|
24
31
|
return this.request<T>('POST', endpoint, data, config);
|
|
25
32
|
}
|
|
26
|
-
public put<T = unknown>(endpoint: string, data?: unknown, config?:
|
|
33
|
+
public put<T = unknown>(endpoint: string, data?: unknown, config?: Nutin.RequestConfig): Promise<T> {
|
|
27
34
|
return this.request<T>('PUT', endpoint, data, config);
|
|
28
35
|
}
|
|
29
|
-
public patch<T = unknown>(endpoint: string, data?: unknown, config?:
|
|
36
|
+
public patch<T = unknown>(endpoint: string, data?: unknown, config?: Nutin.RequestConfig): Promise<T> {
|
|
30
37
|
return this.request<T>('PATCH', endpoint, data, config);
|
|
31
38
|
}
|
|
32
|
-
public delete<T = unknown>(endpoint: string, config?:
|
|
39
|
+
public delete<T = unknown>(endpoint: string, config?: Nutin.RequestConfig): Promise<T> {
|
|
33
40
|
return this.request<T>('DELETE', endpoint, undefined, config);
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -37,6 +44,7 @@ export class HttpClient extends Service<HttpClient> {
|
|
|
37
44
|
this._requestInterceptors.push(fn);
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
// Each interceptor gets its own clone, so reading the body there doesn't consume it.
|
|
40
48
|
public addResponseInterceptor(fn: (response: Response) => void): void {
|
|
41
49
|
this._responseInterceptors.push(fn);
|
|
42
50
|
}
|
|
@@ -44,41 +52,45 @@ export class HttpClient extends Service<HttpClient> {
|
|
|
44
52
|
protected onDestroy(): void {
|
|
45
53
|
this._baseUrl = '';
|
|
46
54
|
this._defaultHeaders = {};
|
|
55
|
+
this._trustedAPIs = [];
|
|
47
56
|
this._requestInterceptors = [];
|
|
48
57
|
this._responseInterceptors = [];
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
private async request<T>(
|
|
52
|
-
method: HttpMethod,
|
|
61
|
+
method: Nutin.HttpMethod,
|
|
53
62
|
endpoint: string,
|
|
54
63
|
data?: unknown,
|
|
55
|
-
config
|
|
64
|
+
config: Nutin.RequestConfig = {}
|
|
56
65
|
): Promise<T> {
|
|
57
|
-
const
|
|
58
|
-
const fullUrl = `${this._baseUrl}${endpoint}`;
|
|
59
|
-
const mergedConfig: IRequestConfig = {
|
|
60
|
-
...config,
|
|
61
|
-
headers: { ...this._defaultHeaders, ...config?.headers },
|
|
62
|
-
};
|
|
66
|
+
const abort = HttpManager.createAbortController(config.timeout, config.signal);
|
|
63
67
|
|
|
64
68
|
try {
|
|
65
|
-
const url = HttpBuilder.
|
|
66
|
-
|
|
69
|
+
const url = HttpBuilder.resolveUrl(this._baseUrl, endpoint, this._trustedAPIs);
|
|
70
|
+
HttpBuilder.appendQueryParams(url, config.queryParams);
|
|
71
|
+
|
|
72
|
+
// Without a base URL, the client's default headers only go to the page's own origin;
|
|
73
|
+
// per-call headers are sent wherever the call goes.
|
|
74
|
+
const sendDefaults = this._baseUrl !== '' || url.origin === window.location.origin;
|
|
75
|
+
const headers = HttpBuilder.mergeHeaders(sendDefaults ? this._defaultHeaders : undefined, config.headers);
|
|
76
|
+
const requestOptions = HttpBuilder.buildRequestOptions(method, data, config, headers, abort.controller.signal);
|
|
67
77
|
|
|
68
78
|
this._requestInterceptors.forEach(fn => fn(url.toString(), requestOptions));
|
|
69
79
|
|
|
70
80
|
const response = await fetch(url.toString(), requestOptions);
|
|
81
|
+
// Checked before interceptors: another origin's response is never handed to app code.
|
|
82
|
+
if (response.redirected) HttpBuilder.assertRedirectTarget(this._baseUrl, url, response.url);
|
|
71
83
|
|
|
72
|
-
this._responseInterceptors.forEach(fn => fn(response));
|
|
84
|
+
this._responseInterceptors.forEach(fn => fn(response.clone()));
|
|
73
85
|
|
|
74
86
|
await HttpManager.validateResponse(response);
|
|
75
87
|
return await HttpManager.parseSuccessResponse<T>(response);
|
|
76
88
|
} catch (error) {
|
|
77
|
-
HttpManager.handleRequestError(error);
|
|
89
|
+
HttpManager.handleRequestError(error, abort.timedOut());
|
|
78
90
|
} finally {
|
|
79
|
-
|
|
91
|
+
abort.cleanup();
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
|
|
84
|
-
export const AppHttpClient = HttpClient.getInstance();
|
|
96
|
+
export const AppHttpClient = /* @__PURE__ */ HttpClient.getInstance();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I18nService,
|
|
1
|
+
import { I18nService, initI18n } from '#root/dist/src/core/services/index.js';
|
|
2
2
|
import { CONFIG } from '#root/dist/src/core/config.js';
|
|
3
3
|
|
|
4
4
|
class FetchMock {
|
|
@@ -57,7 +57,6 @@ describe('i18n module', async () => {
|
|
|
57
57
|
});
|
|
58
58
|
afterEach(() => {
|
|
59
59
|
fetchMock.restore();
|
|
60
|
-
localStorage.removeItem(I18nService.localStorageKey)
|
|
61
60
|
});
|
|
62
61
|
|
|
63
62
|
beforeAll(() =>{
|
|
@@ -125,23 +124,38 @@ describe('i18n module', async () => {
|
|
|
125
124
|
expect(I18nService.translate('non.existent.key')).toBe('non.existent.key');
|
|
126
125
|
});
|
|
127
126
|
|
|
127
|
+
it('translate and getTranslationObject ignore inherited Object.prototype members', () => {
|
|
128
|
+
expect(I18nService.translate('constructor')).toBe('constructor');
|
|
129
|
+
expect(I18nService.translate('__proto__')).toBe('__proto__');
|
|
130
|
+
expect(I18nService.translate('home.toString')).toBe('home.toString');
|
|
131
|
+
expect(I18nService.getTranslationObject('constructor')).toBe(null);
|
|
132
|
+
});
|
|
133
|
+
|
|
128
134
|
it('setCurrentLanguage updates the language, persists it and loads its translations', async () => {
|
|
129
135
|
await I18nService.setCurrentLanguage('fr');
|
|
130
136
|
|
|
131
137
|
expect(I18nService.currentLanguage).toBe('fr');
|
|
132
138
|
expect(I18nService.translate('home.title')).toBe('Mon App');
|
|
133
|
-
expect(localStorage.getItem(I18nService.localStorageKey)).toBe('fr');
|
|
134
139
|
});
|
|
135
140
|
|
|
136
141
|
it('setCurrentLanguage emits a language-changed event with the new language', async () => {
|
|
137
142
|
const received = [];
|
|
138
|
-
const
|
|
139
|
-
I18nService.onLanguageChange(callback);
|
|
143
|
+
const unsubscribe = I18nService.onLanguageChange((data) => received.push(data));
|
|
140
144
|
|
|
141
145
|
await I18nService.setCurrentLanguage('fr');
|
|
142
146
|
|
|
143
147
|
expect(received).toEqual([{ lang: 'fr' }]);
|
|
144
|
-
|
|
148
|
+
unsubscribe();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('onLanguageChange returns a function that stops further notifications', async () => {
|
|
152
|
+
const received = [];
|
|
153
|
+
const unsubscribe = I18nService.onLanguageChange((data) => received.push(data));
|
|
154
|
+
unsubscribe();
|
|
155
|
+
|
|
156
|
+
await I18nService.setCurrentLanguage('fr');
|
|
157
|
+
|
|
158
|
+
expect(received).toEqual([]);
|
|
145
159
|
});
|
|
146
160
|
|
|
147
161
|
it('getTranslationObject returns the nested value for the current language', async () => {
|
|
@@ -224,12 +238,6 @@ describe('i18n module', async () => {
|
|
|
224
238
|
errSpy.restore();
|
|
225
239
|
});
|
|
226
240
|
|
|
227
|
-
it('honors a previously saved language preference from localStorage', async () => {
|
|
228
|
-
localStorage.setItem(I18nService.localStorageKey, 'fr');
|
|
229
|
-
await I18nService.initTranslations();
|
|
230
|
-
expect(I18nService.currentLanguage).toBe('fr');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
241
|
it('loadTranslations logs and leaves the language unchanged when the fetch rejects', async () => {
|
|
234
242
|
Object.defineProperty(navigator, 'language', { writable: true, configurable: true, value: 'en-US' });
|
|
235
243
|
const errSpy = spyOn(console, 'error');
|
|
@@ -258,13 +266,46 @@ describe('i18n module', async () => {
|
|
|
258
266
|
errSpy.restore();
|
|
259
267
|
});
|
|
260
268
|
|
|
261
|
-
it('onDestroy
|
|
262
|
-
await I18nService.
|
|
263
|
-
expect(I18nService.translate('home.title')).toBe('
|
|
269
|
+
it('onDestroy clears translations but keeps the saved language preference', async () => {
|
|
270
|
+
await I18nService.setCurrentLanguage('fr');
|
|
271
|
+
expect(I18nService.translate('home.title')).toBe('Mon App');
|
|
264
272
|
|
|
265
273
|
I18nService.onDestroy();
|
|
266
274
|
|
|
267
275
|
expect(I18nService.translate('home.title')).toBe('home.title');
|
|
268
|
-
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('resetTranslations also forgets the saved language preference', async () => {
|
|
279
|
+
await I18nService.setCurrentLanguage('fr');
|
|
280
|
+
|
|
281
|
+
I18nService.resetTranslations();
|
|
282
|
+
|
|
283
|
+
expect(I18nService.currentLanguage).toBe('en');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('initI18n loads the translations when CONFIG.i18n is enabled', async () => {
|
|
287
|
+
const original = I18nService.initTranslations;
|
|
288
|
+
let calls = 0;
|
|
289
|
+
I18nService.initTranslations = async () => { calls++; };
|
|
290
|
+
CONFIG.i18n = true;
|
|
291
|
+
try {
|
|
292
|
+
await initI18n();
|
|
293
|
+
expect(calls).toBe(1);
|
|
294
|
+
} finally {
|
|
295
|
+
CONFIG.i18n = false;
|
|
296
|
+
I18nService.initTranslations = original;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('initI18n does nothing when CONFIG.i18n is disabled (default)', async () => {
|
|
301
|
+
const original = I18nService.initTranslations;
|
|
302
|
+
let calls = 0;
|
|
303
|
+
I18nService.initTranslations = async () => { calls++; };
|
|
304
|
+
try {
|
|
305
|
+
await initI18n();
|
|
306
|
+
expect(calls).toBe(0);
|
|
307
|
+
} finally {
|
|
308
|
+
I18nService.initTranslations = original;
|
|
309
|
+
}
|
|
269
310
|
});
|
|
270
311
|
});
|
|
@@ -1,49 +1,46 @@
|
|
|
1
|
-
import { DEFAULT_LANGUAGE, LANGUAGES
|
|
2
|
-
import { AppEventBus
|
|
1
|
+
import { DEFAULT_LANGUAGE, LANGUAGES } from './languages.js';
|
|
2
|
+
import { AppEventBus } from '../event-bus/event-bus.js';
|
|
3
|
+
import { Service } from '../../base-classes/service/service.js';
|
|
3
4
|
import { CONFIG } from '../../config.js';
|
|
4
5
|
|
|
5
6
|
export class I18n extends Service<I18n> {
|
|
6
|
-
private readonly _DEFAULT_LANGUAGE: Language = DEFAULT_LANGUAGE;
|
|
7
|
+
private readonly _DEFAULT_LANGUAGE: Nutin.Language = DEFAULT_LANGUAGE;
|
|
7
8
|
private readonly _LANGUAGES = LANGUAGES;
|
|
8
|
-
private _currentLanguage: Language = this._DEFAULT_LANGUAGE;
|
|
9
|
-
private _translations: Translations = {};
|
|
10
|
-
private _defaultTranslations: Translations = {};
|
|
11
|
-
private readonly _localStorageKey = 'nutin-fav-lang';
|
|
9
|
+
private _currentLanguage: Nutin.Language = this._DEFAULT_LANGUAGE;
|
|
10
|
+
private _translations: Nutin.Translations = {};
|
|
11
|
+
private _defaultTranslations: Nutin.Translations = {};
|
|
12
12
|
|
|
13
13
|
constructor() {
|
|
14
14
|
super();
|
|
15
|
-
this.registerCleanup(this.
|
|
15
|
+
this.registerCleanup(this.clearTranslations);
|
|
16
16
|
this._currentLanguage = this.getPreferredLanguage();
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
public get currentLanguage(): Language {
|
|
19
|
+
public get currentLanguage(): Nutin.Language {
|
|
20
20
|
return this._currentLanguage;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
public get defaultLanguage(): Language {
|
|
23
|
+
public get defaultLanguage(): Nutin.Language {
|
|
24
24
|
return this._DEFAULT_LANGUAGE;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
public get languages(): Language[] {
|
|
27
|
+
public get languages(): Nutin.Language[] {
|
|
28
28
|
return this._LANGUAGES;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
public
|
|
32
|
-
return this._localStorageKey;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
public async setCurrentLanguage(lang: Language): Promise<void> {
|
|
31
|
+
public async setCurrentLanguage(lang: Nutin.Language): Promise<void> {
|
|
36
32
|
this._currentLanguage = lang;
|
|
37
|
-
this.savePreferences();
|
|
38
33
|
await this.loadTranslations(lang);
|
|
39
34
|
AppEventBus.emit('language-changed', { lang });
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
|
|
37
|
+
// Returns the unsubscribe function, like Navigation.onNavigate().
|
|
38
|
+
public onLanguageChange(callback: (payload: { lang: string }) => void): () => void {
|
|
43
39
|
AppEventBus.subscribe('language-changed', callback);
|
|
40
|
+
return () => AppEventBus.off('language-changed', callback);
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
public async loadTranslations(lang: Language): Promise<void> {
|
|
43
|
+
public async loadTranslations(lang: Nutin.Language): Promise<void> {
|
|
47
44
|
try {
|
|
48
45
|
const response = await fetch(`/locales/${lang}.json`);
|
|
49
46
|
|
|
@@ -56,8 +53,7 @@ export class I18n extends Service<I18n> {
|
|
|
56
53
|
await this.loadDefaultTranslations();
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
document.documentElement.lang = lang;
|
|
60
|
-
this.savePreferences();
|
|
56
|
+
document.documentElement.lang = lang;
|
|
61
57
|
} catch (error) {
|
|
62
58
|
console.error('Translation load error:', error);
|
|
63
59
|
}
|
|
@@ -101,31 +97,33 @@ export class I18n extends Service<I18n> {
|
|
|
101
97
|
return value || null;
|
|
102
98
|
}
|
|
103
99
|
|
|
100
|
+
// Also forgets the saved language preference.
|
|
104
101
|
public resetTranslations(): void {
|
|
105
|
-
this.
|
|
106
|
-
this._defaultTranslations = {};
|
|
107
|
-
localStorage.removeItem(this._localStorageKey);
|
|
108
|
-
this._currentLanguage = this._DEFAULT_LANGUAGE;
|
|
102
|
+
this.clearTranslations();
|
|
109
103
|
}
|
|
110
104
|
|
|
105
|
+
// Teardown keeps the saved preference: it belongs to the user, not to this instance.
|
|
111
106
|
protected onDestroy(): void | Promise<void> {
|
|
112
|
-
this.resetTranslations();
|
|
113
107
|
this.dispose();
|
|
114
108
|
}
|
|
115
109
|
|
|
116
|
-
private
|
|
110
|
+
private clearTranslations(): void {
|
|
111
|
+
this._translations = {};
|
|
112
|
+
this._defaultTranslations = {};
|
|
113
|
+
this._currentLanguage = this._DEFAULT_LANGUAGE;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private getPreferredLanguage(): Nutin.Language {
|
|
117
117
|
const urlLocale = this.getLocaleFromUrl();
|
|
118
118
|
if (urlLocale) return urlLocale;
|
|
119
|
-
const
|
|
120
|
-
if (stored && this._LANGUAGES.includes(stored as Language)) return stored as Language;
|
|
121
|
-
const browserLang = navigator.language.split('-')[0] as Language;
|
|
119
|
+
const browserLang = navigator.language.split('-')[0] as Nutin.Language;
|
|
122
120
|
return this._LANGUAGES.includes(browserLang) ? browserLang : this._DEFAULT_LANGUAGE;
|
|
123
121
|
}
|
|
124
122
|
|
|
125
|
-
private getLocaleFromUrl(): Language | null {
|
|
126
|
-
if (!CONFIG.i18n) return null;
|
|
123
|
+
private getLocaleFromUrl(): Nutin.Language | null {
|
|
124
|
+
if (!(globalThis.__NUTIN_I18N__ ?? CONFIG.i18n)) return null;
|
|
127
125
|
|
|
128
|
-
const first = window.location.pathname.split('/').filter(Boolean)[0] as Language;
|
|
126
|
+
const first = window.location.pathname.split('/').filter(Boolean)[0] as Nutin.Language;
|
|
129
127
|
return first && this._LANGUAGES.includes(first) ? first : null;
|
|
130
128
|
}
|
|
131
129
|
|
|
@@ -143,20 +141,22 @@ export class I18n extends Service<I18n> {
|
|
|
143
141
|
|
|
144
142
|
private getNestedValue(obj: any, keys: string[]): any {
|
|
145
143
|
let value = obj;
|
|
144
|
+
// Own keys only: "constructor" or "__proto__" must not resolve to Object.prototype members.
|
|
146
145
|
for (const key of keys) {
|
|
147
|
-
value = value
|
|
146
|
+
value = value !== null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key)
|
|
147
|
+
? value[key]
|
|
148
|
+
: undefined;
|
|
148
149
|
if (!value) break;
|
|
149
150
|
}
|
|
150
151
|
return value;
|
|
151
152
|
}
|
|
153
|
+
}
|
|
152
154
|
|
|
153
|
-
|
|
154
|
-
localStorage.setItem(this._localStorageKey, this._currentLanguage);
|
|
155
|
-
}
|
|
155
|
+
export const I18nService = /* @__PURE__ */ I18n.getInstance();
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Loads the translations when i18n is enabled in nutin.config.js; a no-op otherwise.
|
|
159
|
+
*/
|
|
160
|
+
export async function initI18n(): Promise<void> {
|
|
161
|
+
if (globalThis.__NUTIN_I18N__ ?? CONFIG.i18n) await I18nService.initTranslations();
|
|
160
162
|
}
|
|
161
|
-
|
|
162
|
-
export const I18nService = I18n.getInstance();
|
|
@@ -3,5 +3,3 @@ import { CONFIG } from '../../config.js';
|
|
|
3
3
|
export const LANGUAGES = CONFIG.langs.languages;
|
|
4
4
|
export const DEFAULT_LANGUAGE = CONFIG.langs.defaultLanguage as (typeof LANGUAGES)[number];
|
|
5
5
|
|
|
6
|
-
export type Language = typeof LANGUAGES[number];
|
|
7
|
-
export type Translations = Record<string, any>;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { Service } from "../../base-classes/
|
|
2
|
-
|
|
3
|
-
export type PipeFunction = (value: any, ...args: any[]) => string;
|
|
1
|
+
import { Service } from "../../base-classes/service/service.js";
|
|
4
2
|
|
|
5
3
|
class PipeRegistry extends Service<PipeRegistry> {
|
|
6
|
-
private _pipes: Record<string, PipeFunction> = {};
|
|
4
|
+
private _pipes: Record<string, Nutin.PipeFunction> = {};
|
|
7
5
|
|
|
8
6
|
constructor() { super(); }
|
|
9
7
|
|
|
10
|
-
public register(name: string, fn: PipeFunction): void {
|
|
8
|
+
public register(name: string, fn: Nutin.PipeFunction): void {
|
|
11
9
|
if(this._pipes[name]) {
|
|
12
10
|
console.warn(`Pipe "${name}" already exists - skipping.`);
|
|
13
11
|
return;
|
|
@@ -29,4 +27,4 @@ class PipeRegistry extends Service<PipeRegistry> {
|
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
export const AppPipeRegistry = PipeRegistry.getInstance();
|
|
30
|
+
export const AppPipeRegistry = /* @__PURE__ */ PipeRegistry.getInstance();
|
|
@@ -52,6 +52,16 @@ describe('pipes (registerPipes)', () => {
|
|
|
52
52
|
expect(AppPipeRegistry.apply('date', value, ['en-US', 'long', true])).toBe(expected);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
it('date reads the time flag from template strings: "true" adds the time, "false" does not', () => {
|
|
56
|
+
const value = '2024-03-15T14:30:00Z';
|
|
57
|
+
const dateOnly = new Date(value).toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
58
|
+
const withTime = new Date(value).toLocaleString('en-US', {
|
|
59
|
+
year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
|
60
|
+
});
|
|
61
|
+
expect(AppPipeRegistry.apply('date', value, ['en-US', 'long', 'false'])).toBe(dateOnly);
|
|
62
|
+
expect(AppPipeRegistry.apply('date', value, ['en-US', 'long', 'true'])).toBe(withTime);
|
|
63
|
+
});
|
|
64
|
+
|
|
55
65
|
it('date warns and returns the raw stringified value for an invalid date', () => {
|
|
56
66
|
const warnSpy = spyOn(console, 'warn');
|
|
57
67
|
warnSpy.andCallFake(() => {});
|
|
@@ -18,12 +18,14 @@ export const registerPipes = (): void => {
|
|
|
18
18
|
value: string | number | Date,
|
|
19
19
|
locale = navigator.language,
|
|
20
20
|
format = 'long',
|
|
21
|
-
time = false,
|
|
21
|
+
time: boolean | string = false,
|
|
22
22
|
) => {
|
|
23
23
|
const date = new Date(value);
|
|
24
24
|
// '' is not a valid Intl.DateTimeFormatOptions value for hour/minute, so
|
|
25
25
|
// omit those keys entirely rather than setting them when time is false.
|
|
26
|
-
|
|
26
|
+
// From a template the flag arrives as a string ("false" is truthy).
|
|
27
|
+
const withTime = time === true || time === 'true';
|
|
28
|
+
const timeOptions: Intl.DateTimeFormatOptions = withTime ? { hour: '2-digit', minute: '2-digit' } : {};
|
|
27
29
|
const formats = {
|
|
28
30
|
short: { year: 'numeric', month: 'short', day: 'numeric', ...timeOptions } as Intl.DateTimeFormatOptions,
|
|
29
31
|
long: { year: 'numeric', month: 'long', day: 'numeric', ...timeOptions } as Intl.DateTimeFormatOptions,
|