@nutin/cli 1.2.3 → 1.3.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 +70 -1
- package/README.md +25 -18
- package/lib/index.mjs +23 -20
- package/lib/src/context-builder.mjs +40 -0
- package/lib/src/file-generator.mjs +26 -126
- package/lib/src/git-manager.mjs +1 -1
- package/lib/src/json-generator.mjs +31 -0
- package/lib/src/package-manager.mjs +21 -18
- package/lib/src/print.mjs +1 -0
- package/lib/src/project-generator.mjs +53 -0
- package/lib/src/prompts.mjs +16 -75
- package/lib/src/template-compiler.mjs +1 -1
- package/lib/src/utils.mjs +9 -9
- package/lib/src/version.mjs +1 -0
- package/package.json +1 -1
- package/templates/base/.gitignore.hbs +1 -0
- package/templates/base/builder.config.js.hbs +8 -0
- package/templates/base/src/app/globals.d.ts.hbs +12 -8
- package/templates/base/src/app/main.ts.hbs +2 -1
- package/templates/base/src/app/views/home/home.view.test.js.hbs +26 -0
- 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 +1 -1
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +46 -26
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +36 -22
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +4 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +60 -13
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +16 -17
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +40 -8
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +16 -16
- package/templates/base/src/core/base-classes/component/component.ts.hbs +20 -24
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +19 -2
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/service/service.ts.hbs +35 -12
- package/templates/base/src/core/base-classes/view/view.ts.hbs +6 -6
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +13 -13
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +18 -18
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +24 -24
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +19 -19
- package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +6 -6
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +15 -14
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +22 -22
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +4 -36
- package/templates/base/src/core/services/router/router.ts.hbs +45 -46
- package/templates/base/src/core/services/store/store.ts.hbs +6 -6
- package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +41 -15
- package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +40 -19
- package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +11 -5
- package/templates/base/src/libs/pipes/pipes.ts.hbs +1 -1
- package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +197 -0
- package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +75 -0
- package/templates/base/src/libs/popover/popover.ts.hbs +89 -96
- package/templates/base/src/libs/snackbar/snackbar.ts.hbs +7 -4
- package/templates/base/src/styles/base/_variables.scss.hbs +1 -0
- package/templates/base/src/styles/core/_anchors.scss.hbs +10 -0
- package/templates/base/src/styles/core/_index.scss.hbs +6 -0
- package/templates/base/src/styles/core/_popover.scss.hbs +96 -0
- package/templates/base/src/styles/core/_snackbar.scss.hbs +50 -0
- package/templates/base/src/styles/main.scss.hbs +1 -1
- package/templates/base/tools/builder/builder.js.hbs +9 -3
- package/templates/base/tools/builder/core/add-tags.js.hbs +46 -0
- package/templates/base/tools/builder/core/copy-static.js.hbs +35 -35
- package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -6
- package/templates/base/tools/builder/core/hash-files.js.hbs +52 -0
- package/templates/base/tools/builder/core/validate-html.js.hbs +12 -32
- package/templates/base/tools/utils/get-files-recursive.js.hbs +17 -8
- package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +56 -0
- package/templates/{base → features/deployHelper}/tools/deployment/Dockerfile.hbs +12 -4
- package/templates/{base → features/deployHelper}/tools/deployment/nginx.conf.hbs +30 -2
- package/templates/features/i18n/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +4 -6
- package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +76 -52
- package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +31 -0
- package/templates/features/template/src/app/views/home/home.view.html.hbs +1 -1
- package/templates/features/template/tools/builder/core/merge-templates.js.hbs +2 -2
- package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +2 -2
- package/templates/features/testinNutin/testin-nutin/runner.js.hbs +1 -0
- package/templates/features/testinNutin/testin-nutin.config.js.hbs +2 -2
- package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
- package/templates/base/tools/deployment/gzip.conf.hbs +0 -19
- package/templates/features/i18n/src/app/views/home/locales/fr.json.hbs +0 -9
- /package/templates/{base → features/deployHelper}/.dockerignore.hbs +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
@use "sass:map";
|
|
2
|
+
@use "variables" as *;
|
|
3
|
+
{{#if stylinNutin}}
|
|
4
|
+
@use "mixins";
|
|
5
|
+
{{/if}}
|
|
6
|
+
|
|
7
|
+
.app-snackbar {
|
|
8
|
+
position: fixed;
|
|
9
|
+
left: 50%;
|
|
10
|
+
transform: translateX(-50%);
|
|
11
|
+
{{#if stylinNutin}}
|
|
12
|
+
@include mixins.flex-center;
|
|
13
|
+
{{else}}
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
{{/if}}
|
|
18
|
+
background: $snack-default;
|
|
19
|
+
color: white;
|
|
20
|
+
padding: 0.75rem 1.5rem;
|
|
21
|
+
border-radius: 0.5rem;
|
|
22
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
23
|
+
gap: 1rem;
|
|
24
|
+
animation: fade-in 0.3s ease;
|
|
25
|
+
{{#if stylinNutin}}
|
|
26
|
+
@include mixins.respond-to(medium) {
|
|
27
|
+
min-width: 80%;
|
|
28
|
+
}
|
|
29
|
+
{{/if}}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.app-snackbar--bottom { bottom: 1rem; }
|
|
33
|
+
.app-snackbar--top { top: 1rem; }
|
|
34
|
+
|
|
35
|
+
.app-snackbar--success { background: $snack-success; }
|
|
36
|
+
.app-snackbar--error { background: $snack-error; }
|
|
37
|
+
.app-snackbar--info { background: $snack-info; }
|
|
38
|
+
|
|
39
|
+
.app-snackbar button {
|
|
40
|
+
background: transparent;
|
|
41
|
+
border: none;
|
|
42
|
+
color: #fff;
|
|
43
|
+
font-weight: bold;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@keyframes fade-in {
|
|
48
|
+
from { opacity: 0; transform: translateX(-50%) translateY(10px); }
|
|
49
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
50
|
+
}
|
|
@@ -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'), '
|
|
24
|
+
runScript(path.join(scriptsDir, 'validate-html.js'), 'Adding and validating tags in index.html...');
|
|
25
25
|
|
|
26
|
-
if (isProd)
|
|
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
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
2
|
import { exit } from 'process';
|
|
3
|
-
import { print,
|
|
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
|
-
|
|
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()
|
|
19
|
-
|
|
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
|
|
25
|
+
EXPOSE ${PORT}
|
|
18
26
|
|
|
19
27
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
20
|
-
CMD curl -f http://localhost
|
|
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 (
|
|
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
|
}
|
package/templates/features/i18n/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
16
|
+
el.placeholder = I18nService.translate(key, el.textContent);
|
|
19
17
|
} else {
|
|
20
|
-
el.textContent = I18nService.translate(key,
|
|
18
|
+
el.textContent = I18nService.translate(key, el.textContent);
|
|
21
19
|
}
|
|
22
20
|
}
|
|
23
21
|
}
|