@nutin/cli 1.2.3 → 1.3.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +23 -15
  3. package/lib/index.mjs +7 -4
  4. package/lib/src/context-builder.mjs +35 -0
  5. package/lib/src/file-generator.mjs +26 -126
  6. package/lib/src/git-manager.mjs +1 -1
  7. package/lib/src/json-generator.mjs +31 -0
  8. package/lib/src/package-manager.mjs +21 -18
  9. package/lib/src/project-generator.mjs +53 -0
  10. package/lib/src/prompts.mjs +28 -15
  11. package/lib/src/template-compiler.mjs +1 -1
  12. package/lib/src/utils.mjs +9 -9
  13. package/lib/src/version.mjs +1 -0
  14. package/package.json +1 -1
  15. package/templates/base/.gitignore.hbs +1 -0
  16. package/templates/base/builder.config.js.hbs +7 -0
  17. package/templates/base/src/app/main.ts.hbs +2 -1
  18. package/templates/base/src/app/views/home/home.view.test.js.hbs +26 -0
  19. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
  20. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +1 -1
  21. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +23 -20
  22. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +2 -2
  23. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +4 -4
  24. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +26 -13
  25. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +9 -9
  26. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +40 -8
  27. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +16 -16
  28. package/templates/base/src/core/base-classes/component/component.ts.hbs +12 -12
  29. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +2 -2
  30. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +2 -2
  31. package/templates/base/src/core/base-classes/service/service.ts.hbs +35 -12
  32. package/templates/base/src/core/base-classes/view/view.ts.hbs +6 -6
  33. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +13 -13
  34. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +18 -18
  35. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +24 -24
  36. package/templates/base/src/core/services/http-client/http-client.ts.hbs +19 -19
  37. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +6 -6
  38. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +13 -13
  39. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +22 -22
  40. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +6 -5
  41. package/templates/base/src/core/services/router/router.ts.hbs +45 -45
  42. package/templates/base/src/core/services/store/store.ts.hbs +6 -6
  43. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +6 -8
  44. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +9 -11
  45. package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +11 -5
  46. package/templates/base/src/libs/pipes/pipes.ts.hbs +1 -1
  47. package/templates/base/src/libs/popover/popover.ts.hbs +48 -48
  48. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +1 -1
  49. package/templates/base/tools/builder/builder.js.hbs +9 -3
  50. package/templates/base/tools/builder/core/add-tags.js.hbs +46 -0
  51. package/templates/base/tools/builder/core/copy-static.js.hbs +35 -35
  52. package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -6
  53. package/templates/base/tools/builder/core/hash-files.js.hbs +52 -0
  54. package/templates/base/tools/builder/core/validate-html.js.hbs +12 -32
  55. package/templates/base/tools/utils/get-files-recursive.js.hbs +17 -8
  56. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +56 -0
  57. package/templates/{base → features/deployHelper}/tools/deployment/Dockerfile.hbs +12 -4
  58. package/templates/{base → features/deployHelper}/tools/deployment/nginx.conf.hbs +30 -2
  59. package/templates/features/i18n/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +4 -6
  60. package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +38 -47
  61. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +17 -0
  62. package/templates/features/template/src/app/views/home/home.view.html.hbs +1 -1
  63. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +2 -2
  64. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +2 -2
  65. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +1 -0
  66. package/templates/features/testinNutin/testin-nutin.config.js.hbs +2 -2
  67. package/templates/base/tools/deployment/gzip.conf.hbs +0 -19
  68. package/templates/features/i18n/src/app/views/home/locales/fr.json.hbs +0 -9
  69. /package/templates/{base → features/deployHelper}/.dockerignore.hbs +0 -0
@@ -31,6 +31,54 @@ export class PopoverView extends View {
31
31
  );
32
32
  }
33
33
 
