@nutin/cli 2.0.0 → 2.1.1
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 +71 -0
- package/README.md +5 -8
- package/lib/src/create-app/success-message.mjs +2 -3
- package/lib/src/update-app/project-updater.mjs +6 -0
- package/package.json +4 -4
- package/templates/base/.gitignore.hbs +1 -0
- package/templates/base/AGENTS.md.hbs +3 -3
- package/templates/base/GETTING_STARTED.md.hbs +40 -65
- package/templates/base/nutin.config.js.hbs +1 -8
- package/templates/base/src/app/components/.gitkeep +0 -0
- package/templates/base/src/app/main.ts.hbs +0 -1
- package/templates/base/src/app/routes.ts.hbs +2 -1
- package/templates/base/src/app/services/.gitkeep +0 -0
- package/templates/base/src/app/views/home/home.view.ts.hbs +1 -1
- package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +1 -1
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +46 -0
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +5 -2
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +45 -0
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +15 -1
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +14 -3
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +10 -3
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +4 -1
- package/templates/base/src/core/base-classes/view/view.test.js.hbs +11 -14
- package/templates/base/src/core/base-classes/view/view.ts.hbs +8 -9
- package/templates/base/src/core/config.ts.hbs +3 -0
- package/templates/base/src/core/services/globals/globals.test.js.hbs +38 -0
- package/templates/base/src/core/services/globals/globals.ts.hbs +19 -10
- package/templates/base/src/core/services/http-client/http-client.test.js.hbs +30 -2
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +4 -1
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +142 -0
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +42 -4
- package/templates/base/src/core/services/router/router.test.js.hbs +93 -0
- package/templates/base/src/core/services/router/router.ts.hbs +26 -15
- 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.js.hbs +8 -2
- 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/prod-bundle/esbuild.js.hbs +11 -2
- package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +19 -3
- package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +24 -1
- package/templates/base/tools/builder/core/seo/route-output.js.hbs +2 -2
- package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +5 -1
- package/templates/base/tools/builder/core/styles/sass.js.hbs +5 -4
- package/templates/base/tools/dev/dev-serve.js.hbs +43 -4
- package/templates/base/tools/dev/watcher.js.hbs +42 -26
- package/templates/base/tools/generator/generator.js.hbs +32 -47
- package/templates/base/tools/generator/handle-file.js.hbs +2 -24
- package/templates/base/tools/generator/handle-file.test.js.hbs +1 -20
- package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +5 -1
- package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +11 -1
- package/templates/base/tools/generator/templates/view/view.template.js.hbs +1 -1
- package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
- package/templates/base/tools/utils/build-lock.js.hbs +69 -0
- package/templates/base/tools/utils/index.js.hbs +1 -0
- package/templates/features/docker/tools/docker/Dockerfile.template.hbs +2 -2
- package/templates/features/docker/tools/docker/nginx.conf.template +15 -0
- package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +12 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.1
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- `src/app` no longer has an imposed structure.
|
|
8
|
+
|
|
9
|
+
The generator now creates elements in `src/app/<path>/` instead of `src/app/<type>s/<path>/` (e.g. `generate view user/user-view` → `src/app/user/user-view/`).
|
|
10
|
+
|
|
11
|
+
New projects still ship `components/`, `services/` (empty) and `views/` as the suggested layout, so pass the folder explicitly to keep it (`generate component components/my-component`).
|
|
12
|
+
|
|
13
|
+
- Removed the `src/app/{components,views,services}/index.ts` barrel files from new projects, and the generator no longer appends exports to them.
|
|
14
|
+
|
|
15
|
+
Import app elements by their file path.
|
|
16
|
+
|
|
17
|
+
Import Nutin elements by the framework's barrel (`src/core/index.ts`).
|
|
18
|
+
|
|
19
|
+
- Sass now compiles every `.scss` file under `src/app` instead of only `src/app/components` and `src/app/views`.
|
|
20
|
+
|
|
21
|
+
- The generator now exits before writing anything if the target folder already exists and isn't empty, since folder names carry no type suffix.
|
|
22
|
+
|
|
23
|
+
- Dev builds (`dev`/`serve`/`build`) now bundle with esbuild too, unminified with a sourcemap and `console`/`debugger` kept, so npm runtime dependencies work in dev without import maps or vendoring. tsc's per-file output is still emitted for testin-nutin.
|
|
24
|
+
|
|
25
|
+
- Sass now resolves `@use "pkg:<package>/<path>"` imports through `node_modules`.
|
|
26
|
+
|
|
27
|
+
- The default `builder.esbuild.target` in `nutin.config.js` is now `es2020` (was `es2015`), which libraries relying on native `async` functions (e.g. Alpine) require. Existing projects keep their own `nutin.config.js` value.
|
|
28
|
+
|
|
29
|
+
- Generated SEO HTML now includes `hreflang` alternates (one per language plus `x-default`) when i18n is enabled, `og:type` and `twitter:card` (`summary_large_image`) meta tags, and an absolute `og:image`/`twitter:image` URL (a relative `ogImage` in `config/seo.json` is now prefixed with `baseUrl`).
|
|
30
|
+
|
|
31
|
+
- The router now supports `#hash` fragments: navigating to `path#id` keeps the hash in the URL and scrolls to the matching element, falling back to the top of the page. The hash is also kept on reload and on language change.
|
|
32
|
+
|
|
33
|
+
- New `NavigationManager.replaceState(path)` (exported from `core/index.ts`) rewrites the current URL without adding a history entry. The router now updates history before rendering the new view, so a view's `onEnter()` can call it without the change being overwritten.
|
|
34
|
+
|
|
35
|
+
- Docker feature: `tools/docker/Dockerfile.template` now defaults to Node `24.21.0` (was `22.23.2`) Alpine `3.24` (was `3.20`, end-of-life) for the nginx runtime image (nginx `1.26` → `1.30`).
|
|
36
|
+
|
|
37
|
+
- Docker feature: in i18n projects, nginx now redirects a bare `/` to the default language (generated into `nginx.conf` through a new `__ROOT_REDIRECT_PLACEHOLDER__` token).
|
|
38
|
+
|
|
39
|
+
### Fixes
|
|
40
|
+
|
|
41
|
+
- `generate` with a missing argument now prints its usage message instead of crashing.
|
|
42
|
+
|
|
43
|
+
- Attributes set on a `data-component` placeholder (`class`, `id`, `aria-*`, `data-*`, …) are now kept on the mounted component's element instead of being dropped. Classes are merged with the component's own.
|
|
44
|
+
|
|
45
|
+
- Builds now take a lock (`.build-lock/`, gitignored) before touching `dist-build`, so concurrent builds (a manual build during `dev`, or stray watcher processes) no longer corrupt each other's output.
|
|
46
|
+
|
|
47
|
+
- The dev watcher no longer drops a file change made while a rebuild is running. It now rebuilds again once the current build finishes.
|
|
48
|
+
|
|
49
|
+
- Stopping `dev` now kills the whole process group of live-server and the watcher (including an in-flight build), so no orphaned watcher processes are left behind. On Windows, the process tree is killed with `taskkill` instead.
|
|
50
|
+
|
|
51
|
+
- SSR now renders views into a real `<main id="app">`, so unset `data-optional` fields are removed from prerendered HTML instead of leaking the literal text `undefined`.
|
|
52
|
+
|
|
53
|
+
- Ctrl/Cmd/Shift/Alt-clicks and middle-clicks on `<a href>` elements with a `data-event` click handler now fall through to the browser's native behaviour (e.g. open in a new tab), instead of being intercepted.
|
|
54
|
+
|
|
55
|
+
- Docker feature: nginx now issues relative redirects (`absolute_redirect off`), so the trailing-slash redirect on prerendered routes no longer leaks the container's internal port or scheme behind a proxy. A route directory without its own `index.html` now serves the SPA shell instead of a 403.
|
|
56
|
+
|
|
57
|
+
## 2.1.0
|
|
58
|
+
|
|
59
|
+
### Breaking Changes
|
|
60
|
+
|
|
61
|
+
- `View`'s `viewName` is now a required constructor option. Every `View` subclass must now pass `viewName` explicitly.
|
|
62
|
+
- Removed the `generator` object from `nutin.config.js`. The generator now always scaffolds a stylesheet, and gates locale/test file generation on the existing `i18n` and `testinNutin.includeApp` flags.
|
|
63
|
+
|
|
64
|
+
### Fixes
|
|
65
|
+
|
|
66
|
+
- Restored `document.title` updates on route change, regressed silently in 2.0.0's rewrite, default to the view's `viewName`.
|
|
67
|
+
- Fixed leaked-listener/subscription bug that affected every re-rendering component. Previously-mounted child components (single or catalog) are now properly destroyed before a re-render mounts their replacements.
|
|
68
|
+
- `HttpClient`'s constructor `defaultHeaders` are now actually merged into outgoing requests (previously silently dropped — only per-call `config.headers` were sent).
|
|
69
|
+
- `data-optional` elements with literal `"null"` text content (e.g. `<span data-optional>null</span>`) are now correctly removed, matching the existing `"undefined"` handling.
|
|
70
|
+
- `revealGlobals` now restores the element's original `display` value by default (captured automatically by `hideGlobals`), instead of always forcing `block`; an optional second argument still allows an explicit override.
|
|
71
|
+
- A `data-pipe` chain segment with an empty name (e.g. `data-pipe=":arg"`) now logs a warning and is skipped, instead of silently aborting the entire chain with no write-back.
|
|
72
|
+
- Removed a redundant duplicate translation-file fetch on startup when i18n is enabled.
|
|
73
|
+
|
|
3
74
|
## 2.0.0
|
|
4
75
|
|
|
5
76
|
Version 2.0.0 focuses on simplicity and clear APIs over feature accumulation: fewer abstractions, clearer responsibilities, and a more focused API.
|
package/README.md
CHANGED
|
@@ -28,19 +28,16 @@ npx @nutin/cli
|
|
|
28
28
|
## Update Nutin
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
# update
|
|
31
|
+
# update to latest version while preserving your changes
|
|
32
32
|
nutin-update
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
## Add
|
|
35
|
+
## Add Feature
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
|
|
39
|
-
nutin-add docker
|
|
38
|
+
nutin-add <feature>
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
##
|
|
41
|
+
## Documentation
|
|
43
42
|
|
|
44
|
-
- [
|
|
45
|
-
- [First app tutorial](https://www.nutin.org/tutorial)
|
|
46
|
-
- [Repository documentation](https://github.com/LoisKOUNINEF/nutin/tree/main/docs)
|
|
43
|
+
- [Repository documentation](https://github.com/LoisKOUNINEF/nutin/tree/main/docs/API.md)
|
|
@@ -3,12 +3,11 @@ import { PACKAGE_HOMEPAGE } from '../common/package-data.mjs';
|
|
|
3
3
|
|
|
4
4
|
export function displaySuccessMessage(answers) {
|
|
5
5
|
const { projectName, packageManager, deployHelper } = answers;
|
|
6
|
+
print.info(`Visit https://${PACKAGE_HOMEPAGE}`);
|
|
7
|
+
|
|
6
8
|
print.boldSuccess('\n🎉 Your project is ready!\n');
|
|
7
9
|
|
|
8
10
|
print.boldInfo('Next steps:');
|
|
9
11
|
print.info(`cd ${projectName}`);
|
|
10
12
|
print.info(`${packageManager} run serve\n`);
|
|
11
|
-
|
|
12
|
-
print.boldInfo('Documentation:');
|
|
13
|
-
print.info(`https://${PACKAGE_HOMEPAGE}\n`);
|
|
14
13
|
}
|
|
@@ -98,6 +98,12 @@ export class ProjectUpdater {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
if (!yes && (toUpdate.length > 0 || toAdd.length > 0)) {
|
|
101
|
+
if (!process.stdin.isTTY) {
|
|
102
|
+
print.warn('\n⚠️ Non-interactive shell detected — skipping the confirmation prompt.');
|
|
103
|
+
print.boldInfo('Re-run with --yes to apply these updates automatically. No files were changed.');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
const { proceed } = await inquirer.prompt([
|
|
102
108
|
{
|
|
103
109
|
type: 'confirm',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nutin/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "The structure of a framework. The freedom of vanilla.",
|
|
5
5
|
"preferGlobal": true,
|
|
6
6
|
"main": "lib/index.mjs",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"minor": "npm version minor -m 'CI/CD: Bump version to %s'",
|
|
18
18
|
"major": "npm version major -m 'CI/CD: Bump version to %s'",
|
|
19
19
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
20
|
-
"
|
|
20
|
+
"release": "npm publish --access public"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"Nutin",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
},
|
|
43
43
|
"repository": {
|
|
44
44
|
"type": "git",
|
|
45
|
-
"url": "https://github.com/
|
|
45
|
+
"url": "git+https://github.com/LoisKOUNINEF/nutin.git"
|
|
46
46
|
},
|
|
47
47
|
"bugs": {
|
|
48
|
-
"url": "https://github.com/
|
|
48
|
+
"url": "https://github.com/LoisKOUNINEF/nutin/issues"
|
|
49
49
|
},
|
|
50
50
|
"homepage": "www.nutin.org"
|
|
51
51
|
}
|
|
@@ -19,13 +19,14 @@ nutin.config.js build/feature config — see below
|
|
|
19
19
|
|
|
20
20
|
- Favor small, composable components.
|
|
21
21
|
- One `View` = one route.
|
|
22
|
-
- Import
|
|
22
|
+
- Import framework code from the `core/index.js` barrel — do not deep-import into `core/`. App elements have no barrels: import them by file path.
|
|
23
|
+
- `src/app/` layout is free; `components/`, `views/`, `services/` is the default suggestion.
|
|
23
24
|
|
|
24
25
|
## Commands
|
|
25
26
|
|
|
26
27
|
- Serve: `{{packageManager}} run serve` (build + serve), `{{packageManager}} run serve:only` (no rebuild), `{{packageManager}} run dev` (watch mode).
|
|
27
28
|
- Build: `{{packageManager}} run build` (dev) or `{{packageManager}} run build:prod` (production).
|
|
28
|
-
- Generate: `{{packageManager}} run generate ELEMENT ELEMENT_NAME` (`ELEMENT` = `component`/`view`/`service
|
|
29
|
+
- Generate: `{{packageManager}} run generate ELEMENT ELEMENT_NAME` (`ELEMENT` = `component`/`view`/`service`; `ELEMENT_NAME` may be a nested path, generated in `src/app/<path>/`; fails if that folder already exists and isn't empty).
|
|
29
30
|
- Test: `{{packageManager}} run testin-nutin` (build + run), `{{packageManager}} run testin-nutin:only` (no rebuild), `{{packageManager}} run testin-nutin:watch`.
|
|
30
31
|
|
|
31
32
|
## Base classes
|
|
@@ -119,7 +120,6 @@ AppPipeRegistry.register('json');
|
|
|
119
120
|
|
|
120
121
|
Central options/tools config (see the inline comments for details):
|
|
121
122
|
- `i18n`, `tailwind`, `generateSEO`, `dockerPorts` — top-level feature toggles (`dockerPorts` only applies once the docker feature is added).
|
|
122
|
-
- `generator.{generateStylesheet, generateLocales, generateTest}` — what the generator scaffolds alongside new elements.
|
|
123
123
|
- `builder.{sass.paths, esbuild}` — build pipeline behavior.
|
|
124
124
|
- `testinNutin.{includeFramework, includeTools, includeApp, jsdomOptions}` — test discovery/environment.
|
|
125
125
|
|
|
@@ -3,22 +3,19 @@
|
|
|
3
3
|
## Table of contents
|
|
4
4
|
|
|
5
5
|
- [Project structure](#project-structure)
|
|
6
|
-
- [
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
6
|
+
- [How do I run my app?](#running-your-app)
|
|
7
|
+
- [How do I generate components, views and services?](#generate-components-views-and-services)
|
|
8
|
+
- [What does a typical component look like?](#a-typical-component)
|
|
9
|
+
- [Options and configuration](#options-and-configuration)
|
|
10
10
|
- [Styling](#styling)
|
|
11
|
-
- [Global layout components](#global-layout-components)
|
|
12
11
|
- [Testing](#testing)
|
|
13
|
-
- [
|
|
12
|
+
- [Go further](#go-further)
|
|
14
13
|
|
|
15
14
|
## Project structure
|
|
16
15
|
|
|
17
16
|
```
|
|
18
|
-
config/ -- configure i18n and SEO files
|
|
19
|
-
|
|
20
17
|
src/
|
|
21
|
-
app/
|
|
18
|
+
app/ -- suggested layout: organize src/app however you like
|
|
22
19
|
components/
|
|
23
20
|
services/
|
|
24
21
|
views/
|
|
@@ -33,7 +30,17 @@ tools/ -- build, development, generator and testing
|
|
|
33
30
|
nutin.config.js -- features and configuration
|
|
34
31
|
```
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
### Imports
|
|
34
|
+
|
|
35
|
+
- Imports use **relative paths** and end with `.js` extension.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { Foo } from 'relative/path/to/foo.js';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Use Nutin's code barrel file (`src/core/index.js`) to import Nutin's modules.
|
|
42
|
+
|
|
43
|
+
## How do I run my app?
|
|
37
44
|
|
|
38
45
|
```bash
|
|
39
46
|
# Development
|
|
@@ -45,29 +52,22 @@ nutin.config.js -- features and configuration
|
|
|
45
52
|
{{packageManager}} run build:prod # build the application for production
|
|
46
53
|
```
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Imports must use **relative paths** and end with `.js` extension.
|
|
51
|
-
|
|
52
|
-
```js
|
|
53
|
-
import { Foo } from 'relative/path/to/foo.js';
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Generate components, views and services
|
|
57
|
-
|
|
58
|
-
Generated elements are automatically exported from the corresponding barrel file:
|
|
59
|
-
`src/app/<components|views|services>/index.ts`.
|
|
55
|
+
## How do I generate components, views and services?
|
|
60
56
|
|
|
57
|
+
Elements are generated in `src/app/<path>/`, so you choose the project structure. The generator stops if the target folder already exists and isn't empty.
|
|
61
58
|
|
|
62
59
|
```bash
|
|
63
|
-
{{packageManager}} run generate <component|view|service> ELEMENT_NAME
|
|
60
|
+
{{packageManager}} run generate <component|view|service> PATH/TO/ELEMENT_NAME
|
|
64
61
|
|
|
65
62
|
# Examples
|
|
66
|
-
{{packageManager}} run generate component my-component # Target dir: src/app/components/my-component/
|
|
67
|
-
{{packageManager}} run generate
|
|
63
|
+
{{packageManager}} run generate component components/my-component # Target dir: src/app/components/my-component/
|
|
64
|
+
{{packageManager}} run generate view user/user-view # Target dir: src/app/user/user-view/
|
|
65
|
+
{{packageManager}} run generate service user/user-service # Target dir: src/app/user/user-service/
|
|
68
66
|
```
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
Import generated elements by their file path (e.g. `./views/home/home.view.js`).
|
|
69
|
+
|
|
70
|
+
## What does a typical component look like?
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
73
|
components/
|
|
@@ -146,29 +146,21 @@ A template can also be written directly inline (skip the `.html` file and the pl
|
|
|
146
146
|
|
|
147
147
|
Defining both an inline template and an external `.html` file, or neither, fails the build.
|
|
148
148
|
|
|
149
|
-
##
|
|
149
|
+
## Options and configuration
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
### Main file
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
`nutin.config.js` - Main Nutin configuration and tool options. See the inline comments for details.
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- `tailwind`, `i18n`, `generateSEOFiles` — top-level features, `false` by default.
|
|
158
|
-
- `generator.*` — controls what the generator scaffolds.
|
|
155
|
+
- `tailwind`, `i18n`, `generateSEOFiles` — top-level features, `false` by default.
|
|
156
|
+
See [Options and features](https://github.com/LoisKOUNINEF/nutin/tree/main/docs/OPTIONS_AND_FEATURES.md).
|
|
159
157
|
- `builder.*` — controls build behavior.
|
|
160
|
-
- `testinNutin.*` — controls
|
|
161
|
-
|
|
162
|
-
### `config/seo.json`
|
|
158
|
+
- `testinNutin.*` — controls Nutin's built-in testing toolkit.
|
|
163
159
|
|
|
164
|
-
|
|
165
|
-
- Data is used to generate static `index.html` per route, `robots.txt`, and `sitemap.xml`.
|
|
166
|
-
- Unused if `nutin.config.js -> generateSEOFiles` is disabled.
|
|
160
|
+
### Specialized files
|
|
167
161
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
- Register supported languages for i18n here.
|
|
171
|
-
- Unused if `nutin.config.js -> i18n` is disabled.
|
|
162
|
+
- `config/seo.json` - with `generateSEOFiles` enabled. Register your routes' description, title and ogImage here.
|
|
163
|
+
- `config/languages.json` - with `i18n` enabled. Register supported languages here.
|
|
172
164
|
|
|
173
165
|
## Styling
|
|
174
166
|
|
|
@@ -178,26 +170,11 @@ Main Nutin configuration and tool options. See the inline comments for details.
|
|
|
178
170
|
|
|
179
171
|
**Note:** Installing Tailwind CSS dependencies may print `npm warn install-scripts`. This is expected with npm 12's default install-script allowlisting and is safe to ignore.
|
|
180
172
|
|
|
181
|
-
## Global layout components
|
|
182
|
-
|
|
183
|
-
If you need components to render outside `<main>` (such as a header or footer), register them with `registerGlobals()` in `main.ts` constructor.
|
|
184
|
-
|
|
185
|
-
Global components are mounted directly in `<body>` and remain present across route changes. Use `before` for components that should appear before `<main>` and `after` for components that should appear after it.
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
registerGlobals({
|
|
189
|
-
before: [{ component: HeaderComponent, id: 'header' }],
|
|
190
|
-
after: [{ component: FooterComponent, id: 'footer' }],
|
|
191
|
-
});
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
**Note**: `hideGlobals(globalIds: string[])` / `revealGlobals(globalIds: string[])` are exported alongside `registerGlobals` and can be called to toggle visibility of the ids you registered.
|
|
195
|
-
|
|
196
173
|
## Testing
|
|
197
174
|
|
|
198
175
|
You can use any testing tool you prefer for application code.
|
|
199
176
|
|
|
200
|
-
This project includes
|
|
177
|
+
This project includes [testin-nutin](https://github.com/LoisKOUNINEF/nutin/tree/main/docs/TESTING.md), Nutin's lightweight testing toolkit. Nutin itself is tested exclusively with it, and the tests are included in the generated project so you can:
|
|
201
178
|
- use them as executable documentation;
|
|
202
179
|
- run and modify Nutin's tests when modifying its code.
|
|
203
180
|
|
|
@@ -205,7 +182,7 @@ Run the tests with:
|
|
|
205
182
|
|
|
206
183
|
`{{packageManager}} run testin-nutin`
|
|
207
184
|
|
|
208
|
-
To use
|
|
185
|
+
To use testin-nutin for application tests, enable:
|
|
209
186
|
|
|
210
187
|
```js
|
|
211
188
|
// nutin.config.js
|
|
@@ -218,10 +195,8 @@ testinNutin: {
|
|
|
218
195
|
import { MyModule } from '#root/dist/src/path/to/my-module.js';
|
|
219
196
|
```
|
|
220
197
|
|
|
221
|
-
|
|
198
|
+
## Go further
|
|
222
199
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
- [Nutin website](https://www.nutin.org)
|
|
200
|
+
- [Repository documentation](https://github.com/LoisKOUNINEF/nutin/tree/main/docs/API.md)
|
|
226
201
|
- [First app tutorial](https://www.nutin.org/tutorial)
|
|
227
|
-
- [
|
|
202
|
+
- [Nutin website](https://www.nutin.org)
|
|
@@ -3,13 +3,6 @@ export default {
|
|
|
3
3
|
tailwind: false, // Enable Tailwind CSS v4
|
|
4
4
|
i18n: false, // Enable i18n; configure languages in config/languages.json
|
|
5
5
|
generateSEOFiles: false, // Generate per-route HTML, robots.txt and sitemap.xml; configure SEO data in config/seo.json
|
|
6
|
-
|
|
7
|
-
// Components / Views scaffolding
|
|
8
|
-
generator: {
|
|
9
|
-
generateStylesheet: false, // Generate a .scss file alongside each component or view
|
|
10
|
-
generateLocales: false, // Generate locale files alongside components and views (requires i18n)
|
|
11
|
-
generateTest: false, // Generate test files alongside components, views and services (requires testinNutin.includeApp)
|
|
12
|
-
},
|
|
13
6
|
|
|
14
7
|
// Build pipeline
|
|
15
8
|
builder: {
|
|
@@ -23,7 +16,7 @@ export default {
|
|
|
23
16
|
bundle: true,
|
|
24
17
|
minify: true,
|
|
25
18
|
sourcemap: false,
|
|
26
|
-
target: ['
|
|
19
|
+
target: ['es2020'],
|
|
27
20
|
drop: ['console', 'debugger'],
|
|
28
21
|
},
|
|
29
22
|
},
|
|
File without changes
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Routes } from '../core/index.js';
|
|
2
|
-
import { HomeView
|
|
2
|
+
import { HomeView } from './views/home/home.view.js';
|
|
3
|
+
import { NotFoundView } from './views/not-found/not-found.view.js';
|
|
3
4
|
|
|
4
5
|
export const appRoutes: Routes = {
|
|
5
6
|
'/': () => new HomeView(),
|
|
File without changes
|
package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChildrenHelper } from '#root/dist/src/core/base-classes/base-component/helpers/children.helper.js';
|
|
2
|
+
import { BaseComponent } from '#root/dist/src/core/index.js';
|
|
2
3
|
|
|
3
4
|
describe('ChildrenHelper', () => {
|
|
4
5
|
let element;
|
|
@@ -55,6 +56,29 @@ describe('ChildrenHelper', () => {
|
|
|
55
56
|
expect(children.length).toBe(2);
|
|
56
57
|
});
|
|
57
58
|
|
|
59
|
+
it('keeps the placeholder attributes (except data-component) on the mounted child element', () => {
|
|
60
|
+
class Widget extends BaseComponent {
|
|
61
|
+
generateTemplate() { return '<p>widget</p>'; }
|
|
62
|
+
}
|
|
63
|
+
element.innerHTML = '<div data-component="widget" class="my-class" id="my-widget"></div>';
|
|
64
|
+
const component = {
|
|
65
|
+
registerChildren: () => [{
|
|
66
|
+
selector: 'widget',
|
|
67
|
+
factory: (el) => new Widget({ mountTarget: el }),
|
|
68
|
+
}],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const children = [];
|
|
72
|
+
ChildrenHelper.addChildren(component, element, children);
|
|
73
|
+
|
|
74
|
+
const childEl = children[0].getElement();
|
|
75
|
+
expect(element.contains(childEl)).toBe(true);
|
|
76
|
+
expect(childEl.classList.contains('my-class')).toBe(true);
|
|
77
|
+
expect(childEl.id).toBe('my-widget');
|
|
78
|
+
expect(childEl.hasAttribute('data-component')).toBe(false);
|
|
79
|
+
expect(childEl.innerHTML).toBe('<p>widget</p>');
|
|
80
|
+
});
|
|
81
|
+
|
|
58
82
|
it('does nothing when there are no registerChildren', () => {
|
|
59
83
|
const component = { registerChildren: () => [] };
|
|
60
84
|
const children = [];
|
|
@@ -62,6 +86,28 @@ describe('ChildrenHelper', () => {
|
|
|
62
86
|
expect(children.length).toBe(0);
|
|
63
87
|
});
|
|
64
88
|
|
|
89
|
+
it('destroys previously-tracked children and resets the array before mounting new ones on a second call', () => {
|
|
90
|
+
let destroyCount = 0;
|
|
91
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
92
|
+
const component = {
|
|
93
|
+
registerChildren: () => [{
|
|
94
|
+
selector: 'widget',
|
|
95
|
+
factory: () => ({ render: () => {}, destroy: () => destroyCount++ }),
|
|
96
|
+
}],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const children = [];
|
|
100
|
+
ChildrenHelper.addChildren(component, element, children);
|
|
101
|
+
expect(children.length).toBe(1);
|
|
102
|
+
expect(destroyCount).toBe(0);
|
|
103
|
+
|
|
104
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
105
|
+
ChildrenHelper.addChildren(component, element, children);
|
|
106
|
+
|
|
107
|
+
expect(destroyCount).toBe(1);
|
|
108
|
+
expect(children.length).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
65
111
|
it('destroyChildren calls destroy on every child', () => {
|
|
66
112
|
let destroyCount = 0;
|
|
67
113
|
const children = [
|
|
@@ -2,10 +2,13 @@ import { BaseComponent, ComponentConfig } from '../base-component.js';
|
|
|
2
2
|
|
|
3
3
|
export class ChildrenHelper {
|
|
4
4
|
public static addChildren(
|
|
5
|
-
component: BaseComponent,
|
|
6
|
-
element: HTMLElement,
|
|
5
|
+
component: BaseComponent,
|
|
6
|
+
element: HTMLElement,
|
|
7
7
|
children: BaseComponent[]
|
|
8
8
|
): void {
|
|
9
|
+
this.destroyChildren(children);
|
|
10
|
+
children.length = 0;
|
|
11
|
+
|
|
9
12
|
const configs: ComponentConfig[] = component.registerChildren();
|
|
10
13
|
configs.forEach(config => {
|
|
11
14
|
element.querySelectorAll(`[data-component="${config.selector}"]`).forEach(el => {
|
|
@@ -39,6 +39,45 @@ describe('DomHelper', () => {
|
|
|
39
39
|
expect(container.contains(el)).toBe(true);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
it('mountElement carries the placeholder attributes onto the element, merging classes', () => {
|
|
43
|
+
const placeholder = document.createElement('div');
|
|
44
|
+
placeholder.className = 'my-class other-class';
|
|
45
|
+
placeholder.id = 'foo';
|
|
46
|
+
placeholder.setAttribute('aria-label', 'Label');
|
|
47
|
+
placeholder.setAttribute('data-extra', 'bar');
|
|
48
|
+
container.appendChild(placeholder);
|
|
49
|
+
|
|
50
|
+
const el = document.createElement('span');
|
|
51
|
+
el.className = 'own-class';
|
|
52
|
+
DomHelper.mountElement(el, placeholder);
|
|
53
|
+
|
|
54
|
+
expect(el.classList.contains('own-class')).toBe(true);
|
|
55
|
+
expect(el.classList.contains('my-class')).toBe(true);
|
|
56
|
+
expect(el.classList.contains('other-class')).toBe(true);
|
|
57
|
+
expect(el.id).toBe('foo');
|
|
58
|
+
expect(el.getAttribute('aria-label')).toBe('Label');
|
|
59
|
+
expect(el.getAttribute('data-extra')).toBe('bar');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('mountElement does not carry data-component over from the placeholder', () => {
|
|
63
|
+
const placeholder = document.createElement('div');
|
|
64
|
+
placeholder.setAttribute('data-component', 'widget');
|
|
65
|
+
container.appendChild(placeholder);
|
|
66
|
+
|
|
67
|
+
const el = document.createElement('span');
|
|
68
|
+
DomHelper.mountElement(el, placeholder);
|
|
69
|
+
|
|
70
|
+
expect(el.hasAttribute('data-component')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('mountElement leaves the element attributes untouched in append mode', () => {
|
|
74
|
+
container.className = 'container-class';
|
|
75
|
+
const el = document.createElement('span');
|
|
76
|
+
DomHelper.mountElement(el, '#mount-target');
|
|
77
|
+
|
|
78
|
+
expect(el.attributes.length).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
42
81
|
it('createElement creates an element with the given tag name', () => {
|
|
43
82
|
const el = DomHelper.createElement('section');
|
|
44
83
|
expect(el.tagName).toBe('SECTION');
|
|
@@ -109,6 +148,12 @@ describe('DomHelper', () => {
|
|
|
109
148
|
expect(container.querySelector('span')).toBe(null);
|
|
110
149
|
});
|
|
111
150
|
|
|
151
|
+
it('cleanupOptionalContent removes an optional element whose textContent is the literal string "null"', () => {
|
|
152
|
+
container.innerHTML = '<span data-optional>null</span>';
|
|
153
|
+
DomHelper.cleanupOptionalContent();
|
|
154
|
+
expect(container.querySelector('span')).toBe(null);
|
|
155
|
+
});
|
|
156
|
+
|
|
112
157
|
it('cleanupOptionalContent keeps an optional element with real textContent', () => {
|
|
113
158
|
container.innerHTML = '<span data-optional>hello</span>';
|
|
114
159
|
DomHelper.cleanupOptionalContent();
|
|
@@ -40,7 +40,7 @@ export class DomHelper {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const content = el.textContent?.trim();
|
|
43
|
-
return !content || content === "undefined";
|
|
43
|
+
return !content || content === "undefined" || content === "null";
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
const isValueUndefined = (el: HTMLElement): boolean => {
|
|
@@ -72,10 +72,24 @@ export class DomHelper {
|
|
|
72
72
|
config.target.appendChild(config.element);
|
|
73
73
|
} else {
|
|
74
74
|
// Replace placeholder mode
|
|
75
|
+
this.copyAttributes(config.target, config.element);
|
|
75
76
|
config.target.replaceWith(config.element);
|
|
76
77
|
}
|
|
77
78
|
} else {
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
82
|
+
|
|
83
|
+
// Carries placeholder attributes (class, id, aria-*, data-*...) onto the mounted
|
|
84
|
+
// element. data-component is skipped so an ancestor's child lookup can't re-match it.
|
|
85
|
+
private static copyAttributes(from: HTMLElement, to: HTMLElement): void {
|
|
86
|
+
Array.from(from.attributes).forEach(attr => {
|
|
87
|
+
if (attr.name === 'data-component') return;
|
|
88
|
+
if (attr.name === 'class') {
|
|
89
|
+
to.classList.add(...Array.from(from.classList));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
to.setAttribute(attr.name, attr.value);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
81
95
|
}
|
|
@@ -32,18 +32,29 @@ export class EventHelper {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
private static createBoundHandler(
|
|
35
|
-
el: Element,
|
|
36
|
-
component: BaseComponent,
|
|
37
|
-
handler: (...args: any[]) => void,
|
|
35
|
+
el: Element,
|
|
36
|
+
component: BaseComponent,
|
|
37
|
+
handler: (...args: any[]) => void,
|
|
38
38
|
rawArgs: string[]
|
|
39
39
|
): EventListener {
|
|
40
40
|
return (event: Event) => {
|
|
41
|
+
if (this.isModifiedAnchorClick(el, event)) return;
|
|
42
|
+
|
|
41
43
|
const resolvedArgs = rawArgs.map(arg => TokenHelper.resolve(arg.trim(), el, event));
|
|
42
44
|
|
|
43
45
|
handler.call(component, ...resolvedArgs);
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
// Lets a real <a href> fall through to native browser behavior (open in new tab, etc.)
|
|
50
|
+
// on a modified or non-primary click, instead of always intercepting via preventDefault.
|
|
51
|
+
private static isModifiedAnchorClick(el: Element, event: Event): boolean {
|
|
52
|
+
if (event.type !== 'click' || el.tagName !== 'A' || !el.getAttribute('href')) return false;
|
|
53
|
+
|
|
54
|
+
const mouseEvent = event as MouseEvent;
|
|
55
|
+
return mouseEvent.ctrlKey || mouseEvent.metaKey || mouseEvent.shiftKey || mouseEvent.altKey || mouseEvent.button !== 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
private static addEvent(
|
|
48
59
|
target: EventTarget,
|
|
49
60
|
event: string,
|