@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
|
@@ -22,7 +22,7 @@ describe('addTags', () => {
|
|
|
22
22
|
const result = await addTags(html, filePath);
|
|
23
23
|
|
|
24
24
|
expect(result).toBe(
|
|
25
|
-
'<html><head><title>T</title><link rel="stylesheet" href="/main.css"></head><body><script type="module" src="/
|
|
25
|
+
'<html><head><title>T</title><link rel="stylesheet" href="/main.css"></head><body><script type="module" src="/bundle.js"></script></body></html>'
|
|
26
26
|
);
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ describe('addTags', () => {
|
|
|
32
32
|
const result = await addTags(html, filePath);
|
|
33
33
|
|
|
34
34
|
expect(result).toBe(
|
|
35
|
-
'<html><link rel="stylesheet" href="/main.css"><title>T</title><body><script type="module" src="/
|
|
35
|
+
'<html><link rel="stylesheet" href="/main.css"><title>T</title><body><script type="module" src="/bundle.js"></script></body></html>'
|
|
36
36
|
);
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -42,7 +42,7 @@ describe('addTags', () => {
|
|
|
42
42
|
const result = await addTags(html, filePath);
|
|
43
43
|
|
|
44
44
|
expect(result).toBe(
|
|
45
|
-
'<html></html><link rel="stylesheet" href="/main.css"><script type="module" src="/
|
|
45
|
+
'<html></html><link rel="stylesheet" href="/main.css"><script type="module" src="/bundle.js"></script>'
|
|
46
46
|
);
|
|
47
47
|
});
|
|
48
48
|
|
|
@@ -2,8 +2,12 @@ import { readFile } from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { print, errorExit } from '../../../utils/index.js';
|
|
4
4
|
import { PATHS } from '../app/paths.js';
|
|
5
|
+
import { readRoutesImporters } from '../app/route-keys.js';
|
|
6
|
+
import { builderConfig } from '../../builder.config.js';
|
|
5
7
|
import { addTags } from './add-tags.js';
|
|
6
8
|
|
|
9
|
+
const ROUTES_FILE = path.join(PATHS.sourceApp, 'routes.{{#if (eq lang "js")}}js{{else}}ts{{/if}}');
|
|
10
|
+
|
|
7
11
|
async function validateHtml() {
|
|
8
12
|
const filePath = path.join(PATHS.tempSource, 'index.html');
|
|
9
13
|
|
|
@@ -14,8 +18,12 @@ async function validateHtml() {
|
|
|
14
18
|
errorExit(`Failed to read ${filePath}`);
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
// The router mounts views into #app, and generateSEOFiles prerenders into it. An app without
|
|
22
|
+
// the router mounts its components wherever it wants: a missing target is reported at runtime.
|
|
23
|
+
const needsApp = builderConfig.generateSEO || readRoutesImporters(PATHS.sourceApp, ROUTES_FILE).length > 0;
|
|
24
|
+
if (needsApp && !htmlContent.includes('id="app"')) {
|
|
25
|
+
const reason = builderConfig.generateSEO ? 'generateSEOFiles prerenders pages into it' : 'the router mounts views into it';
|
|
26
|
+
errorExit(`Missing #app container in index.html: ${reason}.`);
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
htmlContent = await addTags(htmlContent, filePath).catch(err => errorExit(err.message));
|
|
@@ -1,41 +1,70 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import esbuild from 'esbuild';
|
|
3
4
|
import { getFilesRecursive, print, pascalCased, capitalized } from '../../../utils/index.js';
|
|
4
5
|
import { PATHS } from '../app/paths.js';
|
|
5
|
-
import {
|
|
6
|
+
import { findInlineTemplate, minifyTemplateBody } from './template-literal.js';
|
|
6
7
|
|
|
7
8
|
const PLACEHOLDER = '__TEMPLATE_PLACEHOLDER__';
|
|
8
9
|
const TARGET_EXTENSIONS = ['.ts', '.js'];
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
// Where each merged .html template sits in its code file (merge-templates writes it,
|
|
12
|
+
// compile-ts reads it to report type errors at the .html line). Keyed by path relative to
|
|
13
|
+
// dist-build (`src/app/...`), which is also the path of the source file in the project.
|
|
14
|
+
export const TEMPLATE_MAP_FILE = path.join(PATHS.temp, '.template-map.json');
|
|
10
15
|
|
|
11
16
|
// A component/view's template can live inline in its .ts/.js file or in a
|
|
12
17
|
// colocated .html sibling merged in via `__TEMPLATE_PLACEHOLDER__` — resolved
|
|
13
18
|
// automatically per file rather than through a global config flag:
|
|
14
19
|
// external + placeholder -> merge external in
|
|
15
20
|
// external + real inline markup -> conflict, fail
|
|
16
|
-
// no external + real inline markup ->
|
|
21
|
+
// no external + real inline markup -> keep inline
|
|
17
22
|
// no external + still the placeholder -> nothing to use, fail
|
|
18
|
-
|
|
23
|
+
// Templates are merged as written, so tsc type-checks them with readable line numbers
|
|
24
|
+
// (see compile-ts); minifyTemplates() minifies every template afterwards.
|
|
25
|
+
export async function mergeTemplates() {
|
|
19
26
|
const htmlFiles = await getFilesRecursive(PATHS.tempApp, 'html');
|
|
20
27
|
const resolvedPaths = new Set();
|
|
21
28
|
const failures = [];
|
|
29
|
+
const templateMap = {};
|
|
22
30
|
|
|
23
31
|
for (const htmlPath of htmlFiles) {
|
|
24
|
-
await
|
|
32
|
+
await mergeExternalTemplate(htmlPath, resolvedPaths, failures, templateMap);
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
await
|
|
35
|
+
await checkRemainingTemplates(resolvedPaths, failures);
|
|
36
|
+
await fs.writeFile(TEMPLATE_MAP_FILE, JSON.stringify(templateMap));
|
|
28
37
|
|
|
29
38
|
if (failures.length) {
|
|
30
39
|
throw new Error(`Failed to resolve ${failures.length} template(s).`);
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
// Minifies the template of every code file, merged or inline. Anything that can't be
|
|
44
|
+
// minified safely is kept as written.
|
|
45
|
+
export async function minifyTemplates() {
|
|
46
|
+
const templateMap = await readTemplateMap();
|
|
47
|
+
const codeFiles = await getFilesRecursive(PATHS.tempApp, TARGET_EXTENSIONS);
|
|
48
|
+
|
|
49
|
+
for (const codePath of codeFiles) {
|
|
50
|
+
const content = await fs.readFile(codePath, 'utf-8');
|
|
51
|
+
const template = findMergedTemplate(content, codePath, templateMap) ?? findInlineTemplate(content);
|
|
52
|
+
if (!template || template.body === null || template.body === PLACEHOLDER) continue;
|
|
53
|
+
|
|
54
|
+
const minified = await minifyTemplateBody(template.body);
|
|
55
|
+
if (minified === null) {
|
|
56
|
+
print.gray(describeUnminified(codePath));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const updated = content.slice(0, template.open) + minified + content.slice(template.end);
|
|
60
|
+
await fs.writeFile(codePath, await keepIfValid(codePath, updated, content));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Normally a view/component has exactly one sibling here: its copied `.ts` (TypeScript
|
|
65
|
+
// projects; tsc compiles it once merged) or `.js` (JS-only projects). Checking whichever
|
|
66
|
+
// exist(s) rather than assuming one, so this can't silently regress if that ever changes.
|
|
67
|
+
async function mergeExternalTemplate(htmlPath, resolvedPaths, failures, templateMap) {
|
|
39
68
|
const htmlFilename = path.basename(htmlPath);
|
|
40
69
|
|
|
41
70
|
let htmlContent;
|
|
@@ -47,7 +76,6 @@ async function resolveExternalTemplate(htmlPath, resolvedPaths, failures) {
|
|
|
47
76
|
return;
|
|
48
77
|
}
|
|
49
78
|
|
|
50
|
-
const minifiedHtml = await minifyHTML(htmlContent);
|
|
51
79
|
let mergedCount = 0;
|
|
52
80
|
let conflictCount = 0;
|
|
53
81
|
|
|
@@ -66,9 +94,18 @@ async function resolveExternalTemplate(htmlPath, resolvedPaths, failures) {
|
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
resolvedPaths.add(codePath);
|
|
97
|
+
warnIfUntagged(codePath, content, htmlContent);
|
|
69
98
|
|
|
70
99
|
if (content.includes(PLACEHOLDER)) {
|
|
71
|
-
|
|
100
|
+
templateMap[path.relative(PATHS.temp, codePath)] = {
|
|
101
|
+
html: path.relative(PATHS.temp, htmlPath),
|
|
102
|
+
...placeholderPosition(content),
|
|
103
|
+
lines: htmlContent.split('\n').length,
|
|
104
|
+
template: htmlContent,
|
|
105
|
+
};
|
|
106
|
+
// Replacements go through functions: a replacement *string* expands `$$`, `$&`,
|
|
107
|
+
// `$'`… found in the markup (e.g. `$${price}` would lose its `$`).
|
|
108
|
+
await fs.writeFile(codePath, content.replace(PLACEHOLDER, () => htmlContent));
|
|
72
109
|
mergedCount++;
|
|
73
110
|
continue;
|
|
74
111
|
}
|
|
@@ -86,29 +123,69 @@ async function resolveExternalTemplate(htmlPath, resolvedPaths, failures) {
|
|
|
86
123
|
}
|
|
87
124
|
}
|
|
88
125
|
|
|
89
|
-
|
|
126
|
+
// Inline templates: a placeholder with no .html to fill it fails; untagged ones warn.
|
|
127
|
+
async function checkRemainingTemplates(resolvedPaths, failures) {
|
|
90
128
|
const codeFiles = await getFilesRecursive(PATHS.tempApp, TARGET_EXTENSIONS);
|
|
91
129
|
|
|
92
130
|
for (const codePath of codeFiles) {
|
|
93
131
|
if (resolvedPaths.has(codePath)) continue;
|
|
94
132
|
|
|
95
133
|
const content = await fs.readFile(codePath, 'utf-8');
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
98
|
-
|
|
99
|
-
const [full, body] = match;
|
|
134
|
+
const template = findInlineTemplate(content);
|
|
135
|
+
if (!template || template.body === null) continue;
|
|
136
|
+
warnIfUntagged(codePath, content, template.body);
|
|
100
137
|
|
|
101
|
-
if (body === PLACEHOLDER) {
|
|
138
|
+
if (template.body === PLACEHOLDER) {
|
|
102
139
|
print.warn(describeMissingTemplate(codePath));
|
|
103
140
|
failures.push(path.basename(codePath));
|
|
104
|
-
continue;
|
|
105
141
|
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
106
144
|
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
export async function readTemplateMap() {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(await fs.readFile(TEMPLATE_MAP_FILE, 'utf-8'));
|
|
148
|
+
} catch {
|
|
149
|
+
return {};
|
|
109
150
|
}
|
|
110
151
|
}
|
|
111
152
|
|
|
153
|
+
// A merged template, found by its markup: it's wherever the placeholder was, whatever the
|
|
154
|
+
// variable holding it is called, in the copied .ts or in tsc's .js emitted from it.
|
|
155
|
+
function findMergedTemplate(content, codePath, templateMap) {
|
|
156
|
+
const relPath = path.relative(PATHS.temp, codePath);
|
|
157
|
+
const entry = templateMap[relPath] ?? templateMap[relPath.replace(/\.js$/, '.ts')];
|
|
158
|
+
if (!entry) return null;
|
|
159
|
+
const open = content.indexOf(entry.template);
|
|
160
|
+
if (open === -1) return null;
|
|
161
|
+
return { open, end: open + entry.template.length, body: entry.template };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 1-based line and column of the placeholder in a code file.
|
|
165
|
+
export function placeholderPosition(content) {
|
|
166
|
+
const index = content.indexOf(PLACEHOLDER);
|
|
167
|
+
const before = content.slice(0, index);
|
|
168
|
+
return { line: before.split('\n').length, col: index - before.lastIndexOf('\n') };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Last safety net: minified code that no longer parses is replaced by the code as written.
|
|
172
|
+
async function keepIfValid(codePath, minified, asWritten) {
|
|
173
|
+
if (minified === asWritten) return minified;
|
|
174
|
+
try {
|
|
175
|
+
await esbuild.transform(minified, { loader: codePath.endsWith('.ts') ? 'ts' : 'js' });
|
|
176
|
+
return minified;
|
|
177
|
+
} catch {
|
|
178
|
+
print.gray(describeUnminified(codePath));
|
|
179
|
+
return asWritten;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Templates are escaped by the html`` tag; an untagged literal with ${} interpolates raw.
|
|
184
|
+
function warnIfUntagged(codePath, content, markup) {
|
|
185
|
+
const template = findInlineTemplate(content);
|
|
186
|
+
if (template && !template.tagged && markup.includes('${')) print.warn(describeUntagged(codePath));
|
|
187
|
+
}
|
|
188
|
+
|
|
112
189
|
function describeCodeFile(codePath) {
|
|
113
190
|
const filename = path.basename(codePath);
|
|
114
191
|
const basename = filename.replace(/\.(ts|js)$/, '');
|
|
@@ -143,10 +220,21 @@ Expected:
|
|
|
143
220
|
Add ${basename}.html, or replace the placeholder in ${filename} with inline markup.`;
|
|
144
221
|
}
|
|
145
222
|
|
|
223
|
+
function describeUnminified(filePath) {
|
|
224
|
+
return `Kept the template of ${path.basename(filePath)} unminified: its markup couldn't be minified safely.`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function describeUntagged(codePath) {
|
|
228
|
+
const { filename, kind, className } = describeCodeFile(codePath);
|
|
229
|
+
|
|
230
|
+
return `${kind} "${className}" has an untagged template: its \${} values are inserted unescaped.
|
|
231
|
+
Tag it in ${filename}: html\`...\` (import html from core), and wrap trusted markup in raw().`;
|
|
232
|
+
}
|
|
233
|
+
|
|
146
234
|
function describeOrphanHtml(htmlPath) {
|
|
147
235
|
const htmlFilename = path.basename(htmlPath);
|
|
148
236
|
const basename = htmlFilename.replace(/\.html$/, '');
|
|
149
237
|
|
|
150
238
|
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.`;
|
|
239
|
+
Remove ${htmlFilename}, or add ${basename}.{{#if (eq lang "js")}}js{{else}}ts{{/if}} with a template placeholder to merge it into.`;
|
|
152
240
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { errorExit } from '../../../utils/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { mergeTemplates } from './merge-templates.js';
|
|
3
3
|
|
|
4
|
+
// Merges each .html template into its code file, as written: TypeScript projects type-check
|
|
5
|
+
// them next (compile-ts), and minify-templates.js minifies them after that.
|
|
4
6
|
try {
|
|
5
|
-
await
|
|
7
|
+
await mergeTemplates();
|
|
6
8
|
} catch (err) {
|
|
7
|
-
errorExit(err);
|
|
9
|
+
errorExit(err, 'process-html-templates');
|
|
8
10
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { minifyHTML } from './minify-html.js';
|
|
2
|
+
|
|
3
|
+
// Minifies the markup of html`` template literals without ever handing JavaScript to the
|
|
4
|
+
// HTML minifier: each ${…} is swapped for a token, the markup is minified, and the
|
|
5
|
+
// expressions are put back as written (html`` templates nested inside them are minified
|
|
6
|
+
// the same way). Anything the scanner can't follow, or a token the minifier drops or
|
|
7
|
+
// duplicates (e.g. a whole style="${…}" emptied as invalid CSS), leaves the template as
|
|
8
|
+
// written: the worst case is an unminified template, never a broken one.
|
|
9
|
+
|
|
10
|
+
const TOKEN_PREFIX = '__nutin_expr_';
|
|
11
|
+
const token = (n) => `${TOKEN_PREFIX}${n}__`;
|
|
12
|
+
|
|
13
|
+
// After these, a "/" starts a regex literal rather than a division.
|
|
14
|
+
const REGEX_PRECEDING_WORDS = new Set([
|
|
15
|
+
'return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void',
|
|
16
|
+
'throw', 'case', 'do', 'else', 'yield', 'await',
|
|
17
|
+
]);
|
|
18
|
+
const REGEX_PRECEDING_PUNCTUATION = new Set('(,=:[!&|?{};+-*%<>~^'.split(''));
|
|
19
|
+
|
|
20
|
+
const isIdentifierChar = (ch) => /[\w$]/.test(ch);
|
|
21
|
+
|
|
22
|
+
// Index just past the closing quote of the string starting at `i`, or -1.
|
|
23
|
+
function skipString(src, i) {
|
|
24
|
+
const quote = src[i];
|
|
25
|
+
for (i++; i < src.length; i++) {
|
|
26
|
+
if (src[i] === '\\') i++;
|
|
27
|
+
else if (src[i] === quote) return i + 1;
|
|
28
|
+
else if (src[i] === '\n') return -1;
|
|
29
|
+
}
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Index just past the regex literal (and its flags) starting at `i`, or -1.
|
|
34
|
+
function skipRegex(src, i) {
|
|
35
|
+
let inClass = false;
|
|
36
|
+
for (i++; i < src.length; i++) {
|
|
37
|
+
const ch = src[i];
|
|
38
|
+
if (ch === '\\') i++;
|
|
39
|
+
else if (ch === '\n') return -1;
|
|
40
|
+
else if (inClass) inClass = ch !== ']';
|
|
41
|
+
else if (ch === '[') inClass = true;
|
|
42
|
+
else if (ch === '/') {
|
|
43
|
+
i++;
|
|
44
|
+
while (i < src.length && isIdentifierChar(src[i])) i++;
|
|
45
|
+
return i;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return -1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Scans a template literal from `start`, the index just past its opening backtick.
|
|
53
|
+
* Returns `{ end, expressions, nested }`: `end` is the index of the closing backtick,
|
|
54
|
+
* `expressions` the top-level `${…}` spans (`start` at "$", `end` past "}"), `nested` the
|
|
55
|
+
* html``-tagged literals inside those expressions (`open` past their opening backtick,
|
|
56
|
+
* `end` at their closing one). null when the literal isn't closed or can't be followed.
|
|
57
|
+
*/
|
|
58
|
+
export function scanTemplate(src, start) {
|
|
59
|
+
const expressions = [];
|
|
60
|
+
const nested = [];
|
|
61
|
+
|
|
62
|
+
for (let i = start; i < src.length; i++) {
|
|
63
|
+
const ch = src[i];
|
|
64
|
+
if (ch === '\\') {
|
|
65
|
+
i++;
|
|
66
|
+
} else if (ch === '`') {
|
|
67
|
+
return { end: i, expressions, nested };
|
|
68
|
+
} else if (ch === '$' && src[i + 1] === '{') {
|
|
69
|
+
const end = scanExpression(src, i + 2, nested);
|
|
70
|
+
if (end === -1) return null;
|
|
71
|
+
expressions.push({ start: i, end });
|
|
72
|
+
i = end - 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Index just past the "}" closing the expression that starts at `i`, or -1.
|
|
79
|
+
function scanExpression(src, i, nested) {
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let previous = '('; // the expression start behaves like an opening parenthesis
|
|
82
|
+
|
|
83
|
+
while (i < src.length) {
|
|
84
|
+
const ch = src[i];
|
|
85
|
+
|
|
86
|
+
if (/\s/.test(ch)) {
|
|
87
|
+
i++;
|
|
88
|
+
} else if (ch === '/' && src[i + 1] === '/') {
|
|
89
|
+
const newline = src.indexOf('\n', i);
|
|
90
|
+
if (newline === -1) return -1;
|
|
91
|
+
i = newline + 1;
|
|
92
|
+
} else if (ch === '/' && src[i + 1] === '*') {
|
|
93
|
+
const close = src.indexOf('*/', i + 2);
|
|
94
|
+
if (close === -1) return -1;
|
|
95
|
+
i = close + 2;
|
|
96
|
+
} else if (ch === '"' || ch === "'") {
|
|
97
|
+
i = skipString(src, i);
|
|
98
|
+
if (i === -1) return -1;
|
|
99
|
+
previous = 'value';
|
|
100
|
+
} else if (ch === '`') {
|
|
101
|
+
const inner = scanTemplate(src, i + 1);
|
|
102
|
+
if (!inner) return -1;
|
|
103
|
+
if (previous === 'html') nested.push({ open: i + 1, end: inner.end });
|
|
104
|
+
i = inner.end + 1;
|
|
105
|
+
previous = 'value';
|
|
106
|
+
} else if (ch === '/' && (REGEX_PRECEDING_PUNCTUATION.has(previous) || REGEX_PRECEDING_WORDS.has(previous))) {
|
|
107
|
+
i = skipRegex(src, i);
|
|
108
|
+
if (i === -1) return -1;
|
|
109
|
+
previous = 'value';
|
|
110
|
+
} else if (isIdentifierChar(ch)) {
|
|
111
|
+
let end = i;
|
|
112
|
+
while (end < src.length && isIdentifierChar(src[end])) end++;
|
|
113
|
+
previous = src.slice(i, end);
|
|
114
|
+
i = end;
|
|
115
|
+
} else {
|
|
116
|
+
if (ch === '{') depth++;
|
|
117
|
+
if (ch === '}') {
|
|
118
|
+
if (depth === 0) return i + 1;
|
|
119
|
+
depth--;
|
|
120
|
+
}
|
|
121
|
+
previous = ch;
|
|
122
|
+
i++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return -1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Minifies a template body (the text between the backticks, or an external .html file).
|
|
130
|
+
* Returns the minified body, or null when it is left as written.
|
|
131
|
+
*/
|
|
132
|
+
export async function minifyTemplateBody(body) {
|
|
133
|
+
const scan = scanTemplate(`${body}\``, 0);
|
|
134
|
+
if (!scan || scan.end !== body.length || body.includes(TOKEN_PREFIX)) return null;
|
|
135
|
+
|
|
136
|
+
// Expressions (with their own nested html`` minified first), in source order.
|
|
137
|
+
const expressions = [];
|
|
138
|
+
for (const { start, end } of scan.expressions) {
|
|
139
|
+
const nestedInside = scan.nested.filter((n) => n.open > start && n.end < end);
|
|
140
|
+
expressions.push(await minifyNested(body, start, end, nestedInside));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let markup = '';
|
|
144
|
+
let last = 0;
|
|
145
|
+
scan.expressions.forEach(({ start, end }, n) => {
|
|
146
|
+
markup += body.slice(last, start) + token(n);
|
|
147
|
+
last = end;
|
|
148
|
+
});
|
|
149
|
+
markup += body.slice(last);
|
|
150
|
+
|
|
151
|
+
let minified;
|
|
152
|
+
try {
|
|
153
|
+
minified = await minifyHTML(markup);
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (let n = 0; n < expressions.length; n++) {
|
|
159
|
+
if (minified.split(token(n)).length !== 2) return null;
|
|
160
|
+
}
|
|
161
|
+
return minified.replace(/__nutin_expr_(\d+)__/g, (_match, n) => expressions[Number(n)]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The source of body[start, end) with each nested html`` body minified (kept when it can't be).
|
|
165
|
+
async function minifyNested(body, start, end, nested) {
|
|
166
|
+
let out = '';
|
|
167
|
+
let last = start;
|
|
168
|
+
for (const { open, end: close } of nested) {
|
|
169
|
+
const inner = body.slice(open, close);
|
|
170
|
+
out += body.slice(last, open) + ((await minifyTemplateBody(inner)) ?? inner);
|
|
171
|
+
last = close;
|
|
172
|
+
}
|
|
173
|
+
return out + body.slice(last, end);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Up to the opening backtick; scanTemplate() finds the closing one, past any nested html``.
|
|
177
|
+
const TEMPLATE_START = /const\s+(?:template|templateFn)\s*=?\s*(?:\(.*?\)\s*=>\s*)?(html\s*)?`/;
|
|
178
|
+
|
|
179
|
+
// The first `const template`/`templateFn` literal: whether it is html``-tagged, where its
|
|
180
|
+
// body starts and ends, and the body (null when its closing backtick can't be found).
|
|
181
|
+
export function findInlineTemplate(content) {
|
|
182
|
+
const match = TEMPLATE_START.exec(content);
|
|
183
|
+
if (!match) return null;
|
|
184
|
+
|
|
185
|
+
const open = match.index + match[0].length;
|
|
186
|
+
const scan = scanTemplate(content, open);
|
|
187
|
+
return {
|
|
188
|
+
tagged: Boolean(match[1]),
|
|
189
|
+
open,
|
|
190
|
+
end: scan?.end ?? null,
|
|
191
|
+
body: scan ? content.slice(open, scan.end) : null,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { scanTemplate, minifyTemplateBody } from './template-literal.js';
|
|
2
|
+
|
|
3
|
+
// Scans `source` from just past its first backtick and returns the literal's body.
|
|
4
|
+
function bodyOf(source) {
|
|
5
|
+
const open = source.indexOf('`') + 1;
|
|
6
|
+
const scan = scanTemplate(source, open);
|
|
7
|
+
return scan && source.slice(open, scan.end);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('template-literal', () => {
|
|
11
|
+
it('scanTemplate finds the closing backtick past a nested html`` in a .map()', () => {
|
|
12
|
+
const source = 'const t = html`<ul>${items.map((i) => html`<li>${i}</li>`)}</ul>`; after();';
|
|
13
|
+
expect(bodyOf(source)).toBe('<ul>${items.map((i) => html`<li>${i}</li>`)}</ul>');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('scanTemplate is not fooled by backticks or braces in strings, comments and regex literals', () => {
|
|
17
|
+
const body = "<p>${s.replace(/`/g, '')}${'}`'}${x /* ` } */}${\"{\"}${y // `}\n}</p>";
|
|
18
|
+
expect(bodyOf('html`' + body + '`; x;')).toBe(body);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('scanTemplate reads a "/" after a value as a division, not a regex', () => {
|
|
22
|
+
const body = '<p>${total / count} of ${(a) / 2} / ${b[0] / 3}</p>';
|
|
23
|
+
expect(bodyOf('html`' + body + '`; x;')).toBe(body);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('scanTemplate skips escaped backticks and escaped ${', () => {
|
|
27
|
+
const body = '<p>\\` and \\${not} and ${real}</p>';
|
|
28
|
+
const scan = scanTemplate('html`' + body + '`', 5);
|
|
29
|
+
expect(scan.end).toBe(5 + body.length);
|
|
30
|
+
expect(scan.expressions.length).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('scanTemplate returns null for an unclosed literal or expression', () => {
|
|
34
|
+
expect(scanTemplate('html`<p>${a</p>', 5)).toBe(null);
|
|
35
|
+
expect(scanTemplate('html`<p>', 5)).toBe(null);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('minifyTemplateBody minifies the markup and the nested html`` bodies', async () => {
|
|
39
|
+
const body = '\n<ul class="list">\n ${items.map((i) => html`\n <li>\n ${i}\n </li>\n `)}\n</ul>\n<p>\n after nested\n</p>\n';
|
|
40
|
+
expect(await minifyTemplateBody(body)).toBe('<ul class="list">${items.map((i) => html`<li>${i}</li>`)}</ul><p>after nested</p>');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('minifyTemplateBody puts every expression back exactly as written', async () => {
|
|
44
|
+
const body = '<p title="${t}"> ${\'a b\'} ${a < b ? x : y} </p>';
|
|
45
|
+
expect(await minifyTemplateBody(body)).toBe('<p title="${t}">${\'a b\'} ${a < b ? x : y}</p>');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('minifyTemplateBody keeps expressions in attribute-name position', async () => {
|
|
49
|
+
expect(await minifyTemplateBody('<input ${attrs} ${more}> <b> x </b>')).toBe('<input ${attrs} ${more}> <b>x</b>');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('minifyTemplateBody leaves a template with a dynamic tag name as written', async () => {
|
|
53
|
+
expect(await minifyTemplateBody('<${tag}> x </${tag}>')).toBe(null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('minifyTemplateBody leaves a template as written when the minifier would drop an expression', async () => {
|
|
57
|
+
expect(await minifyTemplateBody('<div style="${css}">x</div>')).toBe(null);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('minifyTemplateBody leaves a template as written when the scanner cannot follow it', async () => {
|
|
61
|
+
expect(await minifyTemplateBody('<p>${a</p>')).toBe(null);
|
|
62
|
+
expect(await minifyTemplateBody('<p>__nutin_expr_0__ ${a}</p>')).toBe(null);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('minifyTemplateBody keeps a nested html`` it cannot minify as written', async () => {
|
|
66
|
+
const body = '<ul> ${items.map((i) => html`<li style="${i}">x</li>`)} </ul>';
|
|
67
|
+
expect(await minifyTemplateBody(body)).toBe('<ul>${items.map((i) => html`<li style="${i}">x</li>`)}</ul>');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -33,13 +33,21 @@ function getKeyForFile(filePath) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function combineJsonFiles(files) {
|
|
36
|
+
function combineJsonFiles(locale, files) {
|
|
37
37
|
const combined = {};
|
|
38
|
+
const sources = {};
|
|
38
39
|
|
|
39
40
|
files.forEach(file => {
|
|
40
41
|
const key = getKeyForFile(file);
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// Keys are folder names, so e.g. admin/user/ and public/user/ would share "user".
|
|
43
|
+
if (sources[key]) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Translation key "${key}" (${locale}) is defined by two folders:\n ${path.relative(process.cwd(), sources[key])}\n ${path.relative(process.cwd(), file)}\n` +
|
|
46
|
+
`One would silently overwrite the other — rename one of the folders.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
sources[key] = file;
|
|
50
|
+
combined[key] = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
43
51
|
});
|
|
44
52
|
|
|
45
53
|
return combined;
|
|
@@ -107,9 +115,9 @@ async function mergeJson() {
|
|
|
107
115
|
|
|
108
116
|
const localeFilesMap = findLocaleFiles();
|
|
109
117
|
for (const [locale, files] of Object.entries(localeFilesMap)) {
|
|
110
|
-
const combined = combineJsonFiles(files);
|
|
118
|
+
const combined = combineJsonFiles(locale, files);
|
|
111
119
|
fs.writeFileSync(
|
|
112
|
-
`${
|
|
120
|
+
path.join(destDir, `${locale}.json`),
|
|
113
121
|
JSON.stringify(combined, null, 2)
|
|
114
122
|
);
|
|
115
123
|
}
|
|
@@ -117,5 +125,5 @@ async function mergeJson() {
|
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
mergeJson().catch((err) => {
|
|
120
|
-
errorExit(err)
|
|
128
|
+
errorExit(err, 'build-i18n');
|
|
121
129
|
});
|