34
+ public override shouldUpdateMetaContent(): boolean {
35
+ return false;
36
+ }
37
+
38
+ public override render(): HTMLElement {
39
+ document.body.classList.add('no-scroll');
40
+ this.overlay = this.createOverlay();
41
+
42
+ const wrapper = this.createWrapper();
43
+ const content = this.createContent();
44
+
45
+ this.appendCloseButton(content);
46
+ this.appendFooterButtons(content);
47
+
48
+ wrapper.appendChild(content);
49
+ this.overlay.appendChild(wrapper);
50
+ document.body.appendChild(this.overlay);
51
+
52
+ this.autoBindEvents();
53
+ this.parseDataAttributes();
54
+ this.animateIn(wrapper);
55
+
56
+ this.bindOverlayClose();
57
+
58
+ return this.overlay;
59
+ }
60
+
61
+ public override destroy(): void {
62
+ if (this.overlay) {
63
+ const wrapper = this.overlay.querySelector('.popover-wrapper');
64
+ this.overlay.classList.remove('show');
65
+ wrapper?.classList.remove('show');
66
+
67
+ setTimeout(() => {
68
+ wrapper?.remove()
69
+ this.overlay?.remove();
70
+ this.overlay = null;
71
+ }, 250); // match transition duration
72
+ }
73
+
74
+ document.body.classList.remove('no-scroll');
75
+ super.destroy();
76
+
77
+ if (typeof this.onClose === 'function') {
78
+ this.onClose();
79
+ }
80
+ }
81
+
34
82
  // Wraps button callbacks to include destroy() call
35
83
  private wrapButtonCallbacks(buttons: PopoverButton[]): PopoverButton[] {
36
84
  return buttons.map(btn => ({
@@ -94,52 +142,4 @@ export class PopoverView extends View {
94
142
  wrapper.classList.add('show');
95
143
  });
96
144
  }
97
-
98
- public override shouldUpdateMetaContent(): boolean {
99
- return false;
100
- }
101
-
102
- public override render(): HTMLElement {
103
- document.body.classList.add('no-scroll');
104
- this.overlay = this.createOverlay();
105
-
106
- const wrapper = this.createWrapper();
107
- const content = this.createContent();
108
-
109
- this.appendCloseButton(content);
110
- this.appendFooterButtons(content);
111
-
112
- wrapper.appendChild(content);
113
- this.overlay.appendChild(wrapper);
114
- document.body.appendChild(this.overlay);
115
-
116
- this.autoBindEvents();
117
- this.parseDataAttributes();
118
- this.animateIn(wrapper);
119
-
120
- this.bindOverlayClose();
121
-
122
- return this.overlay;
123
- }
124
-
125
- public override destroy(): void {
126
- if (this.overlay) {
127
- const wrapper = this.overlay.querySelector('.popover-wrapper');
128
- this.overlay.classList.remove('show');
129
- wrapper?.classList.remove('show');
130
-
131
- setTimeout(() => {
132
- wrapper?.remove()
133
- this.overlay?.remove();
134
- this.overlay = null;
135
- }, 250); // match transition duration
136
- }
137
-
138
- document.body.classList.remove('no-scroll');
139
- super.destroy();
140
-
141
- if (typeof this.onClose === 'function') {
142
- this.onClose();
143
- }
144
- }
145
145
  }
@@ -37,7 +37,7 @@ export function notify(message: string, options: NotifyOptions = {}): void {
37
37
  }
38
38
  }
39
39
 
