@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,273 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
v3 is a major revision focused on security, performance and maintainability.
|
|
6
|
+
It corrects or reworks several earlier decisions and establishes a more solid, stable fundation for the future.
|
|
7
|
+
|
|
8
|
+
### Breaking Changes
|
|
9
|
+
|
|
10
|
+
- **Templates are escaped at output**
|
|
11
|
+
|
|
12
|
+
- New `html` tag, `raw()` and `trustedRaw()`. In an `html`... template, every `${}` is HTML-escaped, so data from any source renders as text and can't inject markup or `data-event`/`data-component` attributes. Nested `html` results are inserted as-is. Unquoted attribute values (`title=${x}`) are quoted automatically, so a value with spaces can't add attributes. `raw()` inserts HTML unescaped: at render it's parsed, stripped of Nutin's binding attributes and sanitized as nodes, so injected markup can't call component methods or mount children; in tag position (`<input ${raw('checked')}>`) it keeps only safe attributes; `trustedRaw()` keeps them, for markup you wrote yourself (the new [`markdown` feature](#features) uses it).
|
|
13
|
+
- `data-optional="${value}"` still removes its element when `value` is `null`/`undefined`: in that attribute, `html` writes them as the literal strings the check looks for. Everywhere else they render as empty.
|
|
14
|
+
- `html` checks URL attributes (`href`, `src`, `action`, `formaction`, `poster`, `background`, `xlink:href`) at every trust level, `trusted` included: a value from data that makes one a `javascript:` URL is replaced with `about:invalid#nutin-blocked`, with a warning in development. At `normal`/`strict` such a link now keeps an inert `href` instead of losing the attribute. A scheme written in the template itself (`href="/p/${id}"`) is left alone. New `SecurityHelper.isScriptUrl()`.
|
|
15
|
+
- `raw()` inside `<textarea>`/`<title>` is inserted as text (entities decoded, tags shown as written, `<` escaped so it can't close the element). This also stops a `raw('</textarea>…')` from breaking out of the element in `String()` of the template. Nested `html` and `trustedRaw()` there are unchanged.
|
|
16
|
+
- `String()` of a template with `raw()` no longer corrupts sources containing `$&`, `$1`, … (they were read as replacement patterns).
|
|
17
|
+
- `data-event` tokens now pass raw values instead of HTML-escaped ones. Removed `SecurityHelper.sanitizeInputElement()`.
|
|
18
|
+
- The default `normal` trust level now also strips `srcdoc` attributes, SVG `<animate>`/`<set>` elements that rewrite a URL attribute and `javascript:` URLs. `data:` URLs are still stripped under `strict` only.
|
|
19
|
+
- `normal` also strips `<style>`, `<link>`, `<base>` and `<meta>` elements, SVG `<script>`, and `data:`/`javascript:` documents in `<iframe>`/`<object>`/`<embed>`. Move `<style>` blocks from templates into the component's `.scss`. `style="…"` attributes are kept.
|
|
20
|
+
- Rendering inserts the sanitized nodes directly instead of assigning the sanitized string to `innerHTML`. New `SecurityHelper.sanitizeToFragment()`; `sanitizeTemplate()` still returns a string.
|
|
21
|
+
- Migration: tag each `template`/`templateFn` with `html` (`import { html } from core`), drop `.join('')` on mapped `html` results, and wrap markup you trust in `raw()`. The build warns about every untagged template that contains `${}`, since its values are no longer escaped anywhere.
|
|
22
|
+
|
|
23
|
+
- **`HttpClient` URLs, bodies and responses**
|
|
24
|
+
|
|
25
|
+
- URLs: endpoints are joined onto the base URL as URLs (not strings) and rejected before `fetch` when they leave its origin or path. Before, `${baseUrl}${endpoint}` let an endpoint like `@other.host/x` send the request and its default headers (e.g. `Authorization`) to another host. The base URL's own query is kept. Without a base URL, a relative endpoint now resolves against the page's origin, and default headers only go to that origin. Only `http:`/`https:` URLs are accepted. Error messages leave out query strings.
|
|
26
|
+
- Encoded `/` and `\` (`%2F`, `%5C`) in a path are rejected, since servers often decode them into separators; list APIs that expect them in the new `trustedAPIs` option.
|
|
27
|
+
- Bodies: `FormData`, `Blob`, `URLSearchParams`, binary data and streams are sent as they are (they used to be sent as `{}`); `0`/`false`/`''` are sent (they were dropped). `Content-Type: application/json` is only set for JSON bodies, so GET/DELETE no longer trigger a CORS preflight. Headers merge case-insensitively, and request interceptors get a `Headers` object.
|
|
28
|
+
- Responses: 204/205 and empty bodies resolve to `undefined` (they threw); `+json` types are parsed (they were returned as text). Response interceptors get a clone, so reading its body no longer breaks the request.
|
|
29
|
+
- Redirects: a followed redirect that ends outside the base URL (or, without one, on another origin than requested) now throws instead of returning that origin's response, and response interceptors don't see it. Browsers strip `Authorization` on cross-origin redirects but forward custom headers (e.g. `X-Api-Key`), so APIs using those should pass `redirect: 'error'`.
|
|
30
|
+
|
|
31
|
+
- **Framework types live in a global `Nutin` namespace**
|
|
32
|
+
|
|
33
|
+
- The public types are now declared in `src/core/internals.d.ts`, inside `declare namespace Nutin`, and no longer exported from `core/index`. Use them without an import: `Nutin.ComponentConfig`, `Nutin.BaseComponentOptions`, `Nutin.ComponentOptions`, `Nutin.ComponentProps`, `Nutin.ViewOptions`, `Nutin.CatalogConfig`, `Nutin.CatalogItemConfig`, `Nutin.CatalogItemObject`, `Nutin.CatalogItemPrimitive`, `Nutin.Template`, `Nutin.TrustLevel`, `Nutin.Routes`, `Nutin.RouteGuard`, `Nutin.RouteConfig`, `Nutin.ViewFactory`, `Nutin.RouteMatch`, `Nutin.Router`, `Nutin.EventBus`, `Nutin.HttpClient`, `Nutin.HttpClientOptions`, `Nutin.RequestConfig`, `Nutin.QueryValue`, `Nutin.HttpMethod`, `Nutin.Language`, `Nutin.Translations`, `Nutin.PipeFunction`, `Nutin.GlobalMountable`, `Nutin.GlobalConfig`, `Nutin.RegisterGlobalsOptions`, `Nutin.Subscription`, and the event maps (`Nutin.EventMap`, `Nutin.EventKey`, `Nutin.AppEventMap`…).
|
|
34
|
+
- `IRouter`, `IEventBus`, `IHttpClient`, `IHttpClientOptions` and `IRequestConfig` lose their `I` prefix: `Nutin.Router`, `Nutin.EventBus`, `Nutin.HttpClient`, `Nutin.HttpClientOptions`, `Nutin.RequestConfig`.
|
|
35
|
+
- Only the name `Nutin` is global, so these types can no longer collide with lib.dom (as `NavigationEventMap` did) or with other packages' global types.
|
|
36
|
+
- Migration: remove these names from your `import { … } from '…/core/index.js'` lines and prefix them with `Nutin.`. In `src/app/globals.d.ts` (not touched by `nutin-update`), wrap your `declare interface AppEventMap { … }` in `declare namespace Nutin { interface AppEventMap { … } }`: until then, emitting an app event is a type error. Classes and functions (`Component`, `View`, `html`, `AppRouter`, …) are still imported as before.
|
|
37
|
+
|
|
38
|
+
- **No teardown on page unload**
|
|
39
|
+
|
|
40
|
+
- Services no longer register a `beforeunload` listener, and the generated `main.ts` no longer calls `Service.destroyAll()` there. That teardown ran before the page was actually left: a page restored by Back from the back/forward cache, or kept after a cancelled "Leave site?" prompt, came back with every service disposed.
|
|
41
|
+
- `dispose()`, `MyService.destroy()` and `Service.destroyAll()` now all run `registerCleanup` callbacks; before, only `dispose()` did.
|
|
42
|
+
- Migration: delete the `window.addEventListener('beforeunload', …Service.destroyAll()…)` block from `src/app/main.ts`, along with its `Service` import if nothing else uses it.
|
|
43
|
+
|
|
44
|
+
- **`data-event` only cancels navigation defaults**
|
|
45
|
+
|
|
46
|
+
- A `data-event` handler now calls `preventDefault()` for link clicks (`<a href>`), form `submit` events and submit-button clicks inside a form, whether or not it takes arguments. Call `@event`'s `preventDefault()` in your handler for any other default you relied on.
|
|
47
|
+
- `TokenHelper.resolve()` no longer calls `preventDefault()`.
|
|
48
|
+
|
|
49
|
+
- **`nutin new`: `-p` replaces `-pm`**
|
|
50
|
+
|
|
51
|
+
- The package manager flag is now `-p, --package-manager`, and only accepts `npm`, `yarn`, `pnpm` or `bun`. Any other value used to end up in the install command and in the generated scripts. `-pm` was a multi-letter short flag that newer Commander versions reject.
|
|
52
|
+
|
|
53
|
+
- **Typed `emit()` payloads and `getInstance()` arguments**
|
|
54
|
+
|
|
55
|
+
- `AppEventBus.emit(event, data)` requires the payload its `Nutin.EventMap` entry declares; `emit('navigate')` no longer compiles. An event declared `undefined` takes none. The framework's payload-less events (`reload`, `before-render`, `after-render`, `before-destroy`, `after-destroy`) are now declared `undefined` instead of `{}`.
|
|
56
|
+
- `MyService.getInstance(...args)` checks its arguments against the service's constructor and returns the service's type. A service without its own constructor accepts no arguments. `Service`'s own constructor takes none.
|
|
57
|
+
- Migration: in `src/app/globals.d.ts`, declare payload-less app events as `undefined` instead of `{}` (or pass `{}` when emitting them); fix the type errors reported for `getInstance()` calls.
|
|
58
|
+
|
|
59
|
+
### Features
|
|
60
|
+
|
|
61
|
+
- **JS-only projects**
|
|
62
|
+
|
|
63
|
+
- `nutin-new --js-only` generates a plain JavaScript project. TypeScript remains the default and recommended option, and documentation only covers TypeScript.
|
|
64
|
+
|
|
65
|
+
- **New `markdown` feature (`nutin-add markdown`)**
|
|
66
|
+
|
|
67
|
+
- Compiles Markdown folders listed in `nutin.config.js`'s `markdownSources.sourceFolders` into `/generated/<name>.json` manifests at build time.
|
|
68
|
+
- Rendered by a `MarkdownView` with navigation and a table of contents. Each folder gets a route from `markdownRoutes()` (spread into `appRoutes`), and its manifest is loaded on first visit (`loadAll()` preloads every manifest).
|
|
69
|
+
- The `markdown` feature's routes are lazy by default: `markdownRoutes({ view })` accepts a factory returning a Promise.
|
|
70
|
+
- Adding a folder only takes a config change. Supports YAML frontmatter, hub (table of contents) files, sections in routes (`/docs/:section?/:slug?`) and internal links between pages.
|
|
71
|
+
- Duplicate slugs and broken links fail the build; content is validated before building. A page listed more than once in a folder's hub files (twice, or in two hubs) fails the build too. Hub entries may use `-`, `*` or `+` bullets; any other list item under `## Table of Contents` is ignored with a warning giving its line. Broken internal Markdown links are all reported at once per page.
|
|
72
|
+
- Heading ids turn accents into plain letters (`Été` → `ete`) and give a heading repeated on a page a `-1`, `-2`, ... suffix. Table of contents entries, page descriptions and hub descriptions are plain text: a description is the first paragraph or list item, without Markdown marks or HTML tags.
|
|
73
|
+
- A folder whose manifest can't be loaded shows "This page couldn't be loaded." (new `loadError` locale key) instead of its empty state, and is tried again on the next visit.
|
|
74
|
+
- Customizable from your own files: `markdownRoutes({ view })` builds each folder's view; `MarkdownView` takes `navComponent`, `contentComponent`, `landingComponent` (your own classes) and `onContentRendered(element, page)` (e.g. syntax highlighting), and its getters are protected. The nav, content and landing components take their template as an optional third constructor argument, and their render helpers are exported.
|
|
75
|
+
- Easy to restyle: the feature's styles have no specificity (`:where()`), so any app rule wins, and sizes are custom properties on `.markdown-view` (`--markdown-gap`, `--markdown-nav-width`, `--markdown-toc-width`, `--markdown-line-height`).
|
|
76
|
+
- `landing` folder option: the bare route shows an index of the folder (hub title, description, groups and pages; with `sectionInPath`, a landing per section) instead of its first page, prerendered and in the sitemap with `generateSEOFiles`. It's styled like the folder's pages (`.markdown-landing` and `.markdown-landing__body` share the content styles).
|
|
77
|
+
- `ogImage` folder option: the default `og:image` of the folder's SEO pages.
|
|
78
|
+
- Without hub files, subfolders are groups in the navigation, titled after the folder (`01-getting-started/` → "Getting started"); a page's frontmatter `group` overrides it.
|
|
79
|
+
- In-app navigation sets `document.title` to the page's title, like its prerendered SEO page.
|
|
80
|
+
- With `generateSEOFiles: true`, every page is prerendered at its own URL with its title, description and `og:image` (new frontmatter `ogImage`), and listed in `sitemap.xml`, with no `config/seo.json` entries needed. A `seo.json` route for the same URL overrides the generated one; `seo: false` on a folder opts it out. Descriptions taken from a page's first paragraph are plain text (no `[link](url)` or `**` marks).
|
|
81
|
+
- i18n: with `i18n: true`, a folder with one subfolder per language (`markdown-content/en/`, `markdown-content/fr/`) is compiled to one manifest per language (`/generated/<name>.<lang>.json`). The default language defines the pages; a missing translation shows the default-language page and is listed in a build warning, and a page that only exists in a translation fails the build. Links carry the `/<lang>` prefix, a language change reloads the current page in the new language, and with `generateSEOFiles` every language gets its own SEO page with `hreflang` alternates. The feature's UI texts are translatable through `src/app/markdown/locales/<lang>.json`.
|
|
82
|
+
- `docker:build` works with the `markdown` feature: the Dockerfile copies every `markdownSources` folder into the image build (folder names with spaces included). A folder outside the project is rejected with a clear error.
|
|
83
|
+
|
|
84
|
+
- **Keeping children across re-renders**
|
|
85
|
+
|
|
86
|
+
- Children can now survive their parent's re-render. Give a single child a `keep` value in `registerChildren()`, or a catalog a `keepBy(item, index)`: while that value stays the same (and, for catalogs, the item is shallow-equal), the child keeps its instance and DOM — its factory isn't called and it isn't re-rendered. Its focus, caret, scroll position and unsaved input are kept. A changed or removed value still destroys and recreates it, and children without one are recreated as before.
|
|
87
|
+
- A kept child is moved into its new placeholder with `Element.moveBefore()`, so it never leaves the document: iframes don't reload, CSS animations keep running and custom elements don't get `disconnectedCallback`. Where `moveBefore()` isn't supported (Safari), it is re-inserted, and focus, caret and scroll are restored.
|
|
88
|
+
- New `onKept()` lifecycle hook, called on a kept child instead of a render.
|
|
89
|
+
|
|
90
|
+
- **Code splitting**
|
|
91
|
+
|
|
92
|
+
- The client bundle is now built with esbuild's code splitting: each dynamic `import()` becomes a chunk fetched only the first time it runs instead of being inlined in `bundle.js`.
|
|
93
|
+
|
|
94
|
+
- **Lazy routes**
|
|
95
|
+
|
|
96
|
+
- A route's view factory may return a Promise: `'/reports': () => import('./views/reports/reports.view.js').then((m) => new m.ReportsView())`. New type `Nutin.ViewFactory`.
|
|
97
|
+
- If a chunk fails to load, the router logs it and does a full page load of the target URL. A URL that fails again within 10 seconds is only logged.
|
|
98
|
+
- Don't use lazy routes for the default route (`/`) (or the `404`, to a lesser extent).
|
|
99
|
+
|
|
100
|
+
- **Focus moves to the new view after navigation**
|
|
101
|
+
|
|
102
|
+
- After an in-app navigation (link, `Navigation.navigateTo()`, back/forward, guard redirect, `/404`), the router focuses the view's `<h1>`, else the `<h1>` in `<main>`, else the view itself while announcing the page title in a polite live region. Keyboard and screen-reader users used to be left at the top of the page with no signal that it had changed.
|
|
103
|
+
- An element without a `tabindex` gets `tabindex="-1"` and no focus ring while focused (it isn't interactive), both removed on blur. The page isn't scrolled to it. The first load and `Navigation.reload()` don't move focus.
|
|
104
|
+
|
|
105
|
+
- **`data-event` handlers are checked**
|
|
106
|
+
|
|
107
|
+
- TypeScript builds check every handler named in a component's or view's template (`data-event="click:_save"`) against its class: a typo fails `tsc`, reported at the template's line with TypeScript's "Did you mean" suggestion. The check references the handlers from the class in `dist-build`'s copy only, for `tsc`, and is removed before bundling. Handlers named with `${}` aren't checked, and a handler used by a subclass's template must be `protected`.
|
|
108
|
+
- Template-only handlers now count as used, so new apps' `tsconfig.json` enables `noUnusedLocals`. Existing apps can add it (`nutin-update` lists it as a difference).
|
|
109
|
+
- At runtime, a `data-event` whose handler doesn't exist logs a `console.warn` naming the component and the method (development builds only). It used to be ignored silently.
|
|
110
|
+
|
|
111
|
+
- **Views can set their document title**
|
|
112
|
+
|
|
113
|
+
- New `View.documentTitle()`: return a title (e.g. the article shown) and the router uses it for `document.title` after each navigation, before `config/seo.json`'s title, the `<viewName>.title` translation and `viewName`.
|
|
114
|
+
|
|
115
|
+
- **`nutin-update` reports `package.json`/`tsconfig.json` drift**
|
|
116
|
+
|
|
117
|
+
- The update summary now lists each `scripts`/`devDependencies` entry and `tsconfig.json` option that differs from what the installed nutin generates. They are listed only: nothing is written.
|
|
118
|
+
|
|
119
|
+
### Changes
|
|
120
|
+
|
|
121
|
+
- **i18n links are localized at render.** A root-relative `<a href="/docs">` is rendered as `/en/docs/` (current language, trailing slash as in the canonical URL), so prerendered SEO pages and copied links point at the real page. They used to link to `/docs`, which served the bare SPA shell with a 200 (no title, canonical or `hreflang`) as a duplicate of `/en/docs/`. External, relative, already-prefixed and file links are left as written; i18n-off builds don't include the code.
|
|
122
|
+
|
|
123
|
+
- **Docker: unprefixed paths redirect with i18n.** The `docker` feature's nginx config 301-redirects every unprefixed, extension-less path (`/docs` → `/en/docs`) to the default language, not only `/`, so old and external links stop getting a duplicate shell. `/health` is now an exact-match location so the redirect can't catch it. Language codes in `config/languages.json` must be letters, digits and `-`.
|
|
124
|
+
|
|
125
|
+
- **Prerendered SEO pages drop Nutin's binding attributes.** `data-event`, `data-i18n`, `data-component`, `data-catalog`, `data-bind`, `data-pipe` and `data-pipe-source` are removed from the HTML written by `generateSEOFiles`: the app renders the page again when it starts, so nothing ever binds them. They're removed on the DOM, so text showing them (code samples) is kept, as are classes, styles and other `data-*` attributes. On nutin.dev: −10% HTML, −6% brotli. `SecurityHelper.stripBindingsFrom(root)`, the in-place version of `stripBindings(markup)`, is now exported.
|
|
126
|
+
|
|
127
|
+
- **`.html` templates are type-checked.** The build now merges `.html` templates into their components and views before running `tsc` (it ran on `src/`, where they were still `__TEMPLATE_PLACEHOLDER__`), so `${task.nmae}` in an `.html` template fails the build like it does in an inline one. Errors are reported at the `.html` file and line. An app upgrading may see errors that were always there. Builds otherwise produce the same output: templates are still minified, after type-checking. `tsc` now runs on `dist-build`'s copies through a generated `dist-build/tsconfig.json` that extends yours (removed at the end of the build).
|
|
128
|
+
|
|
129
|
+
- **Core's static helper classes are now modules of functions.** The 16 helpers made only of static methods (`DomHelper`, `SecurityHelper`, `TokenHelper`, `NavigationManager`, `HttpBuilder`, …) export plain functions, imported as namespaces (`import * as DomHelper from './helpers/dom.helper.js'`). esbuild can now shorten their names and drop what's unused: a fresh app's bundle went from ~34 KB to ~30 KB minified (~10 KB → ~9 KB brotli). Their former `private static` members are no longer exported.
|
|
130
|
+
- Calls are unchanged: `TokenHelper.registerCustomToken()`, `SecurityHelper.escapeHtml()`, `NavigationManager.replaceState()` work as before (`core/index.ts` exports them as namespaces).
|
|
131
|
+
- Code that subclassed a helper, reassigned one of its members, or spied on it in tests (`spyOn(NavigationManager, 'replaceState')`) must change: module namespaces are read-only. Spy on what the function calls instead (e.g. `window.history.replaceState`), or check its effect.
|
|
132
|
+
- `nutin-update` merges the helper files you edited, like any other core file.
|
|
133
|
+
|
|
134
|
+
- **Node 24 required.** The CLI and generated apps now declare `engines.node: ">=24"` (was `>=22`), and the SEO prerender bundle targets `node24`. The Docker feature's image and the CI workflows already used Node 24. Upgraded apps: set `engines.node` in `package.json` by hand (`nutin-update` doesn't manage it).
|
|
135
|
+
- **CLI dependencies upgraded.** handlebars 4.7.10 (fixes a critical advisory), inquirer 14 (drops `tmp`, flagged high), commander 15, fs-extra 11.4. `npm audit` is clean. With commander 15, extra arguments are rejected (`nutin update extra` errors instead of ignoring `extra`).
|
|
136
|
+
|
|
137
|
+
- **`src/core` files import each other directly, not through `core/index.ts`.** The barrel made circular imports (e.g. `Component` → `index.ts` → `BaseComponent` → `index.ts`), which a single bundle tolerated but code splitting doesn't: once core code is shared between chunks, a class could be evaluated before the class it extends (`Class extends value undefined`). Apps keep importing from `core/index.ts`. Internally, `ViewRenderManager.renderNewView()` now takes the view instance instead of its factory.
|
|
138
|
+
|
|
139
|
+
- **`nutin-update` merges files you edited.** A file you changed that also changed upstream used to be left at the old version, with a two-way diff in `NUTIN-UPDATE-REPORT.md`. The files around it were updated, so the project often didn't build until you merged by hand, and nothing said so. Now your file and the new version are merged three-way against the old one (`git merge-file`):
|
|
140
|
+
- Edits that don't overlap are merged cleanly.
|
|
141
|
+
- Overlapping ones get `diff3` conflict markers (yours / the original nutin version / the new one), and the update says the project won't build until they're resolved. This means `nutin-update` now writes to files you edited.
|
|
142
|
+
- Files left with markers are recorded in `.nutin-meta.json` (`unmergedFiles`). Running `nutin-update` again refuses to update while any are left and lists them, instead of saying "Already up to date".
|
|
143
|
+
- `NUTIN-UPDATE-REPORT.md` is now a short list of merged, conflicting and binary files, without diffs.
|
|
144
|
+
- `nutin-update` needs git for this (the binary only, not a repository). Without it, it stops before changing anything.
|
|
145
|
+
- Before writing anything, `nutin-update` checks that the project folder is committed: no changes and no untracked files (ignored files don't count), in a git repository. Otherwise it lists them and stops. So an update can always be undone with `git checkout -- . && git clean -fd`, which it prints when done. New `--allow-dirty` flag to skip the check.
|
|
146
|
+
|
|
147
|
+
- **The router keeps every path on the site.** A path passed to `Navigation.navigateTo()` that combined a dot segment and a backslash (`/./\host`, `/a/..\/host`) was normalized to `//host`, which the browser reads as another host. The 404 view rendered, then `pushState` threw a `SecurityError`; and when the route's view failed to load (a lazy route after a deploy), the full page load went to that host. Paths are now collapsed again after parsing, so they always stay on the current origin (`/host`). This matters when an app navigates to a path it didn't write (e.g. a `?next=` parameter).
|
|
148
|
+
|
|
149
|
+
- **Apps without the router build.** The route check only runs when app code imports `routes.ts` (or `generateSEOFiles` is on, since prerendering uses `appRoutes`), and `index.html` only needs an `id="app"` element in those cases. An app that mounts its components into its own elements (a widget in an existing page, a browser extension popup) needs neither. A mount target that isn't found (a selector matching nothing, or a `null` element) now logs a `console.error` in development instead of leaving the page silently empty. testin-nutin's framework tests still mount into `#app`: without it, keep an empty `<div id="app"></div>` in `src/index.html` or set `testinNutin.includeFramework: false`.
|
|
150
|
+
|
|
151
|
+
- `build:prod` runs `node tools/builder/builder.js --prod` instead of `NODE_ENV=production node …`, which Windows shells don't understand. `NODE_ENV=production` still works, so existing `package.json` scripts keep working.
|
|
152
|
+
|
|
153
|
+
- The dev server (`serve`, `dev`) listens on `127.0.0.1` only, the address it prints, instead of every network interface.
|
|
154
|
+
|
|
155
|
+
- `dev`, `serve`, `serve:prod` and `serve:only` accept a port: `npm run dev -- --port 3000`, `npm run serve -- --port=3000`, or `PORT=3000 npm run dev`. Precedence is `--port` > `PORT` > `9090` (default). Invalid ports fail fast with a clear error.
|
|
156
|
+
|
|
157
|
+
- **live-server is replaced by a built-in dev server** (`tools/dev/serve.js`, `node:http`, no dependency). live-server hasn't been released since 2019 and pulled in outdated dependencies. The new server logs its URL and fails with a `--port` hint when the port is busy (live-server silently moved to a random port). Prerendered SEO pages are served from their folder's `index.html`. Page navigations without a matching file get the SPA shell, even when the URL contains a dot (`/users/john.doe` used to 404); a missing asset gets a 404. `dev` reloads open pages after each successful rebuild (Server-Sent Events, via an external script so a CSP without `'unsafe-inline'` still allows it). `serve`/`serve:prod` no longer live-reload. Existing apps can remove `live-server` from their `devDependencies`.
|
|
158
|
+
|
|
159
|
+
- New apps get `esbuild` 0.28, `jsdom` 30 and `chokidar` 5 (they were on 0.25, 26 and 4). Existing apps keep their versions, and the updated tools work with both.
|
|
160
|
+
|
|
161
|
+
- **New TypeScript apps use TypeScript 7** (`^7.0.2`, the native compiler).
|
|
162
|
+
|
|
163
|
+
- The build's route check (`validate-routes.js`) no longer imports `typescript`, since TypeScript 7 has no JavaScript API. It now uses esbuild for both TS and JS apps and reports the line of each duplicate route. A duplicate key anywhere in `routes.ts` is now reported, not only inside `appRoutes`. The CLI keeps its own pinned TypeScript 5.9.3 for `--js-only` generation; that version is separate from your app's.
|
|
164
|
+
- The generated `tsconfig.json` no longer sets `baseUrl` (removed in TypeScript 7) or the empty `paths`.
|
|
165
|
+
- The global `NavigationEventMap` declaration in `core/internals.d.ts` is now `Nutin.RouterEventMap`. lib.dom now has its own `NavigationEventMap` (the Navigation API), and the two merged into conflicting `navigate` types.
|
|
166
|
+
- To upgrade an existing app: run `nutin-update`, set `"typescript": "^7.0.2"` in `devDependencies`, and remove `baseUrl` and the empty `paths` from `tsconfig.json`. The updated tools also still work on TypeScript 5.
|
|
167
|
+
|
|
168
|
+
- The `dev` watcher now also rebuilds when files are added or deleted (e.g. by `npm run generate`), and when `config/`, `public/` or `nutin.config.js` change. It used to react to edits of existing `src/` files only.
|
|
169
|
+
|
|
170
|
+
- Opt-in dependencies (Tailwind, `markdown`) are installed by one shared helper, `tools/utils/ensure-deps.js`. Outside an interactive terminal (CI, the dev watcher) it no longer installs packages on its own; pass `-- -y` to allow it. The `-y` env variable is `NUTIN_ASSUME_YES`. The Tailwind CLI is started through `node`, so it also works on Windows.
|
|
171
|
+
|
|
172
|
+
- Dynamic SEO routes are written to their real URL: `/blog/:slug` with `mockParams: { "slug": "hello-world" }` goes to `/blog/hello-world/` and is listed in `sitemap.xml` under that URL. Before, it went to a literal `:slug/` folder that no URL reached. New optional `outputPath` route field overrides that path.
|
|
173
|
+
|
|
174
|
+
- Prod builds compress files last, so prerendered SEO pages, `index.html`, `sitemap.xml` and `robots.txt` also get `.gz`/`.br` versions.
|
|
175
|
+
|
|
176
|
+
- testin-nutin mocks match the real services' public APIs.
|
|
177
|
+
|
|
178
|
+
- `I18nService.onLanguageChange(callback)` now returns a function that unsubscribes the callback, like `Navigation.onNavigate()`. `MockI18n` does the same.
|
|
179
|
+
|
|
180
|
+
- New `testin-nutin:only` script runs the tests without rebuilding.
|
|
181
|
+
|
|
182
|
+
- i18n: Two folders with the same name in different places (e.g. `admin/user/` and `public/user/`) used to overwrite each other's translations silently, since locales are keyed by folder name. That now fails the build and names both folders.
|
|
183
|
+
|
|
184
|
+
- `runCommand` no longer goes through a shell outside Windows, so arguments with spaces are passed unsplit.
|
|
185
|
+
|
|
186
|
+
- `dev` no longer leaves the server and watcher running (holding the port) when the terminal tab/window is closed or `dev-serve.js` is killed hard: it now handles `SIGHUP`, and both children exit when their IPC channel to it drops.
|
|
187
|
+
|
|
188
|
+
- Prod builds no longer hash or compress the copied `src/app` and `src/core` sources.
|
|
189
|
+
|
|
190
|
+
- `nutin-add` now also updates base files that depend on the added feature. The change is applied as a patch, so your own edits to those files are kept. When you edited right around it, each inserted block is placed next to the unchanged lines it belongs with instead. If those lines are gone too, the file is left untouched and the patch is printed to apply by hand.
|
|
191
|
+
|
|
192
|
+
- `nutin-add` features share a single `// Nutin features` block in `nutin.config.js`.
|
|
193
|
+
|
|
194
|
+
- New apps still get empty `src/app/components/` and `src/app/services/` folders, but no longer get a `.gitkeep` inside them.
|
|
195
|
+
|
|
196
|
+
- **Smaller prod bundle**: a new app's `bundle.js` goes from ~39 KB to ~30 KB minified (~10 KB gzipped, ~9 KB brotlied).
|
|
197
|
+
|
|
198
|
+
- Core services you don't use are left out of the bundle.
|
|
199
|
+
- With `i18n: false`, the i18n code is left out of the bundle (unless app code references `I18nService`). New `initI18n()` loads the translations when `i18n` is enabled and does nothing otherwise. `main.ts` now calls `await initI18n()` instead of checking `nutinConfig.i18n` itself. `I18nService.initTranslations()` is unchanged.
|
|
200
|
+
- With i18n disabled, a per-language `title` in `config/seo.json` resolves to its first value. It used to depend on the browser's language.
|
|
201
|
+
- The browser bundle no longer includes the tooling-only parts of `nutin.config.js` (`builder`, `testinNutin`, `dockerPorts`, `markdownSources`).
|
|
202
|
+
|
|
203
|
+
### Fixes
|
|
204
|
+
|
|
205
|
+
- i18n lookups only follow a translation file's own keys: `translate('constructor')` returned `Object`'s source text.
|
|
206
|
+
|
|
207
|
+
- The `docker` feature's nginx config sends `X-XSS-Protection: 0` (the old `1; mode=block` is deprecated and can itself be abused in older browsers).
|
|
208
|
+
|
|
209
|
+
- A parent's render no longer applies `data-pipe` a second time to content inside its child components.
|
|
210
|
+
|
|
211
|
+
- When a route guard redirects during back/forward navigation, a `reload()` or the first page load, the address bar now shows the redirect target. The guarded URL is replaced in history instead of being left in place, so Back no longer lands on it again. Redirects from in-app navigation still add a new history entry.
|
|
212
|
+
|
|
213
|
+
- New apps no longer ship with a CSS reset.
|
|
214
|
+
|
|
215
|
+
- A build killed by Ctrl-C, `kill` or a crash no longer leaves `.build-lock` behind. The next build used to wait 2 minutes and then fail. The lock is released on `SIGINT`/`SIGTERM`/`SIGHUP`, and a lock whose process is gone is freed right away.
|
|
216
|
+
|
|
217
|
+
- `$$`, `$&`, `` $` `` and `$'` in component/view templates are kept literally when merged. `Cost: $$5` used to become `Cost: $5`, and `$&` pasted the placeholder back in. The same applies to prerendered SEO pages (`$&` in rendered content duplicated the `#app` element).
|
|
218
|
+
|
|
219
|
+
- SEO files generation
|
|
220
|
+
- `robots.txt`: a bot listed in `disallowBots` stays fully blocked even when a route also names it in `disallow`. A bot with its own group now repeats the global `disallow: true` paths, since robots.txt groups don't inherit `*`. With i18n, disallowed paths cover each `/<lang>/` prefix, and dynamic segments become `*`.
|
|
221
|
+
- `sitemap.xml` leaves out routes disallowed for every bot, and its URLs are XML-escaped.
|
|
222
|
+
- SEO prerendering runs each route's guards first, as an anonymous visitor. A route a guard blocks or redirects (e.g. a logged-out `/admin` sent to `/`) is no longer written as public static HTML. It's also left out of `sitemap.xml`, with a build warning naming it. Guards that allow the route still run, so loader guards have their data when the page renders.
|
|
223
|
+
- SEO prerendering fills any element with `id="app"`, not only `<main id="app">`, and keeps its attributes (other elements used to get empty pages). It fails the build when index.html has no closed `#app` element.
|
|
224
|
+
- SSR's route check passes the locales folder, so a locale fetch during module load no longer throws an unhandled rejection.
|
|
225
|
+
- SEO page generation no longer expands `$&`, `` $` ``, `$'` in `config/seo.json` titles/descriptions (they copied raw page HTML into `<title>`/meta tags). The page URL and `hreflang` links are now HTML-escaped too.
|
|
226
|
+
|
|
227
|
+
- Sass partials (`_name.scss`) under `src/app` are no longer compiled on their own, which duplicated their CSS in the output.
|
|
228
|
+
|
|
229
|
+
- testin-nutin:
|
|
230
|
+
- `toBeLessThan`/`toBeGreaterThan` no longer pass when both values are equal.
|
|
231
|
+
- a `beforeEach`/`afterEach`/`beforeAll`/`afterAll` declared after some `it()` calls now applies to them too.
|
|
232
|
+
- TODO lines now show the real `.test.js` path in TypeScript projects instead of `.test.ts`.
|
|
233
|
+
- `testin-nutin:watch` works and also rebuilds before each run, watches `src/` (plus `tools/` with `includeTools`), and reacts to added and deleted files. A change made while tests are running now triggers another run instead of being dropped.
|
|
234
|
+
- A test file that fails to import (syntax error, bad import) now fails the run with exit code 1. It used to be printed and skipped, so CI passed.
|
|
235
|
+
- The printed `Time` (and the coverage summary's) is the whole run, from the runner's start: it only counted running the queued tests, leaving out Node startup and importing the test files (about a third of the run).
|
|
236
|
+
|
|
237
|
+
- A coverage run that covers no file now reports 0% instead of 100%, so it fails a threshold instead of passing it.
|
|
238
|
+
|
|
239
|
+
- The SSR "unguarded browser global" hint points to the real `tools/builder/core/seo/ssr/ssr-polyfills.js` path.
|
|
240
|
+
|
|
241
|
+
- A component no longer binds, fills or reads the `data-event`, `data-bind` and `data-i18n` elements of nested child components. A child `data-event="click:toggle"` used to fire the parent's `toggle()` too, the parent's `props` overwrote the child's `data-bind` fields, and `getValues()` returned them.
|
|
242
|
+
|
|
243
|
+
- `data-optional` cleanup only looks inside the rendering component. It used to scan the whole document on every render, and it missed components rendered while detached.
|
|
244
|
+
|
|
245
|
+
- `props.className` accepts several space-separated classes; it threw `InvalidCharacterError`.
|
|
246
|
+
|
|
247
|
+
- A comma inside a quoted `data-event` literal (`click:_tag:'a,b'`) no longer splits it into two arguments.
|
|
248
|
+
|
|
249
|
+
- The `date` pipe's time flag reads `"true"`/`"false"` from templates: `date:en-US,long,false` showed the time.
|
|
250
|
+
|
|
251
|
+
- Router:
|
|
252
|
+
- A navigation whose guards resolve after a newer navigation has started is dropped; it used to render over the newer one.
|
|
253
|
+
- The query string is kept by `navigate()`, `reload()`, back/forward and language switches.
|
|
254
|
+
- Route params are URL-decoded (`/users/J%C3%B6rg` → `Jörg`).
|
|
255
|
+
- Static route segments match literally (`.` in a pattern matched any character).
|
|
256
|
+
|
|
257
|
+
- `EventBus.emit()`: a listener that throws is reported with `console.error` and no longer keeps the other listeners from running.
|
|
258
|
+
|
|
259
|
+
- i18n no longer crashes the app at load when the browser blocks site storage (`localStorage` throws a `SecurityError`). Teardown keeps the saved language preference; `resetTranslations()` still forgets it.
|
|
260
|
+
|
|
261
|
+
- Catalog items no longer get the `normalizeKeys` array spread into their props as `0`, `1`, … keys.
|
|
262
|
+
|
|
263
|
+
- Template minification no longer breaks a template that contains a nested `html`, such as ``${items.map((i) => html`<li>${i}</li>`)}``. An inline template was cut at the inner backtick, and the minifier closed the "unclosed" markup, turning the code into a syntax error. The build now finds the real end of each template. A template that can't be minified safely, such as one with `style="${…}"` or a dynamic tag name, is kept as written, with a note in the build output.
|
|
264
|
+
|
|
265
|
+
- `nutin-add` builds the feature template path from the generator's templates root instead of recomputing it. The CLI also drops several unused leftovers.
|
|
266
|
+
|
|
267
|
+
- `nutin new` no longer fails when git isn't installed or `git init` fails. It warns and creates the project without a repository.
|
|
268
|
+
|
|
269
|
+
- `nutin-update` and `nutin-add` in a project without `.nutin-meta.json` no longer crash (stdin closed, e.g. CI) or wait forever (stdin an open pipe) when the shell is non-interactive. They exit with a message that says to run the command in a terminal or create `.nutin-meta.json` by hand, with an example. Nothing is written.
|
|
270
|
+
|
|
3
271
|
## 2.1.1
|
|
4
272
|
|
|
5
273
|
### Changes
|
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# [Nutin](https://www.nutin.
|
|
1
|
+
# [Nutin](https://www.nutin.dev)
|
|
2
2
|
|
|
3
3
|
`The structure of a framework. The freedom of vanilla.`
|
|
4
4
|
|
|
5
5
|
Nutin provides the structure and tooling you'd expect from a framework, while keeping the underlying web platform visible and giving you code ownership.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Nutin is deliberately pragmatic, lightweight, and dependency-free on runtime.
|
|
7
|
+
Nutin is deliberately pragmatic and lightweight, with no runtime dependencies.
|
|
10
8
|
|
|
11
9
|
## Install
|
|
12
10
|
|
|
11
|
+
Requires Node.js 24 or later.
|
|
12
|
+
|
|
13
13
|
```bash
|
|
14
14
|
# install package globally
|
|
15
15
|
npm install -g @nutin/cli
|
|
@@ -23,6 +23,10 @@ nutin-new # or create-nutin-app
|
|
|
23
23
|
|
|
24
24
|
# without global installation
|
|
25
25
|
npx @nutin/cli
|
|
26
|
+
|
|
27
|
+
# options
|
|
28
|
+
nutin-new my-app -p pnpm # package manager (npm, yarn, pnpm or bun), asked otherwise
|
|
29
|
+
nutin-new my-app --js-only # plain JavaScript project (TypeScript is the default)
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
## Update Nutin
|
|
@@ -32,12 +36,16 @@ npx @nutin/cli
|
|
|
32
36
|
nutin-update
|
|
33
37
|
```
|
|
34
38
|
|
|
39
|
+
Your project must be committed first (git repository, no changes or untracked files), so an update can always be undone; `--allow-dirty` skips that check. Files you edited are merged three-way with `git merge-file`. Overlapping edits get conflict markers: the project won't build, and `nutin-update` won't run again, until they're resolved. `NUTIN-UPDATE-REPORT.md` lists merged and conflicting files.
|
|
40
|
+
|
|
35
41
|
## Add Feature
|
|
36
42
|
|
|
37
43
|
```bash
|
|
38
44
|
nutin-add <feature>
|
|
39
45
|
```
|
|
40
46
|
|
|
47
|
+
Features: `docker` (Dockerfile, nginx config, `docker:build`/`docker:run` scripts) and `markdown` (Markdown folders compiled into routed pages).
|
|
48
|
+
|
|
41
49
|
## Documentation
|
|
42
50
|
|
|
43
|
-
- [
|
|
51
|
+
- [Documentation](https://www.nutin.dev/docs)
|
package/bin/add-feature.mjs
CHANGED
|
@@ -4,6 +4,6 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { addFeatureCommand } from '../lib/index.mjs';
|
|
5
5
|
|
|
6
6
|
addFeatureCommand(new Command()).parseAsync(process.argv).catch((err) => {
|
|
7
|
-
console.error(err instanceof Error ? err.
|
|
7
|
+
console.error(err instanceof Error ? err.message : err);
|
|
8
8
|
process.exitCode = 1;
|
|
9
9
|
});
|
package/bin/cli.mjs
CHANGED
package/bin/create-app.mjs
CHANGED
|
@@ -4,6 +4,6 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { createAppCommand } from '../lib/index.mjs';
|
|
5
5
|
|
|
6
6
|
createAppCommand(new Command()).parseAsync(process.argv).catch((err) => {
|
|
7
|
-
console.error(err instanceof Error ? err.
|
|
7
|
+
console.error(err instanceof Error ? err.message : err);
|
|
8
8
|
process.exitCode = 1;
|
|
9
9
|
});
|
package/bin/update-app.mjs
CHANGED
|
@@ -4,6 +4,6 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { updateAppCommand } from '../lib/index.mjs';
|
|
5
5
|
|
|
6
6
|
updateAppCommand(new Command()).parseAsync(process.argv).catch((err) => {
|
|
7
|
-
console.error(err instanceof Error ? err.
|
|
7
|
+
console.error(err instanceof Error ? err.message : err);
|
|
8
8
|
process.exitCode = 1;
|
|
9
9
|
});
|
package/lib/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
2
|
import { print, chalk } from './src/utils/print.mjs';
|
|
3
3
|
import { newAppPrompt } from './src/create-app/new-app-prompt.mjs';
|
|
4
4
|
import { createProject } from './src/create-app/project-generator.mjs';
|
|
5
5
|
import { displaySuccessMessage } from './src/create-app/success-message.mjs';
|
|
6
6
|
import { PACKAGE_VERSION } from './src/common/package-data.mjs';
|
|
7
7
|
import { FEATURES, findFeatureByCli } from './src/common/feature-registry.mjs';
|
|
8
|
+
import { PACKAGE_MANAGERS } from './src/common/package-json-helper.mjs';
|
|
8
9
|
import { addFeatureToProject } from './src/add-feature/feature-adder.mjs';
|
|
9
10
|
import { updateProject } from './src/update-app/project-updater.mjs';
|
|
10
11
|
|
|
@@ -15,7 +16,9 @@ export function createAppCommand(command) {
|
|
|
15
16
|
helpWidth: 100
|
|
16
17
|
})
|
|
17
18
|
.argument('[projectName]', 'Name of the project')
|
|
18
|
-
|
|
19
|
+
// Restricted to known managers: the value ends up in shell commands and package.json scripts.
|
|
20
|
+
.addOption(new Option('-p, --package-manager <manager>', 'Specify package manager').choices(PACKAGE_MANAGERS))
|
|
21
|
+
.option('--js-only', 'Generate a plain JavaScript project (no TypeScript)')
|
|
19
22
|
.action(async (projectName, cliOptions) => {
|
|
20
23
|
print.boldSuccess('\n🚀 Welcome to your new nutin app!\n');
|
|
21
24
|
try {
|
|
@@ -68,6 +71,7 @@ export function updateAppCommand(command) {
|
|
|
68
71
|
})
|
|
69
72
|
.option('-y, --yes', 'Skip the confirmation prompt and apply safe updates immediately')
|
|
70
73
|
.option('--from <path>', 'Use a local templates/ directory as the old baseline instead of fetching from npm')
|
|
74
|
+
.option('--allow-dirty', 'Update even with uncommitted changes or untracked files, or outside a git repository (the update then can\'t be undone with git)')
|
|
71
75
|
.action(async (cliOptions) => {
|
|
72
76
|
print.boldSuccess('🚀 nutin — update');
|
|
73
77
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import * as fsExtra from 'fs-extra';
|
|
3
|
+
import * as Diff from 'diff';
|
|
4
|
+
import { print } from '../utils/print.mjs';
|
|
5
|
+
|
|
6
|
+
const fs = fsExtra.default;
|
|
7
|
+
|
|
8
|
+
// Base templates may carry `{{#if <featureKey>}}` blocks (e.g. the markdown build step in
|
|
9
|
+
// builder.js.hbs). New projects render them with the feature off; this brings an existing
|
|
10
|
+
// project's files in line once the feature is added, by rendering each gated template
|
|
11
|
+
// without and with the feature and applying the difference as a patch.
|
|
12
|
+
// Unrelated user edits to the file are kept.
|
|
13
|
+
function gatesOnFeature(source, featureKey) {
|
|
14
|
+
return new RegExp(`\\{\\{[#^]?(if|unless)\\s+${featureKey}\\b`).test(source);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function findGatedTemplates(dir, featureKey, found = []) {
|
|
18
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
19
|
+
const entryPath = path.join(dir, entry.name);
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
await findGatedTemplates(entryPath, featureKey, found);
|
|
22
|
+
} else if (entry.name.endsWith('.hbs') && gatesOnFeature(await fs.readFile(entryPath, 'utf8'), featureKey)) {
|
|
23
|
+
found.push(entryPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const toLines = (text) => text.replace(/\n$/, '').split('\n');
|
|
30
|
+
|
|
31
|
+
const MAX_ANCHOR_LINES = 3;
|
|
32
|
+
|
|
33
|
+
// The blocks a feature inserts, each with the unchanged lines just before and after it.
|
|
34
|
+
// null when the change also removes or rewrites lines: only insertions can be re-anchored.
|
|
35
|
+
function getInsertions(before, after) {
|
|
36
|
+
const parts = Diff.diffLines(before, after);
|
|
37
|
+
if (parts.some((part) => part.removed)) return null;
|
|
38
|
+
|
|
39
|
+
return parts.flatMap((part, i) => {
|
|
40
|
+
if (!part.added) return [];
|
|
41
|
+
const previous = parts[i - 1] ? toLines(parts[i - 1].value) : [];
|
|
42
|
+
const next = parts[i + 1] ? toLines(parts[i + 1].value) : [];
|
|
43
|
+
return [{ lines: toLines(part.value), previous: previous.slice(-MAX_ANCHOR_LINES), next: next.slice(0, MAX_ANCHOR_LINES) }];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findBlock(lines, block, from = 0) {
|
|
48
|
+
for (let start = from; start + block.length <= lines.length; start++) {
|
|
49
|
+
if (block.every((line, offset) => lines[start + offset] === line)) return start;
|
|
50
|
+
}
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Start of the shortest run of anchor lines (1 to 3, taken from the side next to the
|
|
55
|
+
// inserted block) that appears exactly once in the file and isn't only blank lines, or -1.
|
|
56
|
+
function findUniqueAnchor(lines, anchor, side) {
|
|
57
|
+
for (let size = 1; size <= anchor.length; size++) {
|
|
58
|
+
const block = side === 'previous' ? anchor.slice(-size) : anchor.slice(0, size);
|
|
59
|
+
if (block.every((line) => !line.trim())) continue;
|
|
60
|
+
const first = findBlock(lines, block);
|
|
61
|
+
if (first === -1) return -1;
|
|
62
|
+
if (findBlock(lines, block, first + 1) === -1) return first + (side === 'previous' ? size : 0);
|
|
63
|
+
}
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fallback for files the user changed around the insertion points (e.g. routes added to
|
|
68
|
+
// routes.ts): each missing block goes right after the unchanged lines that preceded it in
|
|
69
|
+
// the template, or right before the ones that followed it, wherever those lines are unique
|
|
70
|
+
// in the file. Returns the patched content, the unchanged content when every block is
|
|
71
|
+
// already there, or null when a block can't be placed.
|
|
72
|
+
function applyInsertions(current, insertions) {
|
|
73
|
+
const lines = toLines(current);
|
|
74
|
+
const missing = insertions.filter((insertion) => findBlock(lines, insertion.lines) === -1);
|
|
75
|
+
if (!missing.length) return current;
|
|
76
|
+
|
|
77
|
+
const placements = [];
|
|
78
|
+
for (const insertion of missing) {
|
|
79
|
+
const afterPrevious = findUniqueAnchor(lines, insertion.previous, 'previous');
|
|
80
|
+
const beforeNext = findUniqueAnchor(lines, insertion.next, 'next');
|
|
81
|
+
if (afterPrevious !== -1) placements.push({ at: afterPrevious, lines: insertion.lines });
|
|
82
|
+
else if (beforeNext !== -1) placements.push({ at: beforeNext, lines: insertion.lines });
|
|
83
|
+
else return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Bottom-up, so earlier indexes stay valid.
|
|
87
|
+
placements.sort((a, b) => b.at - a.at).forEach(({ at, lines: block }) => lines.splice(at, 0, ...block));
|
|
88
|
+
return lines.join('\n') + (current.endsWith('\n') ? '\n' : '');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function patchBaseTemplates(projectPath, feature, context, fileGenerator) {
|
|
92
|
+
const baseDir = path.join(fileGenerator.getTemplatesRoot(), 'base');
|
|
93
|
+
const templates = await findGatedTemplates(baseDir, feature.key);
|
|
94
|
+
|
|
95
|
+
for (const templatePath of templates) {
|
|
96
|
+
const fileName = path.basename(templatePath);
|
|
97
|
+
const before = await fileGenerator.isRenderTemplateFile(templatePath, fileName, { ...context, [feature.key]: false });
|
|
98
|
+
const after = await fileGenerator.isRenderTemplateFile(templatePath, fileName, context);
|
|
99
|
+
if (!before || !after || before.content === after.content) continue;
|
|
100
|
+
|
|
101
|
+
const relativePath = path.join(path.relative(baseDir, path.dirname(templatePath)), after.outputFileName);
|
|
102
|
+
const targetPath = path.join(projectPath, relativePath);
|
|
103
|
+
const patch = Diff.createPatch(relativePath, before.content, after.content);
|
|
104
|
+
|
|
105
|
+
const current = (await fs.pathExists(targetPath)) ? await fs.readFile(targetPath, 'utf8') : null;
|
|
106
|
+
|
|
107
|
+
if (current !== null) {
|
|
108
|
+
if (current === after.content || Diff.applyPatch(current, Diff.reversePatch(Diff.parsePatch(patch)[0])) !== false) {
|
|
109
|
+
print.gray(`Already up to date: ${relativePath}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const patched = Diff.applyPatch(current, patch);
|
|
114
|
+
if (patched !== false) {
|
|
115
|
+
await fs.writeFile(targetPath, patched);
|
|
116
|
+
print.info(`Updated ${relativePath}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const insertions = getInsertions(before.content, after.content);
|
|
121
|
+
const inserted = insertions && applyInsertions(current, insertions);
|
|
122
|
+
if (inserted === current) {
|
|
123
|
+
print.gray(`Already up to date: ${relativePath}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (inserted) {
|
|
127
|
+
await fs.writeFile(targetPath, inserted);
|
|
128
|
+
print.info(`Updated ${relativePath}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
print.warn(
|
|
134
|
+
`Could not update ${relativePath} automatically (${current === null ? 'file not found' : 'it was modified'}). ` +
|
|
135
|
+
`Apply this change by hand for ${feature.key} to work:`
|
|
136
|
+
);
|
|
137
|
+
print.gray(patch.split('\n').slice(4).join('\n'));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
2
|
import * as fsExtra from 'fs-extra';
|
|
4
3
|
import { print } from '../utils/print.mjs';
|
|
5
4
|
import { FileGenerator } from '../common/file-generator.mjs';
|
|
6
5
|
import { FEATURES } from '../common/feature-registry.mjs';
|
|
7
6
|
import { PACKAGE_VERSION } from '../common/package-data.mjs';
|
|
8
|
-
import { installDependencies } from '../common/package-json-helper.mjs';
|
|
9
7
|
import { readProjectMeta, updateProjectMeta } from '../common/project-meta.mjs';
|
|
10
8
|
import { FeatureContextBuilder } from './feature-context-builder.mjs';
|
|
11
9
|
import { updatePackageJson } from './package-json-updater.mjs';
|
|
12
10
|
import { updateNutinConfig } from './nutin-config-updater.mjs';
|
|
11
|
+
import { patchBaseTemplates } from './base-template-patcher.mjs';
|
|
13
12
|
|
|
14
13
|
const fs = fsExtra.default;
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = path.dirname(__filename);
|
|
17
14
|
|
|
18
15
|
export class FeatureAdder {
|
|
19
16
|
constructor() {
|
|
@@ -28,7 +25,7 @@ export class FeatureAdder {
|
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
const projectPath = process.cwd();
|
|
31
|
-
await this.validateProject(projectPath
|
|
28
|
+
await this.validateProject(projectPath);
|
|
32
29
|
|
|
33
30
|
const meta = await readProjectMeta(projectPath);
|
|
34
31
|
if (meta?.features?.[feature.key]) {
|
|
@@ -44,9 +41,10 @@ export class FeatureAdder {
|
|
|
44
41
|
await this.runPostAddTasks(projectPath, feature, context);
|
|
45
42
|
|
|
46
43
|
print.boldInfo(`${feature.key} added.`);
|
|
44
|
+
this.printNextSteps(feature, context);
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
async validateProject(projectPath
|
|
47
|
+
async validateProject(projectPath) {
|
|
50
48
|
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
51
49
|
if (!(await fs.pathExists(packageJsonPath))) {
|
|
52
50
|
throw new Error(`No package.json found in ${projectPath} — run this from a nutin project's root.`);
|
|
@@ -57,12 +55,13 @@ export class FeatureAdder {
|
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
async applyFeatureTemplate(projectPath, feature, context) {
|
|
60
|
-
const featureTemplateDir = path.join(
|
|
58
|
+
const featureTemplateDir = path.join(this.fileGenerator.getTemplatesRoot(), 'features', feature.key);
|
|
61
59
|
|
|
62
60
|
await this.fileGenerator.processTemplateDirectory(featureTemplateDir, projectPath, context, { skipExisting: true });
|
|
63
61
|
|
|
64
62
|
await updatePackageJson(projectPath, feature);
|
|
65
63
|
await updateNutinConfig(projectPath, feature);
|
|
64
|
+
await patchBaseTemplates(projectPath, feature, context, this.fileGenerator);
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
async runPostAddTasks(projectPath, feature, context) {
|
|
@@ -71,8 +70,20 @@ export class FeatureAdder {
|
|
|
71
70
|
packageManager: context.packageManager,
|
|
72
71
|
features: { [feature.key]: true },
|
|
73
72
|
});
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
// No feature adds dependencies here: markdown's are installed by its first build
|
|
74
|
+
// (tools/utils/ensure-deps.js), docker has none.
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
printNextSteps(feature, context) {
|
|
78
|
+
if (feature.key === 'docker') {
|
|
79
|
+
print.info('\nNext steps:');
|
|
80
|
+
print.gray(' Edit dockerPorts in nutin.config.js.');
|
|
81
|
+
}
|
|
82
|
+
if (feature.key === 'markdown') {
|
|
83
|
+
print.info('\nNext steps:');
|
|
84
|
+
print.gray(' 1. List your Markdown folders in nutin.config.js "markdownSources.sourceFolders" — each one is served at /<folder name>. (Start with the generated default markdown-content).');
|
|
85
|
+
print.gray(` 2. Run "${context.packageManager} run build" — you will be asked to install marked and gray-matter (pass "-- -y" to accept non-interactively).`);
|
|
86
|
+
}
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
|