@nutin/cli 2.1.1 → 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 +268 -0
- package/README.md +13 -5
- 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 +9 -8
- package/templates/base/AGENTS.md.hbs +42 -41
- package/templates/base/GETTING_STARTED.md.hbs +26 -20
- package/templates/base/README.md.hbs +7 -4
- 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 +7 -2
- 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 +286 -24
- 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 +66 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +135 -75
- 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 +93 -54
- 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 +183 -4
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +207 -84
- 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 +306 -0
- package/templates/base/src/core/services/router/router.ts.hbs +113 -53
- package/templates/base/tools/builder/builder.config.js.hbs +6 -0
- package/templates/base/tools/builder/builder.js.hbs +18 -6
- 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/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 +15 -7
- 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 +43 -13
- 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 +28 -21
- 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 +24 -6
- 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 +24 -11
- package/templates/base/tools/builder/core/styles/sass.js.hbs +3 -2
- package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
- package/templates/base/tools/dev/dev-serve.js.hbs +30 -5
- package/templates/base/tools/dev/serve.js.hbs +152 -22
- package/templates/base/tools/dev/watcher.js.hbs +34 -3
- package/templates/base/tools/generator/generator.js.hbs +1 -1
- package/templates/base/tools/generator/handle-file.js.hbs +4 -2
- package/templates/base/tools/generator/handle-file.test.js.hbs +3 -3
- 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 +30 -2
- 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 +2 -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 +3 -0
- package/templates/features/docker/tools/docker/nginx.conf.template +6 -4
- 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 +21 -4
- 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/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { sharedDefine, slimNutinConfig, slimSeoConfig, isPlainData } from './bundle-options.js';
|
|
2
|
+
|
|
3
|
+
describe('bundle-options', () => {
|
|
4
|
+
it('sharedDefine sets NODE_ENV, the i18n flag and the markdown sources from the build config', () => {
|
|
5
|
+
expect(sharedDefine({ isProd: true, i18n: false })).toEqual({
|
|
6
|
+
'process.env.NODE_ENV': '"production"',
|
|
7
|
+
'globalThis.__NUTIN_I18N__': 'false',
|
|
8
|
+
'globalThis.__NUTIN_MARKDOWN_SOURCES__': JSON.stringify('[]'),
|
|
9
|
+
});
|
|
10
|
+
expect(sharedDefine({ isProd: false, i18n: true, markdownSources: { sourceFolders: ['docs', { folder: 'x', seo: false }] } })).toEqual({
|
|
11
|
+
'process.env.NODE_ENV': '"development"',
|
|
12
|
+
'globalThis.__NUTIN_I18N__': 'true',
|
|
13
|
+
'globalThis.__NUTIN_MARKDOWN_SOURCES__': JSON.stringify('["docs",{"folder":"x","seo":false}]'),
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('slimNutinConfig drops tooling-only keys and keeps everything app code may read', () => {
|
|
18
|
+
const slim = slimNutinConfig({
|
|
19
|
+
i18n: true,
|
|
20
|
+
generateSEOFiles: false,
|
|
21
|
+
markdownSources: { sourceFolders: ['docs'] },
|
|
22
|
+
myAppKey: 'kept',
|
|
23
|
+
builder: { esbuild: { minify: true } },
|
|
24
|
+
testinNutin: { includeApp: false },
|
|
25
|
+
dockerPorts: [9090],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(slim).toEqual({
|
|
29
|
+
i18n: true,
|
|
30
|
+
generateSEOFiles: false,
|
|
31
|
+
myAppKey: 'kept',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('slimNutinConfig returns null when the remaining config is not plain data', () => {
|
|
36
|
+
expect(slimNutinConfig({ i18n: false, onBuild: () => {} })).toBe(null);
|
|
37
|
+
expect(slimNutinConfig({ i18n: false, pattern: /x/ })).toBe(null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('slimNutinConfig ignores non-plain values inside tooling-only keys', () => {
|
|
41
|
+
expect(slimNutinConfig({ i18n: false, builder: { plugin: () => {} } })).toEqual({ i18n: false });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('slimSeoConfig keeps only each route\'s path and title when SEO generation is enabled', () => {
|
|
45
|
+
const seo = {
|
|
46
|
+
baseUrl: 'https://example.com',
|
|
47
|
+
disallowBots: ['GPTBot'],
|
|
48
|
+
routes: [{ path: '/', title: { en: 'Home' }, description: 'd', ogImage: '/og.jpg' }],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
expect(slimSeoConfig(seo, true)).toEqual({ routes: [{ path: '/', title: { en: 'Home' } }] });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('slimSeoConfig returns an empty object when SEO generation is disabled or routes are missing', () => {
|
|
55
|
+
expect(slimSeoConfig({ routes: [{ path: '/', title: 'Home' }] }, false)).toEqual({});
|
|
56
|
+
expect(slimSeoConfig({}, true)).toEqual({ routes: [] });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('isPlainData accepts JSON-like values only', () => {
|
|
60
|
+
expect(isPlainData({ a: [1, 'b', true, null, { c: 2 }] })).toBeTruthy();
|
|
61
|
+
expect(isPlainData(undefined)).toBeFalsy();
|
|
62
|
+
expect(isPlainData(new Date())).toBeFalsy();
|
|
63
|
+
expect(isPlainData([() => {}])).toBeFalsy();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { gzipSync, brotliCompressSync, constants } from 'zlib';
|
|
3
|
-
import {
|
|
4
|
-
import { PATHS } from '../app/paths.js';
|
|
3
|
+
import { getFilesRecursive, errorExit } from '../../../utils/index.js';
|
|
4
|
+
import { PATHS, isBundledSource } from '../app/paths.js';
|
|
5
5
|
|
|
6
6
|
function compressStaticAssets() {
|
|
7
7
|
const files = getFilesRecursive(
|
|
8
8
|
PATHS.tempSource,
|
|
9
|
-
['.js', '.css', '.json', '.svg', '.ttf', '.otf', '.eot']
|
|
10
|
-
);
|
|
9
|
+
['.html', '.js', '.css', '.json', '.svg', '.xml', '.txt', '.ttf', '.otf', '.eot']
|
|
10
|
+
).filter((file) => !isBundledSource(file));
|
|
11
11
|
|
|
12
12
|
for (const file of files) {
|
|
13
13
|
const content = readFileSync(file);
|
|
@@ -27,11 +27,11 @@ function compressStaticAssets() {
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
writeFileSync(`${file}.br`, brotlied);
|
|
30
|
-
}
|
|
30
|
+
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
try {
|
|
34
|
-
compressStaticAssets()
|
|
34
|
+
compressStaticAssets();
|
|
35
35
|
} catch(err) {
|
|
36
36
|
errorExit(err, 'compress-files');
|
|
37
37
|
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import esbuild from 'esbuild';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { errorExit } from '../../../utils/index.js';
|
|
4
4
|
import { PATHS } from '../app/paths.js';
|
|
5
5
|
import { builderConfig } from '../../builder.config.js';
|
|
6
|
+
import { sharedDefine, slimConfigPlugin } from './bundle-options.js';
|
|
6
7
|
|
|
8
|
+
{{#if (eq lang "js")}}
|
|
9
|
+
// Bundles the copied .js sources; in dev they're also kept on disk for testin-nutin.
|
|
10
|
+
const ENTRY_FILE = path.join(PATHS.tempApp, 'main.js');
|
|
11
|
+
{{else}}
|
|
7
12
|
// Prod bundles straight from the copied .ts sources (tsc runs --noEmit there);
|
|
8
13
|
// dev bundles tsc's emitted .js, which is kept on disk for testin-nutin.
|
|
9
14
|
const ENTRY_FILE = path.join(PATHS.tempApp, builderConfig.isProd ? 'main.ts' : 'main.js');
|
|
10
|
-
|
|
15
|
+
{{/if}}
|
|
11
16
|
|
|
12
17
|
const DEV_OVERRIDES = {
|
|
13
18
|
minify: false,
|
|
@@ -25,11 +30,14 @@ async function build() {
|
|
|
25
30
|
loader: {
|
|
26
31
|
'.json': 'json',
|
|
27
32
|
},
|
|
28
|
-
define:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
define: sharedDefine(builderConfig),
|
|
34
|
+
plugins: [slimConfigPlugin({ generateSEO: builderConfig.generateSEO })],
|
|
35
|
+
// Code splitting: each dynamic import() becomes a chunk in chunks/, only fetched
|
|
36
|
+
// the first time it runs. The entry still comes out as bundle.js (see add-tags.js).
|
|
37
|
+
splitting: true,
|
|
38
|
+
entryPoints: [{ in: ENTRY_FILE, out: 'bundle' }],
|
|
39
|
+
outdir: PATHS.tempSource,
|
|
40
|
+
chunkNames: `${path.basename(PATHS.tempChunks)}/[name]-[hash]`,
|
|
33
41
|
keepNames: false,
|
|
34
42
|
});
|
|
35
43
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
|
-
import { PATHS } from '../app/paths.js';
|
|
5
|
-
import {
|
|
4
|
+
import { PATHS, isBundledSource, isChunk } from '../app/paths.js';
|
|
5
|
+
import { getFilesRecursive, errorExit } from '../../../utils/index.js';
|
|
6
6
|
|
|
7
7
|
function addHashToFiles(distDir) {
|
|
8
|
-
const files = getFilesRecursive(distDir, ['.js', '.css']);
|
|
8
|
+
const files = getFilesRecursive(distDir, ['.js', '.css']).filter((file) => !isBundledSource(file) && !isChunk(file));
|
|
9
9
|
|
|
10
10
|
const fileMap = {}; // Track old → new filenames
|
|
11
11
|
|
|
@@ -49,7 +49,7 @@ function updateHtmlReferences(htmlPath, fileMap) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
try {
|
|
52
|
-
addHashToFiles(PATHS.tempSource)
|
|
52
|
+
addHashToFiles(PATHS.tempSource);
|
|
53
53
|
} catch(err) {
|
|
54
54
|
errorExit(err, 'hash-files');
|
|
55
55
|
}
|
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { PATHS } from '../app/paths.js';
|
|
4
|
-
import {
|
|
4
|
+
import { builderConfig } from '../../builder.config.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// The URL path(s) a route's Disallow lines must cover: `:param` segments become `*` (every
|
|
7
|
+
// value of that segment), and with i18n on the pages live under each /<lang>/ prefix.
|
|
8
|
+
function disallowPathsFor(route, { i18n, languages }) {
|
|
9
|
+
const pattern = route.path.replace(/:[^/]+/g, '*');
|
|
10
|
+
if (!i18n) return [pattern];
|
|
11
|
+
const suffix = pattern === '/' ? '/' : pattern;
|
|
12
|
+
return languages.map((lang) => `/${lang}${suffix}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildRobotsTxt(seoConfig, { i18n = false, languages = [] } = {}) {
|
|
16
|
+
const i18nOptions = { i18n, languages };
|
|
9
17
|
const baseUrl = seoConfig.baseUrl.replace(/\/$/, '');
|
|
10
18
|
const routes = seoConfig.routes ?? [];
|
|
11
|
-
const disallowBots = seoConfig.disallowBots ?? [];
|
|
19
|
+
const disallowBots = new Set(seoConfig.disallowBots ?? []);
|
|
12
20
|
|
|
13
21
|
// Paths disallowed for all bots (disallow: true)
|
|
14
22
|
const globalDisallows = routes
|
|
15
23
|
.filter(r => r.disallow === true)
|
|
16
|
-
.
|
|
24
|
+
.flatMap(r => disallowPathsFor(r, i18nOptions));
|
|
17
25
|
|
|
18
26
|
// Paths disallowed per named bot: { botName -> [path, ...] }
|
|
19
27
|
const perBotDisallows = {};
|
|
20
28
|
for (const r of routes) {
|
|
21
29
|
if (!Array.isArray(r.disallow)) continue;
|
|
22
30
|
for (const bot of r.disallow) {
|
|
23
|
-
(perBotDisallows[bot] ??= []).push(r
|
|
31
|
+
(perBotDisallows[bot] ??= []).push(...disallowPathsFor(r, i18nOptions));
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
|
-
// Bots that are fully blocked contribute an empty-path disallow
|
|
27
34
|
for (const bot of disallowBots) {
|
|
28
35
|
perBotDisallows[bot] ??= [];
|
|
29
36
|
}
|
|
@@ -31,21 +38,32 @@ export function generateRobotsTxt() {
|
|
|
31
38
|
const lines = [
|
|
32
39
|
'User-agent: *',
|
|
33
40
|
'Allow: /',
|
|
34
|
-
...globalDisallows,
|
|
41
|
+
...globalDisallows.map(p => `Disallow: ${p}`),
|
|
35
42
|
];
|
|
36
43
|
|
|
37
44
|
for (const [bot, paths] of Object.entries(perBotDisallows)) {
|
|
38
45
|
lines.push('');
|
|
39
46
|
lines.push(`User-agent: ${bot}`);
|
|
40
|
-
if (
|
|
41
|
-
for
|
|
42
|
-
} else {
|
|
47
|
+
if (disallowBots.has(bot)) {
|
|
48
|
+
// Fully blocked — per-route entries for this bot are redundant.
|
|
43
49
|
lines.push('Disallow: /');
|
|
50
|
+
continue;
|
|
44
51
|
}
|
|
52
|
+
// A bot with its own group ignores the `*` group entirely, so it has to
|
|
53
|
+
// repeat the global disallows to stay blocked from them.
|
|
54
|
+
for (const p of new Set([...globalDisallows, ...paths])) lines.push(`Disallow: ${p}`);
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
lines.push('', `Sitemap: ${baseUrl}/sitemap.xml`);
|
|
58
|
+
return lines.join('\n') + '\n';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function generateRobotsTxt() {
|
|
62
|
+
const seoConfigPath = path.join(PATHS.temp, 'config', 'seo.json');
|
|
63
|
+
const seoConfig = JSON.parse(fs.readFileSync(seoConfigPath, 'utf-8'));
|
|
64
|
+
const languagesConfigPath = path.join(PATHS.temp, 'config', 'languages.json');
|
|
65
|
+
const { languages } = JSON.parse(fs.readFileSync(languagesConfigPath, 'utf-8'));
|
|
48
66
|
|
|
49
67
|
const outputPath = path.join(PATHS.tempSource, 'robots.txt');
|
|
50
|
-
fs.writeFileSync(outputPath,
|
|
68
|
+
fs.writeFileSync(outputPath, buildRobotsTxt(seoConfig, { i18n: builderConfig.i18n, languages }), 'utf-8');
|
|
51
69
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { buildRobotsTxt } from './generate-robots-txt.js';
|
|
2
|
+
|
|
3
|
+
const groupOf = (txt, bot) => {
|
|
4
|
+
const lines = txt.split('\n');
|
|
5
|
+
const start = lines.indexOf(`User-agent: ${bot}`);
|
|
6
|
+
const end = lines.indexOf('', start);
|
|
7
|
+
return lines.slice(start + 1, end === -1 ? undefined : end);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe('buildRobotsTxt', () => {
|
|
11
|
+
it('a bot in disallowBots is fully blocked even when a route also names it', () => {
|
|
12
|
+
const txt = buildRobotsTxt({
|
|
13
|
+
baseUrl: 'https://site.test/',
|
|
14
|
+
disallowBots: ['GPTBot'],
|
|
15
|
+
routes: [{ path: '/private', disallow: ['GPTBot'] }],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(groupOf(txt, 'GPTBot')).toEqual(['Disallow: /']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('a bot with its own group repeats the global disallows', () => {
|
|
22
|
+
const txt = buildRobotsTxt({
|
|
23
|
+
baseUrl: 'https://site.test',
|
|
24
|
+
routes: [
|
|
25
|
+
{ path: '/admin', disallow: true },
|
|
26
|
+
{ path: '/drafts', disallow: ['Bingbot'] },
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(groupOf(txt, '*')).toEqual(['Allow: /', 'Disallow: /admin']);
|
|
31
|
+
expect(groupOf(txt, 'Bingbot')).toEqual(['Disallow: /admin', 'Disallow: /drafts']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('turns :param segments into wildcards and prefixes every language with i18n on', () => {
|
|
35
|
+
const txt = buildRobotsTxt(
|
|
36
|
+
{ baseUrl: 'https://site.test', routes: [{ path: '/users/:id', disallow: true }] },
|
|
37
|
+
{ i18n: true, languages: ['en', 'fr'] }
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(groupOf(txt, '*')).toEqual(['Allow: /', 'Disallow: /en/users/*', 'Disallow: /fr/users/*']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('ends with the sitemap URL', () => {
|
|
44
|
+
const txt = buildRobotsTxt({ baseUrl: 'https://site.test/', routes: [] });
|
|
45
|
+
|
|
46
|
+
expect(txt.endsWith('Sitemap: https://site.test/sitemap.xml\n')).toBeTruthy();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { errorExit } from '../../../utils/index.js';
|
|
2
2
|
import { generateSeoHtml } from './generate-seo-html.js';
|
|
3
3
|
import { generateRobotsTxt } from './generate-robots-txt.js';
|
|
4
4
|
import { generateSitemapXml } from './generate-sitemap-xml.js';
|
|
5
5
|
|
|
6
6
|
async function generateSeoFiles() {
|
|
7
|
-
await generateSeoHtml();
|
|
7
|
+
const skippedSuffixes = await generateSeoHtml();
|
|
8
8
|
generateRobotsTxt();
|
|
9
|
-
generateSitemapXml();
|
|
9
|
+
generateSitemapXml(skippedSuffixes);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
try {
|
|
@@ -6,7 +6,8 @@ import { buildSsrBundle, cleanupSsrBundle } from './ssr/ssr-bundle.js';
|
|
|
6
6
|
import { renderRoute } from './ssr/ssr-render.js';
|
|
7
7
|
import { builderConfig } from '../../builder.config.js';
|
|
8
8
|
import { resolveLocaleValue, valueForLangWithFallback, collectI18nSeoIssues } from './i18n-resolution.js';
|
|
9
|
-
import { segmentsOf, validateMockParams, writeRouteHtml, warnForRoutesMissingSeoConfig } from './route-output.js';
|
|
9
|
+
import { segmentsOf, routeSuffixOf, validateMockParams, writeRouteHtml, warnForRoutesMissingSeoConfig } from './route-output.js';
|
|
10
|
+
import { resolveSeoRoutes, resolveSeoOptOutPaths } from './seo-routes.js';
|
|
10
11
|
|
|
11
12
|
// og:image/twitter:image require an absolute URL for social platforms to resolve them;
|
|
12
13
|
// every other absolute field (og:url, canonical) is already baseUrl-prefixed, so ogImage
|
|
@@ -16,10 +17,11 @@ function toAbsoluteUrl(urlOrPath, baseUrl) {
|
|
|
16
17
|
return /^https?:\/\//.test(urlOrPath) ? urlOrPath : `${baseUrl}${urlOrPath}`;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
// Returns the guard result when a route guard refused the route (nothing written), else null.
|
|
19
21
|
async function processRoute(template, baseUrl, defaultLanguage, languages, route, bundleUrl) {
|
|
20
22
|
validateMockParams(route);
|
|
21
23
|
|
|
22
|
-
const routeSuffix = route
|
|
24
|
+
const routeSuffix = routeSuffixOf(route);
|
|
23
25
|
|
|
24
26
|
if (builderConfig.i18n) {
|
|
25
27
|
// Same for every language variant of this route — lets crawlers know these URLs
|
|
@@ -30,13 +32,12 @@ async function processRoute(template, baseUrl, defaultLanguage, languages, route
|
|
|
30
32
|
{ hreflang: 'x-default', href: `${baseUrl}/${defaultLanguage}${routeSuffix}/` },
|
|
31
33
|
];
|
|
32
34
|
|
|
35
|
+
// Every language is rendered before anything is written: a guard refusing any of
|
|
36
|
+
// them skips the whole route, rather than shipping some language variants.
|
|
37
|
+
const pages = [];
|
|
33
38
|
for (const lang of languages) {
|
|
34
|
-
const title = valueForLangWithFallback(route.title, lang, defaultLanguage);
|
|
35
|
-
const description = valueForLangWithFallback(route.description, lang, defaultLanguage);
|
|
36
|
-
const ogImage = toAbsoluteUrl(valueForLangWithFallback(route.ogImage, lang, defaultLanguage), baseUrl);
|
|
37
|
-
|
|
38
39
|
const pageUrl = `${baseUrl}/${lang}${routeSuffix}`;
|
|
39
|
-
const
|
|
40
|
+
const result = await renderRoute({
|
|
40
41
|
bundleUrl,
|
|
41
42
|
appRoutesKey: route.path,
|
|
42
43
|
mockParams: route.mockParams,
|
|
@@ -45,6 +46,14 @@ async function processRoute(template, baseUrl, defaultLanguage, languages, route
|
|
|
45
46
|
pageUrl,
|
|
46
47
|
i18nEnabled: builderConfig.i18n,
|
|
47
48
|
});
|
|
49
|
+
if (result.blocked) return result;
|
|
50
|
+
pages.push({ lang, pageUrl, body: result.html });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const { lang, pageUrl, body } of pages) {
|
|
54
|
+
const title = valueForLangWithFallback(route.title, lang, defaultLanguage);
|
|
55
|
+
const description = valueForLangWithFallback(route.description, lang, defaultLanguage);
|
|
56
|
+
const ogImage = toAbsoluteUrl(valueForLangWithFallback(route.ogImage, lang, defaultLanguage), baseUrl);
|
|
48
57
|
|
|
49
58
|
await writeRouteHtml({
|
|
50
59
|
template, lang, title, description, pageUrl, ogImage, body, hreflangLinks,
|
|
@@ -62,7 +71,7 @@ async function processRoute(template, baseUrl, defaultLanguage, languages, route
|
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
const pageUrl = `${baseUrl}${routeSuffix}`;
|
|
65
|
-
const
|
|
74
|
+
const result = await renderRoute({
|
|
66
75
|
bundleUrl,
|
|
67
76
|
appRoutesKey: route.path,
|
|
68
77
|
mockParams: route.mockParams,
|
|
@@ -71,13 +80,24 @@ async function processRoute(template, baseUrl, defaultLanguage, languages, route
|
|
|
71
80
|
pageUrl,
|
|
72
81
|
i18nEnabled: builderConfig.i18n,
|
|
73
82
|
});
|
|
83
|
+
if (result.blocked) return result;
|
|
74
84
|
|
|
75
85
|
await writeRouteHtml({
|
|
76
|
-
template, lang: defaultLanguage, title, description, pageUrl, ogImage, body,
|
|
86
|
+
template, lang: defaultLanguage, title, description, pageUrl, ogImage, body: result.html,
|
|
77
87
|
outputSegments: segmentsOf(routeSuffix),
|
|
78
88
|
routePath: route.path,
|
|
79
89
|
});
|
|
80
90
|
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function describeBlocked(route, { redirectTo }) {
|
|
96
|
+
const outcome = redirectTo ? `redirected an anonymous visitor to "${redirectTo}"` : 'blocked an anonymous visitor';
|
|
97
|
+
const url = routeSuffixOf(route) || '/';
|
|
98
|
+
const where = url === route.path ? `"${route.path}"` : `"${route.path}" (${url})`;
|
|
99
|
+
return `[generate-seo-html] Route ${where} was not prerendered: its guard ${outcome}. ` +
|
|
100
|
+
`It is left out of sitemap.xml too.`;
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
export async function generateSeoHtml() {
|
|
@@ -131,14 +151,24 @@ export async function generateSeoHtml() {
|
|
|
131
151
|
}
|
|
132
152
|
}
|
|
133
153
|
|
|
154
|
+
// routeSuffixOf() of every route a guard refused, so the sitemap can leave them out too.
|
|
155
|
+
const skippedSuffixes = new Set();
|
|
156
|
+
|
|
134
157
|
try {
|
|
135
158
|
const bundleUrl = await buildSsrBundle();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
// seo.json routes plus feature-generated ones (e.g. one per Markdown page).
|
|
160
|
+
const routes = resolveSeoRoutes(seoConfig);
|
|
161
|
+
await warnForRoutesMissingSeoConfig(bundleUrl, routes, defaultLanguage, baseUrl, resolveSeoOptOutPaths());
|
|
162
|
+
|
|
163
|
+
for (const route of routes) {
|
|
164
|
+
const blocked = await processRoute(template, baseUrl, defaultLanguage, languages, route, bundleUrl);
|
|
165
|
+
if (!blocked) continue;
|
|
166
|
+
print.warn(describeBlocked(route, blocked));
|
|
167
|
+
skippedSuffixes.add(routeSuffixOf(route));
|
|
140
168
|
}
|
|
141
169
|
} finally {
|
|
142
170
|
cleanupSsrBundle();
|
|
143
171
|
}
|
|
172
|
+
|
|
173
|
+
return skippedSuffixes;
|
|
144
174
|
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { PATHS } from '../app/paths.js';
|
|
4
|
-
import { print } from '../../../utils/index.js';
|
|
5
4
|
import { builderConfig } from '../../builder.config.js';
|
|
5
|
+
import { routeSuffixOf } from './route-output.js';
|
|
6
|
+
import { resolveSeoRoutes } from './seo-routes.js';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// `skippedSuffixes`: routeSuffixOf() of routes generate-seo-html.js didn't prerender
|
|
9
|
+
// because a guard refused them.
|
|
10
|
+
export function collectUrls(baseUrl, routes, languages, { i18n = builderConfig.i18n, skippedSuffixes = new Set() } = {}) {
|
|
8
11
|
const urls = [];
|
|
9
12
|
|
|
10
13
|
for (const route of routes) {
|
|
11
|
-
|
|
14
|
+
// Disallowed for every bot in robots.txt — listing it here would contradict that.
|
|
15
|
+
if (route.disallow === true) continue;
|
|
16
|
+
const routeSuffix = routeSuffixOf(route);
|
|
17
|
+
if (skippedSuffixes.has(routeSuffix)) continue;
|
|
12
18
|
|
|
13
|
-
if (
|
|
19
|
+
if (i18n) {
|
|
14
20
|
for (const lang of languages) {
|
|
15
21
|
urls.push(`${baseUrl}/${lang}${routeSuffix}/`);
|
|
16
22
|
}
|
|
@@ -22,12 +28,21 @@ function collectUrls(baseUrl, routes, languages) {
|
|
|
22
28
|
return urls;
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
function escapeXml(str) {
|
|
32
|
+
return str
|
|
33
|
+
.replace(/&/g, '&')
|
|
34
|
+
.replace(/</g, '<')
|
|
35
|
+
.replace(/>/g, '>')
|
|
36
|
+
.replace(/"/g, '"')
|
|
37
|
+
.replace(/'/g, ''');
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
function buildSitemapXml(urls) {
|
|
26
|
-
const urlEntries = urls.map(loc => ` <url>\n <loc>${loc}</loc>\n </url>`).join('\n');
|
|
41
|
+
const urlEntries = urls.map(loc => ` <url>\n <loc>${escapeXml(loc)}</loc>\n </url>`).join('\n');
|
|
27
42
|
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
43
|
}
|
|
29
44
|
|
|
30
|
-
export function generateSitemapXml() {
|
|
45
|
+
export function generateSitemapXml(skippedSuffixes = new Set()) {
|
|
31
46
|
const seoConfigPath = path.join(PATHS.temp, 'config', 'seo.json');
|
|
32
47
|
const seoConfig = JSON.parse(fs.readFileSync(seoConfigPath, 'utf-8'));
|
|
33
48
|
const baseUrl = seoConfig.baseUrl.replace(/\/$/, '');
|
|
@@ -35,7 +50,7 @@ export function generateSitemapXml() {
|
|
|
35
50
|
const languagesConfigPath = path.join(PATHS.temp, 'config', 'languages.json');
|
|
36
51
|
const { languages } = JSON.parse(fs.readFileSync(languagesConfigPath, 'utf-8'));
|
|
37
52
|
|
|
38
|
-
const urls = collectUrls(baseUrl, seoConfig
|
|
53
|
+
const urls = collectUrls(baseUrl, resolveSeoRoutes(seoConfig), languages, { skippedSuffixes });
|
|
39
54
|
const xml = buildSitemapXml(urls);
|
|
40
55
|
|
|
41
56
|
const outputPath = path.join(PATHS.tempSource, 'sitemap.xml');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { collectUrls } from './generate-sitemap-xml.js';
|
|
2
|
+
|
|
3
|
+
const ROUTES = [
|
|
4
|
+
{ path: '/' },
|
|
5
|
+
{ path: '/admin' },
|
|
6
|
+
{ path: '/users/:id', mockParams: { id: '42' } },
|
|
7
|
+
{ path: '/drafts', disallow: true },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
describe('collectUrls', () => {
|
|
11
|
+
it('lists every route at its prerendered URL, minus routes disallowed for every bot', () => {
|
|
12
|
+
expect(collectUrls('https://site.test', ROUTES, ['en'], { i18n: false })).toEqual([
|
|
13
|
+
'https://site.test/',
|
|
14
|
+
'https://site.test/admin/',
|
|
15
|
+
'https://site.test/users/42/',
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('leaves out routes a guard kept from being prerendered', () => {
|
|
20
|
+
const urls = collectUrls('https://site.test', ROUTES, ['en'], { i18n: false, skippedSuffixes: new Set(['/admin']) });
|
|
21
|
+
|
|
22
|
+
expect(urls).toEqual(['https://site.test/', 'https://site.test/users/42/']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('skips a guarded route in every language with i18n on', () => {
|
|
26
|
+
const urls = collectUrls('https://site.test', ROUTES.slice(0, 2), ['en', 'fr'], { i18n: true, skippedSuffixes: new Set(['/admin']) });
|
|
27
|
+
|
|
28
|
+
expect(urls).toEqual(['https://site.test/en/', 'https://site.test/fr/']);
|
|
29
|
+
});
|
|
30
|
+
});
|