40
- function displayNext() {
40
+ function displayNext(): void {
41
41
  if (queue.length === 0) {
42
42
  isDisplaying = false;
43
43
  return;
@@ -3,7 +3,7 @@
3
3
  import path from 'path';
4
4
  import { print, isProd, runScript } from '../utils/index.js';
5
5
 
6
- const scriptsDir = path.join(process.cwd(),'tools', 'builder', 'core');
6
+ const scriptsDir = path.join(process.cwd(), 'tools', 'builder', 'core');
7
7
 
8
8
  if (!isProd) print.boldInfo('For production, use build:prod.\n');
9
9
  print.boldHead(`Starting build...\n`);
@@ -21,9 +21,15 @@ runScript(path.join(scriptsDir, 'minify-html.js'), 'Minifying inline templates..
21
21
  runScript(path.join(scriptsDir, 'build-i18n.js'), 'Combining locales for production...');
22
22
  {{/if}}
23
23
  runScript(path.join(scriptsDir, 'sass.js'), 'Compiling styles from main.scss...');
24
- runScript(path.join(scriptsDir, 'validate-html.js'), 'Validating tags in index.html...');
24
+ runScript(path.join(scriptsDir, 'validate-html.js'), 'Adding and validating tags in index.html...');
25
25
 
26
- if (isProd) runScript(path.join(scriptsDir, 'esbuild.js'), 'Running esbuild...');
26
+ if (isProd) {
27
+ runScript(path.join(scriptsDir, 'esbuild.js'), 'Running esbuild...');
28
+ runScript(path.join(scriptsDir, 'hash-files.js'), 'Hashing files...');
29
+ {{#if deployHelper}}
30
+ runScript(path.join(scriptsDir, 'compress-files.js'), 'Compressing files...');
31
+ {{/if}}
32
+ }
27
33
 
28
34
  runScript(path.join(scriptsDir, 'finalize-build.js'), 'Finalizing build...')
29
35
 
@@ -0,0 +1,46 @@
1
+ import { print, isProd } from '../../utils/index.js';
2
+ import { writeFile } from 'fs/promises';
3
+
4
+ export async function addTags(htmlContent, filePath) {
5
+ const scriptSrc = isProd ? "bundle.js" : "app/main.js";
6
+
7
+ const tags = [
8
+ { type: 'stylesheet', href: 'main.css', position: 'head' },
9
+ { type: 'script', src: scriptSrc, position: 'body' }
10
+ ];
11
+
12
+ let modifiedHtml = htmlContent;
13
+
14
+ for (const tag of tags) {
15
+ modifiedHtml = addTag(modifiedHtml, tag);
16
+ }
17
+
18
+ await writeFile(filePath, modifiedHtml, "utf8");
19
+
20
+ return modifiedHtml;
21
+ }
22
+
23
+ function addTag(htmlContent, { type, src, href, position }) {
24
+ if (type !== 'stylesheet' && type !== 'script') return htmlContent;
25
+
26
+ const tagMap = {
27
+ script: `<script type="module" src="/${src}"></script>`,
28
+ stylesheet: `<link rel="stylesheet" href="/${href}">`
29
+ };
30
+
31
+ const positionMap = {
32
+ body: { regex: /<\/body>/i, replacement: (tag) => `${tag}</body>` },
33
+ head: { regex: /<\/head>/i, replacement: (tag) => `${tag}</head>`, fallback: /<title>/i }
34
+ };
35
+
36
+ const tag = tagMap[type];
37
+ const pos = positionMap[position];
38
+
39
+ if (pos.regex.test(htmlContent)) {
40
+ return htmlContent.replace(pos.regex, pos.replacement(tag));
41
+ } else if (pos.fallback && pos.fallback.test(htmlContent)) {
42
+ return htmlContent.replace(pos.fallback, `${tag}<title>`);
43
+ }
44
+
45
+ return htmlContent + tag;
46
+ }
@@ -5,6 +5,41 @@ import { getFilesRecursive, print, isVerbose, isProd } from '../../utils/index.j
5
5
  import { BINARY_EXTENSIONS } from '../variables/binary-extensions.js';
6
6
  import { PATHS } from './paths.js';
7
7
 
8
+ async function copyStatic() {
9
+ const extensions = [...BINARY_EXTENSIONS, 'html'];
10
+ if (isProd) extensions.push('.ts');
11
+
12
+ if (isVerbose) print.info(`File types that will be copied: ${Array.from(extensions)}`);
13
+
14
+ await ensureDir(PATHS.tempSource);
15
+
16
+ await copyFavicon();
17
+ {{#if i18n}}
18
+ await copyConfig();
19
+ {{/if}}
20
+
21
+ for (const extension of extensions) {
22
+ const files = await getFilesRecursive(PATHS.source, extension);
23
+
24
+ for (const file of files) {
25
+ const relative = path.relative(PATHS.source, file);
26
+ const dest = path.join(PATHS.tempSource, relative);
27
+
28
+ try {
29
+ await copyFile(file, dest);
30
+ if (isVerbose) print.info(`✅ Copied: ${file} → ${dest}`);
31
+ } catch (err) {
32
+ print.error(`❌ Failed to copy: ${file}. ${err.message}`);
33
+ exit(1);
34
+ }
35
+ }
36
+ }
37
+
38
+ if (isProd) await copyJsonFiles(PATHS.source, PATHS.tempSource);
39
+
40
+ if (isVerbose) print.boldInfo(`Copy complete.\n`);
41
+ }
42
+
8
43
  async function ensureDir(dir) {
9
44
  await fs.mkdir(dir, { recursive: true });
10
45
  }
@@ -45,41 +80,6 @@ async function copyConfig() {
45
80
  }
46
81
 
47
82
  {{/if}}
48
- async function copyStatic() {
49
- const extensions = [...BINARY_EXTENSIONS, 'html'];
50
- if (isProd) extensions.push('.ts');
51
-
52
- if (isVerbose) print.info(`File types that will be copied: ${Array.from(extensions)}`);
53
-
54
- await ensureDir(PATHS.tempSource);
55
-
56
- await copyFavicon();
57
- {{#if i18n}}
58
- await copyConfig();
59
- {{/if}}
60
-
61
- for (const extension of extensions) {
62
- const files = await getFilesRecursive(PATHS.source, extension);
63
-
64
- for (const file of files) {
65
- const relative = path.relative(PATHS.source, file);
66
- const dest = path.join(PATHS.tempSource, relative);
67
-
68
- try {
69
- await copyFile(file, dest);
70
- if (isVerbose) print.info(`✅ Copied: ${file} → ${dest}`);
71
- } catch (err) {
72
- print.error(`❌ Failed to copy: ${file}. ${err.message}`);
73
- exit(1);
74
- }
75
- }
76
- }
77
-
78
- if (isProd) await copyJsonFiles(PATHS.source, PATHS.tempSource);
79
-
80
- if (isVerbose) print.boldInfo(`Copy complete.\n`);
81
- }
82
-
83
83
  async function copyJsonFiles(sourceDir, destDir) {
84
84
 
85
85
  const items = await fs.readdir(sourceDir, { withFileTypes: true });
@@ -3,6 +3,12 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { PATHS } from "./paths.js";
5
5
 
6
+ function finalizeBuild() {
7
+ if (isProd) removeFoldersAfterBundle();
8
+ replaceDir(PATHS.temp, PATHS.build);
9
+ if (isVerbose) print.boldInfo('Build finalized.\n');
10
+ }
11
+
6
12
  function removeFoldersAfterBundle() {
7
13
  const foldersToRemove = ['core', 'libs', 'app'];
8
14
  const pathToFolder = (folder) => path.join(PATHS.tempSource, folder);
@@ -15,10 +21,4 @@ function replaceDir(src, dest) {
15
21
  fs.renameSync(src, dest);
16
22
  }
17
23
 
18
- function finalizeBuild() {
19
- if (isProd) removeFoldersAfterBundle();
20
- replaceDir(PATHS.temp, PATHS.build);
21
- if (isVerbose) print.boldInfo('Build finalized.\n');
22
- }
23
-
24
24
  finalizeBuild();
@@ -0,0 +1,52 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { PATHS } from './paths.js';
5
+ import { print, isVerbose, getFilesRecursive } from '../../utils/index.js';
6
+
7
+ function addHashToFiles(distDir) {
8
+ const files = getFilesRecursive(distDir, ['.js', '.css']);
9
+
10
+ const fileMap = {}; // Track old → new filenames
11
+
12
+ files.forEach(fullPath => {
13
+ const ext = path.extname(fullPath);
14
+ const dirOfFile = path.dirname(fullPath);
15
+ const name = path.basename(fullPath, ext);
16
+
17
+ const hash = hashFile(fullPath);
18
+ const newName = `${name}.${hash}${ext}`;
19
+ const newPath = path.join(dirOfFile, newName);
20
+
21
+ fs.renameSync(fullPath, newPath);
22
+
23
+ const oldRel = path.relative(distDir, fullPath).replace(/\\/g, '/');
24
+ const newRel = path.relative(distDir, newPath).replace(/\\/g, '/');
25
+
26
+ fileMap[oldRel] = newRel;
27
+
28
+ if (isVerbose) {
29
+ print.info(`Renamed: ${oldRel} → ${newRel}`);
30
+ }
31
+ });
32
+
33
+ updateHtmlReferences(path.join(distDir, 'index.html'), fileMap);
34
+ }
35
+
36
+ function hashFile(filePath) {
37
+ const content = fs.readFileSync(filePath);
38
+ return crypto.createHash('sha256').update(content).digest('hex').substring(0, 8);
39
+ }
40
+
41
+ function updateHtmlReferences(htmlPath, fileMap) {
42
+ let html = fs.readFileSync(htmlPath, 'utf8');
43
+
44
+ Object.entries(fileMap).forEach(([oldName, newName]) => {
45
+ html = html.replace(new RegExp(oldName, 'g'), newName);
46
+ });
47
+
48
+ fs.writeFileSync(htmlPath, html);
49
+ if (isVerbose) print.info('Updated index.html references');
50
+ }
51
+
52
+ addHashToFiles(PATHS.tempSource);
@@ -1,33 +1,9 @@
1
- import { readFile, writeFile } from 'fs/promises';
1
+ import { readFile } from 'fs/promises';
2
2
  import { exit } from 'process';
3
- import { print, isProd, isVerbose } from '../../utils/index.js';
3
+ import { print, isVerbose } from '../../utils/index.js';
4
4
  import { PATHS } from './paths.js';
5
5
  import path from 'path';
6
-
7
- function errorExit(message) {
8
- print.boldError(`[ERROR] ${message}`);
9
- exit(1);
10
- }
11
-
12
- async function addEntrypoint(htmlContent, filePath) {
13
- if (isVerbose) print.info('Adding script in HTML...');
14
-
15
- const scriptSrc = isProd ? "bundle.js" : "app/main.js";
16
- const scriptTag = `<script type="module" src="/${scriptSrc}"></script>`;
17
-
18
- let modifiedHtml;
19
- if (/<\/body>/i.test(htmlContent)) {
20
- modifiedHtml = htmlContent.replace(/<\/body>/i, `${scriptTag}</body>`);
21
- } else {
22
- modifiedHtml = htmlContent + scriptTag;
23
- }
24
-
25
- const minified = modifiedHtml.replace(/>\s+</g, '><');
26
-
27
- writeFile(filePath, minified, "utf8");
28
-
29
- return modifiedHtml;
30
- }
6
+ import { addTags } from './add-tags.js';
31
7
 
32
8
  async function validateHtml() {
33
9
  const filePath = path.join(PATHS.tempSource, 'index.html');
@@ -43,17 +19,21 @@ async function validateHtml() {
43
19
  errorExit('Missing #app container in HTML');
44
20
  }
45
21
 
46
- if (!/<link[^>]*rel=["']stylesheet["']/.test(htmlContent)) {
47
- errorExit('No stylesheet found in HTML');
48
- }
49
-
50
- htmlContent = await addEntrypoint(htmlContent, filePath).catch(err => errorExit(err.message));
22
+ htmlContent = await addTags(htmlContent, filePath).catch(err => errorExit(err.message));
51
23
 
52
24
  if (!/<script[^>]*type=["']module["']/.test(htmlContent)) {
53
25
  errorExit('No module scripts found in HTML');
54
26
  }
27
+ if (!/<link[^>]*rel=["']stylesheet["']/.test(htmlContent)) {
28
+ errorExit('No stylesheet found in HTML');
29
+ }
55
30
 
56
31
  if (isVerbose) print.boldInfo('HTML validation passed\n');
57
32
  }
58
33
 
34
+ function errorExit(message) {
35
+ print.boldError(`[ERROR] ${message}`);
36
+ exit(1);
37
+ }
38
+
59
39
  validateHtml();
@@ -3,25 +3,34 @@ import * as path from 'path';
3
3
 
4
4
  export function getFilesRecursive(dir, extension) {
5
5
  const fullDir = path.resolve(dir);
6
-
7
6
  if (!fs.existsSync(fullDir)) return [];
8
-
7
+
8
+ // Normalize to an array of extensions, with leading dots
9
+ const exts = Array.isArray(extension)
10
+ ? extension
11
+ : [extension];
12
+
13
+ const normalizedExts = exts.map(ext =>
14
+ ext.startsWith('.') ? ext : `.${ext}`
15
+ );
16
+
9
17
  function findFilesRecursively(currentDir) {
10
18
  const files = [];
11
19
  const items = fs.readdirSync(currentDir, { withFileTypes: true });
12
-
20
+
13
21
  for (const item of items) {
14
22
  const fullPath = path.join(currentDir, item.name);
15
-
23
+
16
24
  if (item.isDirectory()) {
17
25
  files.push(...findFilesRecursively(fullPath));
18
- } else if (item.isFile() && item.name.endsWith(extension)) {
19
- files.push(fullPath);
26
+ } else if (item.isFile()) {
27
+ if (normalizedExts.some(ext => item.name.endsWith(ext))) {
28
+ files.push(fullPath);
29
+ }
20
30
  }
21
31
  }
22
-
23
32
  return files;
24
33
  }
25
-
34
+
26
35
  return findFilesRecursively(fullDir);
27
36
  }
@@ -0,0 +1,56 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { gzipSync
3
+ // BROTLI OPTIONAL
4
+ //, brotliCompressSync, constants
5
+ } from 'zlib';
6
+ import { print, getFilesRecursive, isVerbose } from '../../utils/index.js';
7
+ import { PATHS } from './paths.js';
8
+ import builderConfig from '../../../builder.config.js';
9
+
10
+ const compress = {
11
+ gzip: builderConfig.compression.gzip,
12
+ // BROTLI OPTIONAL
13
+ // brotli: builderConfig.compression.brotli,
14
+ };
15
+
16
+ function compressStaticAssets() {
17
+ if (
18
+ !compress.gzip
19
+ // BROTLI OPTIONAL
20
+ // && !compress.brotli
21
+ ) return;
22
+
23
+ const files = getFilesRecursive(PATHS.tempSource, ['.js', '.css', '.json', '.svg', 'ttf', 'otf', 'eot']);
24
+
25
+ files.forEach(file => {
26
+ const content = readFileSync(file);
27
+
28
+ if (compress.gzip) {
29
+ const gzipped = gzipSync(content, {
30
+ level: 9,
31
+ memLevel: 9,
32
+ windowBits: 15
33
+ });
34
+ writeFileSync(`${file}.gz`, gzipped);
35
+ if (isVerbose) print.info(`Gzip compression: ${file}`);
36
+ }
37
+
38
+ // BROTLI OPTIONAL
39
+ /*
40
+ if (compress.brotli) {
41
+ const brotlied = brotliCompressSync(content, {
42
+ params: {
43
+ [constants.BROTLI_PARAM_QUALITY]: 11,
44
+ [constants.BROTLI_PARAM_LGWIN]: 22,
45
+ [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_GENERIC,
46
+ [constants.BROTLI_PARAM_SIZE_HINT]: content.length
47
+ }
48
+ });
49
+ writeFileSync(`${file}.br`, brotlied);
50
+ if (isVerbose) print.info(`Brotli compression: ${file}`);
51
+ }
52
+ */
53
+ });
54
+ }
55
+
56
+ compressStaticAssets();
@@ -1,9 +1,18 @@
1
1
  ARG NODE_VERSION=22.3.0
2
2
  ARG NGINX_VERSION=1.27-alpine
3
+ ARG PORT=9090
3
4
 
4
5
  FROM node:${NODE_VERSION} AS builder
5
6
  WORKDIR /usr/src/app
6
- COPY . .
7
+ COPY ./src ./src
8
+ COPY ./package* .
9
+ COPY ./builder.config.js .
10
+ {{#if i18n}}
11
+ COPY ./config ./config
12
+ {{/if}}
13
+ COPY ./tools/builder ./tools/builder
14
+ COPY ./tools/deployment ./tools/deployment
15
+ COPY ./tools/utils ./tools/utils
7
16
  RUN {{ciCommand}} && \
8
17
  {{packageManager}} run build:prod
9
18
 
@@ -11,12 +20,11 @@ FROM nginx:${NGINX_VERSION}
11
20
 
12
21
  RUN rm /etc/nginx/conf.d/default.conf
13
22
  COPY --from=builder /usr/src/app/tools/deployment/nginx.conf /etc/nginx/conf.d/default.conf
14
- COPY --from=builder /usr/src/app/tools/deployment/gzip.conf /etc/nginx/conf.d/gzip.conf
15
23
  COPY --from=builder /usr/src/app/dist/src /usr/share/nginx/html
16
24
 
17
- EXPOSE 9090
25
+ EXPOSE ${PORT}
18
26
 
19
27
  HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
20
- CMD curl -f http://localhost:9090/health || exit 1
28
+ CMD curl -f http://localhost:${PORT}/health || exit 1
21
29
 
22
30
  CMD ["nginx", "-g", "daemon off;"]
@@ -1,5 +1,5 @@
1
1
  # ---------------------------
2
- # HEADER VARIABLES (define outside server block)
2
+ # HEADER VARIABLES (outside server block)
3
3
  # ---------------------------
4
4
  map $uri $frame_options {
5
5
  default "SAMEORIGIN";
@@ -25,12 +25,19 @@ map $uri $csp_policy {
25
25
  default "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';";
26
26
  }
27
27
 
28
+ # ---------------------------
29
+ # COMPRESSION (applies globally)
30
+ # ---------------------------
31
+ gzip on;
32
+ gzip_static on;
33
+ gzip_vary on;
34
+ gzip_proxied any;
35
+
28
36
  server {
29
37
  # ---------------------------
30
38
  # LISTEN PORTS
31
39
  # ---------------------------
32
40
  listen 9090;
33
- listen 9091;
34
41
  server_name _;
35
42
 
36
43
  # ---------------------------
@@ -94,6 +101,27 @@ server {
94
101
  location /health {
95
102
  access_log off;
96
103
  add_header Content-Type text/plain always;
104
+ add_header Cache-Control "no-store" always;
97
105
  return 200 "healthy\n";
98
106
  }
107
+
108
+ # ---------------------------
109
+ # ERROR PAGE 404 (rare but possible for SPAs)
110
+ # --------------------------
111
+ error_page 404 /404.html;
112
+
113
+ location = /404.html {
114
+ internal;
115
+ try_files /404.html =404;
116
+ expires -1;
117
+ add_header Cache-Control "no-cache, no-store, must-revalidate" always;
118
+
119
+ # Security headers
120
+ add_header X-Frame-Options $frame_options always;
121
+ add_header X-Content-Type-Options $content_type_options always;
122
+ add_header X-XSS-Protection $xss_protection always;
123
+ add_header Referrer-Policy $referrer_policy always;
124
+ add_header Permissions-Policy $permissions_policy always;
125
+ add_header Content-Security-Policy $csp_policy always;
126
+ }
99
127
  }
@@ -4,20 +4,18 @@ export class I18nHelper {
4
4
  public static parseI18nAttributes(element: HTMLElement): void {
5
5
  element.querySelectorAll('[data-i18n]').forEach(el => {
6
6
  const key = el.getAttribute('data-i18n')!;
7
- const params = JSON.parse(el.getAttribute('data-i18n-params') || '{}');
8
- this.setTranslatedContent(el, key, params);
7
+ this.setTranslatedContent(el, key);
9
8
  });
10
9
  }
11
10
 
12
11
  private static setTranslatedContent(
13
12
  el: Element,
14
- key: string,
15
- params: Record<string, string> = {}
13
+ key: string
16
14
  ): void {
17
15
  if (el instanceof HTMLInputElement) {
18
- el.placeholder = I18nService.translate(key, params);
16
+ el.placeholder = I18nService.translate(key, el.textContent);
19
17
  } else {
20
- el.textContent = I18nService.translate(key, params);
18
+ el.textContent = I18nService.translate(key, el.textContent);
21
19
  }
22
20
  }
23
21
  }