@nutin/cli 1.1.0 → 1.2.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +69 -6
  2. package/README.md +49 -2
  3. package/lib/index.mjs +9 -9
  4. package/lib/src/file-generator.mjs +24 -9
  5. package/lib/src/package-manager.mjs +72 -51
  6. package/lib/src/prompts.mjs +10 -10
  7. package/lib/src/utils.mjs +4 -1
  8. package/package.json +4 -3
  9. package/templates/base/.dockerignore.hbs +12 -2
  10. package/templates/base/README.md.hbs +23 -0
  11. package/templates/base/builder.config.js.hbs +28 -0
  12. package/templates/base/src/app/components/common/anchor/anchor.component.ts.hbs +11 -12
  13. package/templates/base/src/app/dom.test.js.hbs +8 -1
  14. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +2 -1
  15. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +11 -11
  16. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +3 -0
  17. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +3 -0
  18. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +5 -4
  19. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +55 -27
  20. package/templates/base/src/core/services/http-client/http-client.ts.hbs +24 -17
  21. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +3 -3
  22. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +2 -2
  23. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +25 -10
  24. package/templates/base/src/core/services/router/router.ts.hbs +1 -0
  25. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +4 -3
  26. package/templates/base/src/core/utils/button-manager/button-manager.utils.test.js.hbs +1 -1
  27. package/templates/base/src/index.html.hbs +2 -2
  28. package/templates/base/src/libs/popover/popover.ts.hbs +2 -1
  29. package/templates/base/src/styles/main.scss.hbs +4 -1
  30. package/templates/base/tools/builder/builder.js.hbs +19 -26
  31. package/templates/base/tools/builder/core/copy-static.js.hbs +41 -20
  32. package/templates/base/tools/builder/core/esbuild.js.hbs +26 -0
  33. package/templates/base/tools/builder/core/finalize-build.js.hbs +24 -0
  34. package/templates/base/tools/builder/core/minify-html.js.hbs +60 -0
  35. package/templates/base/tools/builder/core/paths.js.hbs +19 -0
  36. package/templates/base/tools/builder/core/sass.js.hbs +22 -0
  37. package/templates/base/tools/builder/core/tsc.js.hbs +12 -0
  38. package/templates/base/tools/builder/core/validate-html.js.hbs +32 -12
  39. package/templates/base/tools/deployment/Dockerfile.hbs +22 -0
  40. package/templates/base/tools/deployment/gzip.conf.hbs +19 -0
  41. package/templates/base/tools/deployment/nginx.conf.hbs +78 -0
  42. package/templates/base/tools/dev/dev-serve.js.hbs +32 -0
  43. package/templates/base/tools/dev/serve.js.hbs +23 -0
  44. package/templates/base/tools/{watcher.js.hbs → dev/watcher.js.hbs} +8 -8
  45. package/templates/base/tools/generator/generator.js.hbs +21 -6
  46. package/templates/base/tools/generator/handle-file.js.hbs +32 -2
  47. package/templates/base/tools/generator/templates/index.js.hbs +3 -0
  48. package/templates/base/tools/utils/export-flags.js.hbs +6 -0
  49. package/templates/base/tools/utils/get-files-recursive.js.hbs +1 -3
  50. package/templates/base/tools/utils/index.js.hbs +8 -1
  51. package/templates/base/tools/utils/prompt-boolean.js.hbs +15 -0
  52. package/templates/base/tools/utils/run-command.js.hbs +12 -0
  53. package/templates/base/tools/utils/run-script.js.hbs +22 -0
  54. package/templates/features/i18n/src/core/config.ts.hbs +3 -0
  55. package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +2 -0
  56. package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +10 -1
  57. package/templates/features/i18n/src/core/services/i18n/languages.ts.hbs +7 -0
  58. package/templates/features/i18n/tools/builder/core/build-i18n.js.hbs +12 -17
  59. package/templates/features/i18n/tools/utils/languages.js.hbs +7 -0
  60. package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +1 -1
  61. package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -0
  62. package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
  63. package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +4 -0
  64. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +13 -26
  65. package/templates/features/testinNutin/{test → testin-nutin}/core/index.js.hbs +1 -1
  66. package/templates/features/testinNutin/testin-nutin/core/src/globals/assertion-lib.js.hbs +136 -0
  67. package/templates/features/testinNutin/testin-nutin/core/src/globals/jsdom-setup.js.hbs +101 -0
  68. package/templates/features/testinNutin/{test → testin-nutin}/core/src/globals/register-test-globals.js.hbs +15 -1
  69. package/templates/features/testinNutin/testin-nutin/core/src/globals/spyon.js.hbs +59 -0
  70. package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/print.js.hbs +3 -1
  71. package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/test-discovery.js.hbs +5 -1
  72. package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/test-queue.js.hbs +3 -1
  73. package/templates/features/testinNutin/{test → testin-nutin}/core/tests/assertions.test.js.hbs +26 -2
  74. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +35 -0
  75. package/templates/features/testinNutin/{test → testin-nutin}/watch-tests.js.hbs +1 -1
  76. package/templates/features/testinNutin/testin-nutin.config.js.hbs +14 -0
  77. package/templates/base/Dockerfile.hbs +0 -13
  78. package/templates/base/tools/builder/core/sass.config.js.hbs +0 -17
  79. package/templates/base/tools/builder/docs.md.hbs +0 -9
  80. package/templates/features/testinNutin/test/core/src/globals/assertion-lib.js.hbs +0 -118
  81. package/templates/features/testinNutin/test/core/src/globals/spyon.js.hbs +0 -22
  82. package/templates/features/testinNutin/test/core/src/jsdom-setup.js.hbs +0 -12
  83. package/templates/features/testinNutin/test/docs.md.hbs +0 -39
  84. package/templates/features/testinNutin/test/runner.js.hbs +0 -36
  85. /package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/queue.js.hbs +0 -0
  86. /package/templates/features/testinNutin/{test → testin-nutin}/core/tests/globals-each-all.test.js.hbs +0 -0
  87. /package/templates/features/testinNutin/{test → testin-nutin}/mocks/create-mock-method.js.hbs +0 -0
  88. /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-event-bus.js.hbs +0 -0
  89. /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-http-client.js.hbs +0 -0
  90. /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-i18n.js.hbs +0 -0
  91. /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-router.js.hbs +0 -0
  92. /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-store.js.hbs +0 -0
@@ -1,16 +1,9 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { exit } from 'process';
4
-
4
+ import { getFilesRecursive, print, isVerbose, isProd } from '../../utils/index.js';
5
5
  import { BINARY_EXTENSIONS } from '../variables/binary-extensions.js';
6
- import { getFilesRecursive, print } from '../../utils/index.js';
7
-
8
- const SOURCE_FOLDER = 'src';
9
- {{#if template}}
10
- const DEST_FOLDER = `dist-build/${SOURCE_FOLDER}`;
11
- {{else}}
12
- const DEST_FOLDER = `dist/${SOURCE_FOLDER}`;
13
- {{/if}}
6
+ import { PATHS } from './paths.js';
14
7
 
15
8
  async function ensureDir(dir) {
16
9
  await fs.mkdir(dir, { recursive: true });
@@ -22,33 +15,59 @@ async function copyFile(src, dest) {
22
15
  }
23
16
 
24
17
  async function copyFavicon() {
25
- const srcPath = path.join('public', 'favicon.ico');
26
- const destPath = path.join( DEST_FOLDER, 'favicon.ico');
18
+ const srcPath = path.resolve(path.join('public', 'favicon.ico'));
19
+ const destPath = path.join(PATHS.tempSource, 'favicon.ico');
27
20
 
28
21
  try {
29
22
  await copyFile(srcPath, destPath);
30
- print.info('✅ Copied: favicon.ico');
23
+ if (isVerbose) print.info('Copied: favicon.ico');
31
24
  } catch {
32
- print.boldError('⚠️ No favicon found at public/favicon.ico\x1b[0m');
25
+ print.boldError('⚠️ No favicon found at public/favicon.ico');
33
26
  }
34
27
  }
35
28
 
29
+ {{#if i18n}}
30
+ async function copyConfig() {
31
+ const srcPath = path.resolve(path.join('config'));
32
+ const temp = isProd ? PATHS.temp : PATHS.tempSource;
33
+ const destPath = path.join(temp, 'config');
34
+ try {
35
+ await copyJsonFiles(srcPath, destPath);
36
+ {{#if testinNutin}}
37
+ // for tests to run
38
+ if (!isProd) await copyJsonFiles(srcPath, path.join(PATHS.temp, 'config'))
39
+ {{/if}}
40
+ if (isVerbose) print.info('Config files copied');
41
+ } catch {
42
+ print.boldError('Issue when copying config files.');
43
+ exit(1);
44
+ }
45
+ }
46
+
47
+ {{/if}}
36
48
  async function copyStatic() {
37
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);
38
55
 
39
- await ensureDir(path.join(DEST_FOLDER));
40
56
  await copyFavicon();
57
+ {{#if i18n}}
58
+ await copyConfig();
59
+ {{/if}}
41
60
 
42
61
  for (const extension of extensions) {
43
- const files = await getFilesRecursive(SOURCE_FOLDER, extension);
62
+ const files = await getFilesRecursive(PATHS.source, extension);
44
63
 
45
64
  for (const file of files) {
46
- const relative = path.relative(SOURCE_FOLDER, file);
47
- const dest = path.join(DEST_FOLDER, relative);
65
+ const relative = path.relative(PATHS.source, file);
66
+ const dest = path.join(PATHS.tempSource, relative);
48
67
 
49
68
  try {
50
69
  await copyFile(file, dest);
51
- print.info(`✅ Copied: ${file} → ${dest}`);
70
+ if (isVerbose) print.info(`✅ Copied: ${file} → ${dest}`);
52
71
  } catch (err) {
53
72
  print.error(`❌ Failed to copy: ${file}. ${err.message}`);
54
73
  exit(1);
@@ -56,7 +75,9 @@ async function copyStatic() {
56
75
  }
57
76
  }
58
77
 
59
- await copyJsonFiles(SOURCE_FOLDER, DEST_FOLDER)
78
+ if (isProd) await copyJsonFiles(PATHS.source, PATHS.tempSource);
79
+
80
+ if (isVerbose) print.boldInfo(`Copy complete.\n`);
60
81
  }
61
82
 
62
83
  async function copyJsonFiles(sourceDir, destDir) {
@@ -72,7 +93,7 @@ async function copyJsonFiles(sourceDir, destDir) {
72
93
  } else if (item.isFile() && path.extname(item.name) === '.json') {
73
94
  await fs.mkdir(path.dirname(destPath), { recursive: true });
74
95
  await fs.copyFile(sourcePath, destPath);
75
- console.log(`Copied JSON: ${sourcePath} -> ${destPath}`);
96
+ if (isVerbose) print.info(`Copied JSON: ${sourcePath} -> ${destPath}`);
76
97
  }
77
98
  }
78
99
  }
@@ -0,0 +1,26 @@
1
+ import esbuild from 'esbuild';
2
+ import path from 'path';
3
+ import { exit } from 'process';
4
+ import { print, isVerbose } from '../../utils/index.js';
5
+ import { PATHS } from './paths.js';
6
+ import builderConfig from '../../../builder.config.js';
7
+
8
+ const ENTRY_FILE = path.join(PATHS.tempApp, 'main.ts');
9
+ const OUT_FILE = path.join(PATHS.tempSource, 'bundle.js');
10
+
11
+ async function build() {
12
+ await esbuild.build({
13
+ ...builderConfig.esbuild,
14
+ entryPoints: [ENTRY_FILE],
15
+ outfile: OUT_FILE,
16
+ keepNames: true, // View names are used to update meta.title
17
+ });
18
+
19
+ if (isVerbose) print.boldInfo(`ESBuild complete.\n`);
20
+ }
21
+
22
+
23
+ build().catch((err) => {
24
+ print.boldError(`ESBuild failed: ${err.message}`);
25
+ exit(1);
26
+ });
@@ -0,0 +1,24 @@
1
+ import { print, isProd, isVerbose } from "../../utils/index.js";
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { PATHS } from "./paths.js";
5
+
6
+ function removeFoldersAfterBundle() {
7
+ const foldersToRemove = ['core', 'libs', 'app'];
8
+ const pathToFolder = (folder) => path.join(PATHS.tempSource, folder);
9
+
10
+ foldersToRemove.forEach(folder => fs.rmSync(pathToFolder(folder), { recursive : true }));
11
+ }
12
+
13
+ function replaceDir(src, dest) {
14
+ fs.rmSync(dest, { recursive: true, force: true });
15
+ fs.renameSync(src, dest);
16
+ }
17
+
18
+ function finalizeBuild() {
19
+ if (isProd) removeFoldersAfterBundle();
20
+ replaceDir(PATHS.temp, PATHS.build);
21
+ if (isVerbose) print.boldInfo('Build finalized.\n');
22
+ }
23
+
24
+ finalizeBuild();
@@ -0,0 +1,60 @@
1
+ import { minify } from 'html-minifier-terser';
2
+ {{#unless template}}
3
+ import fs from 'fs';
4
+ import { exit } from 'process';
5
+ import { getFilesRecursive, print, isProd, isVerbose } from '../../utils/index.js';
6
+ import { PATHS } from './paths.js';
7
+ {{/unless}}
8
+
9
+ {{#if template}}export {{/if}}async function minifyHTML(html) {
10
+ return minify(html, {
11
+ collapseWhitespace: true,
12
+ removeComments: true,
13
+ removeRedundantAttributes: true,
14
+ removeScriptTypeAttributes: true,
15
+ removeStyleLinkTypeAttributes: true,
16
+ useShortDoctype: true,
17
+ keepClosingSlash: true,
18
+ minifyCSS: true,
19
+ minifyJS: true,
20
+ });
21
+ }
22
+
23
+ {{#unless template}}
24
+ const TEMPLATE_REGEX =
25
+ /const\s+(template|templateFn)\s*=?\s*(?:\(.*?\)\s*=>\s*)?`([\s\S]*?)`/gm;
26
+
27
+ async function minifyInlineHTML() {
28
+ const extension = isProd ? '.ts' : '.js';
29
+ const jsFiles = getFilesRecursive(PATHS.tempApp, extension);
30
+
31
+ for (const file of jsFiles) {
32
+ let code = fs.readFileSync(file, 'utf8');
33
+ let hasChanges = false;
34
+
35
+ const matches = [...code.matchAll(TEMPLATE_REGEX)];
36
+
37
+ if (matches.length) {
38
+ for (const match of matches) {
39
+ const [full, , html] = match;
40
+ const minified = await minifyHTML(html);
41
+ const minifiedBlock = full.replace(html, minified);
42
+ code = code.replace(full, minifiedBlock);
43
+ hasChanges = true;
44
+ }
45
+ }
46
+
47
+ if (hasChanges) {
48
+ fs.writeFileSync(file, code, 'utf8');
49
+ if (isVerbose) print.info(`Minified inline HTML in: ${file}`);
50
+ }
51
+ }
52
+ if (isVerbose) print.boldInfo('Inline templates minified.\n');
53
+ }
54
+
55
+ minifyInlineHTML().catch((err) => {
56
+ print.boldError(`Unexpected error: ${err.message}`);
57
+ exit(1);
58
+ });
59
+
60
+ {{/unless}}
@@ -0,0 +1,19 @@
1
+ import path from 'path';
2
+
3
+ const SRC_FOLDERNAME = 'src';
4
+ const APP_FOLDERNAME = 'app';
5
+ const TMP_FOLDERNAME = 'dist-build';
6
+ const BLD_FOLDERNAME = 'dist';
7
+
8
+ const BASE_PATHS = {
9
+ source: path.resolve(SRC_FOLDERNAME),
10
+ temp: path.resolve(TMP_FOLDERNAME),
11
+ build: path.resolve(BLD_FOLDERNAME)
12
+ };
13
+
14
+ export const PATHS = {
15
+ ...BASE_PATHS,
16
+ sourceApp: path.join(BASE_PATHS.source, APP_FOLDERNAME),
17
+ tempSource: path.join(BASE_PATHS.temp, SRC_FOLDERNAME),
18
+ tempApp: path.join(BASE_PATHS.temp, SRC_FOLDERNAME, APP_FOLDERNAME),
19
+ };
@@ -0,0 +1,22 @@
1
+ import * as sass from 'sass';
2
+ import * as fs from 'fs';
3
+ import path from 'path';
4
+ import { isVerbose, print } from '../../utils/index.js';
5
+ import { PATHS } from './paths.js';
6
+ import builderConfig from '../../../builder.config.js';
7
+
8
+ const stylesInput = path.join(PATHS.source, 'styles');
9
+ const stylesOutput = path.join(PATHS.tempSource, 'main.css');
10
+
11
+ const scssPath = (origin) => path.join(stylesInput, origin);
12
+ const scssOrigins = builderConfig.sass.paths;
13
+
14
+ const pathsToLoad = scssOrigins.map(origin => scssPath(origin));
15
+
16
+ sass.compileAsync(path.join(stylesInput, 'main.scss'), {
17
+ loadPaths: [ ...pathsToLoad ],
18
+ style: 'compressed'
19
+ }).then(result => {
20
+ fs.writeFileSync(stylesOutput, result.css);
21
+ if (isVerbose) print.boldInfo('Global styles compiled.\n');
22
+ });
@@ -0,0 +1,12 @@
1
+ import { runCommand, print, isVerbose } from "../../utils/index.js";
2
+ import { exit } from 'process';
3
+
4
+ async function compileTS() {
5
+ await runCommand('tsc', ['--project', 'tsconfig.json']);
6
+ if (isVerbose) print.boldInfo('TypeScript compiled.\n');
7
+ }
8
+
9
+ compileTS().catch((err) => {
10
+ print.boldError(`Unexpected error: ${err.message}`);
11
+ exit(1);
12
+ });
@@ -1,18 +1,36 @@
1
- import { readFile } from 'fs/promises';
1
+ import { readFile, writeFile } from 'fs/promises';
2
2
  import { exit } from 'process';
3
- import { print } from '../../utils/index.js';
3
+ import { print, isProd, isVerbose } from '../../utils/index.js';
4
+ import { PATHS } from './paths.js';
5
+ import path from 'path';
4
6
 
5
7
  function errorExit(message) {
6
8
  print.boldError(`[ERROR] ${message}`);
7
9
  exit(1);
8
10
  }
9
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
+ }
31
+
10
32
  async function validateHtml() {
11
- {{#if template}}
12
- const filePath = 'dist-build/src/index.html';
13
- {{else}}
14
- const filePath = 'dist/src/index.html';
15
- {{/if}}
33
+ const filePath = path.join(PATHS.tempSource, 'index.html');
16
34
 
17
35
  let htmlContent;
18
36
  try {
@@ -25,15 +43,17 @@ async function validateHtml() {
25
43
  errorExit('Missing #app container in HTML');
26
44
  }
27
45
 
28
- if (!/<script[^>]*type=["']module["']/.test(htmlContent)) {
29
- errorExit('No module scripts found in HTML');
30
- }
31
-
32
46
  if (!/<link[^>]*rel=["']stylesheet["']/.test(htmlContent)) {
33
47
  errorExit('No stylesheet found in HTML');
34
48
  }
35
49
 
36
- print.info('HTML validation passed');
50
+ htmlContent = await addEntrypoint(htmlContent, filePath).catch(err => errorExit(err.message));
51
+
52
+ if (!/<script[^>]*type=["']module["']/.test(htmlContent)) {
53
+ errorExit('No module scripts found in HTML');
54
+ }
55
+
56
+ if (isVerbose) print.boldInfo('HTML validation passed\n');
37
57
  }
38
58
 
39
59
  validateHtml();
@@ -0,0 +1,22 @@
1
+ ARG NODE_VERSION=22.3.0
2
+ ARG NGINX_VERSION=1.27-alpine
3
+
4
+ FROM node:${NODE_VERSION} AS builder
5
+ WORKDIR /usr/src/app
6
+ COPY . .
7
+ RUN {{ciCommand}} && \
8
+ {{packageManager}} run build:prod
9
+
10
+ FROM nginx:${NGINX_VERSION}
11
+
12
+ RUN rm /etc/nginx/conf.d/default.conf
13
+ 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
+ COPY --from=builder /usr/src/app/dist/src /usr/share/nginx/html
16
+
17
+ EXPOSE 9090
18
+
19
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
20
+ CMD curl -f http://localhost:9090/health || exit 1
21
+
22
+ CMD ["nginx", "-g", "daemon off;"]
@@ -0,0 +1,19 @@
1
+ gzip on;
2
+ gzip_vary on;
3
+ gzip_proxied any;
4
+ gzip_comp_level 6;
5
+ gzip_min_length 1000;
6
+
7
+ gzip_types
8
+ text/plain
9
+ text/css
10
+ text/xml
11
+ text/javascript
12
+ application/json
13
+ application/javascript
14
+ application/xml+rss
15
+ application/rss+xml
16
+ font/truetype
17
+ font/opentype
18
+ application/vnd.ms-fontobject
19
+ image/svg+xml;
@@ -0,0 +1,78 @@
1
+ server {
2
+ # ---------------------------
3
+ # LISTEN PORTS
4
+ # ---------------------------
5
+ listen 9090;
6
+ server_name _;
7
+
8
+ # ---------------------------
9
+ # ROOT + ROUTING
10
+ # ---------------------------
11
+ root /usr/share/nginx/html;
12
+ index index.html;
13
+
14
+ # SPA routing: serve index.html for unknown paths
15
+ location / {
16
+ try_files $uri $uri/ /index.html;
17
+ }
18
+
19
+ # ---------------------------
20
+ # SECURITY HEADERS
21
+ # ---------------------------
22
+
23
+ # Prevent clickjacking
24
+ add_header X-Frame-Options "SAMEORIGIN" always;
25
+
26
+ # Block MIME type sniffing
27
+ add_header X-Content-Type-Options "nosniff" always;
28
+
29
+ # Legacy XSS filter
30
+ add_header X-XSS-Protection "1; mode=block" always;
31
+
32
+ # Limit referrer leakage
33
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
34
+
35
+ # Restrict powerful browser APIs
36
+ add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
37
+
38
+ # Content Security Policy
39
+ add_header Content-Security-Policy "default-src 'self'; \
40
+ script-src 'self'; \
41
+ style-src 'self' 'unsafe-inline'; \
42
+ img-src 'self' data: https:; \
43
+ font-src 'self'; \
44
+ connect-src 'self'; \
45
+ object-src 'none'; \
46
+ base-uri 'self'; \
47
+ form-action 'self'; \
48
+ frame-ancestors 'none';" always;
49
+
50
+ # Hide nginx version
51
+ server_tokens off;
52
+
53
+ # ---------------------------
54
+ # STATIC ASSETS CACHING
55
+ # ---------------------------
56
+
57
+ # Immutable cache for static assets
58
+ location ~* \.(js|css|png|jpg|jpeg|gif|avif|ico|svg|woff|woff2|ttf|eot)$ {
59
+ expires 1y;
60
+ add_header Cache-Control "public, immutable";
61
+ }
62
+
63
+ # No caching for HTML files (SPA shell)
64
+ location ~* \.html$ {
65
+ expires -1;
66
+ add_header Cache-Control "no-cache, no-store, must-revalidate";
67
+ add_header Pragma "no-cache";
68
+ }
69
+
70
+ # ---------------------------
71
+ # HEALTH ENDPOINT
72
+ # ---------------------------
73
+ location /health {
74
+ access_log off;
75
+ add_header Content-Type text/plain;
76
+ return 200 "healthy\n";
77
+ }
78
+ }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { print, runCommand } from '../utils/index.js';
4
+
5
+ async function startDev() {
6
+ try {
7
+ console.clear();
8
+ print.boldHead('🚀 Starting Dev Environment...');
9
+
10
+ await runCommand('{{packageManager}}', ['run', 'build']);
11
+
12
+ const { serve, watcher } = await Promise.all([
13
+ runCommand('npm', ['run', 'serve:only']),
14
+ runCommand('node', ['tools/dev/watcher.js'])
15
+ ]);
16
+
17
+ serve.on('close', (code) => {
18
+ print.error(`live-server exited with code ${code}`);
19
+ process.exit(code);
20
+ });
21
+ watcher.on('close', (code) => {
22
+ print.error(`watcher exited with code ${code}`);
23
+ });
24
+ } catch (err) {
25
+ print.boldError(`Dev startup failed: ${err.message}`);
26
+ }
27
+ }
28
+
29
+ startDev().catch((err) => {
30
+ print.boldError(`Unexpected error: ${err.message}`);
31
+ exit(1);
32
+ });
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import liveServer from 'live-server';
4
+
5
+ const params = {
6
+ port: 9090,
7
+ root: 'dist/src',
8
+ open: false, // set to true to open a new window in browser
9
+ logLevel: 0, // 0 = errors only, 1 = some info, 2 = verbose
10
+ entryFile: 'index.html',
11
+ wait: 100, // debounce reloads
12
+ middleware: [
13
+ function(req, res, next) {
14
+ if (req.url.includes('.') && !req.url.endsWith('/')) {
15
+ return next();
16
+ }
17
+ req.url = '/index.html';
18
+ next();
19
+ }
20
+ ]
21
+ };
22
+
23
+ liveServer.start(params);
@@ -1,7 +1,7 @@
1
1
  import chokidar from 'chokidar';
2
2
  import { exec } from 'child_process';
3
3
  import path from 'path';
4
- import { print } from './utils/index.js'
4
+ import { print } from '../utils/index.js'
5
5
 
6
6
  const watcher = chokidar.watch(['src'], {
7
7
  ignored: /(^|[/\\])\../,
@@ -9,7 +9,6 @@ const watcher = chokidar.watch(['src'], {
9
9
  });
10
10
 
11
11
  let isBuilding = false;
12
-
13
12
  let buildTimeout = null;
14
13
 
15
14
  watcher.on('change', (filePath) => {
@@ -25,20 +24,21 @@ watcher.on('change', (filePath) => {
25
24
  print.boldInfo(`\n🔄 File changed: ${path.relative(process.cwd(), filePath)}\n`);
26
25
  print.info('\nRebuilding...');
27
26
 
28
- const command = {{#if template}}'npm run build'{{else}}filePath.includes('.scss') ? 'npm run build-static' : 'npm run build'{{/if}};
27
+ const command = '{{packageManager}} run build';
29
28
 
30
29
  exec(command, (err, stdout, stderr) => {
31
30
  if (stdout) process.stdout.write(stdout);
32
31
  if (stderr) process.stderr.write(stderr);
33
-
34
- print.boldSuccess('\n✅ Build completed!');
35
- print.boldHead('\nWaiting for changes...\n');
36
-
32
+ if (err) {
33
+ print.error(`\n❌ Build failed: ${err.message}`);
34
+ } else {
35
+ print.boldHead('Watching for changes...');
36
+ }
37
37
  isBuilding = false;
38
38
  });
39
39
  }, 100);
40
40
  });
41
41
 
42
42
  watcher.on('ready', () => {
43
- print.boldHead('\n👀 Watching for changes...\n');
43
+ print.boldHead('\nWatching for changes...\n');
44
44
  });
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { allFormats, getLastWord, print } from "../utils/index.js";
4
- import { generateFile, appendToIndex{{#if i18n}}, generateJson{{/if}} } from "./handle-file.js";
3
+ import path from "path";
4
+ import { allFormats, getLastWord, print{{#if stylinNutin}}, promptBoolean{{/if}} } from "../utils/index.js";
5
+ import { generateFile, appendToIndex{{#if i18n}}, generateJson{{/if}}{{#if stylinNutin}}, generateStylesheet{{/if}} } from "./handle-file.js";
5
6
  import { serviceTemplate, componentTemplate, viewTemplate{{#if template}}, htmlTemplate{{/if}} } from "./templates/index.js";
6
7
 
7
8
  // Constants and Setup
@@ -14,7 +15,7 @@ if (!rawType || !rawFullPath) {
14
15
  const type = allFormats(rawType);
15
16
  const fullPath = allFormats(rawFullPath);
16
17
  const name = allFormats(getLastWord(fullPath.kebab));
17
- const targetPath = `src/app/${type.kebab}s/${fullPath.kebab}`;
18
+ const targetPath = path.join('src', 'app', `${type.kebab}s/${fullPath.kebab}`);
18
19
 
19
20
  // Creator Mapping
20
21
  const creators = {
@@ -27,8 +28,12 @@ const creators = {
27
28
  handleError("Failed to generate service", err);
28
29
  }
29
30
  },
30
- component: (name, targetPath) => {
31
+ component: {{#if stylinNutin}}async {{/if}}(name, targetPath) => {
31
32
  print.section(`Creating component: ${name.capitalized}`);
33
+ {{#if stylinNutin}}
34
+ const doCreateStylesheet = await promptBoolean('Create component\'s stylesheet in styles/components ?');
35
+ {{/if}}
36
+
32
37
  try {
33
38
  generateFile({ name, targetPath, templateFn: componentTemplate, suffix: 'component' });
34
39
  {{#if template}}
@@ -37,13 +42,20 @@ const creators = {
37
42
  {{#if i18n}}
38
43
  generateJson({ targetPath, name });
39
44
  {{/if}}
45
+ {{#if stylinNutin}}
46
+ if (doCreateStylesheet) generateStylesheet({ name, suffix: 'component' });
47
+ {{/if}}
40
48
  appendToIndex({ name, targetPath, suffix: 'component' });
41
49
  } catch (err) {
42
50
  handleError("Failed to generate component", err);
43
51
  }
44
52
  },
45
- view: (name, targetPath) => {
53
+ view: {{#if stylinNutin}}async {{/if}}(name, targetPath) => {
46
54
  print.section(`Creating view: ${name.capitalized}`);
55
+ {{#if stylinNutin}}
56
+ const doCreateStylesheet = await promptBoolean('Create component\'s stylesheet in styles/components ?');
57
+ {{/if}}
58
+
47
59
  try {
48
60
  generateFile({ name, targetPath, templateFn: viewTemplate, suffix: 'view' });
49
61
  {{#if template}}
@@ -52,6 +64,9 @@ const creators = {
52
64
  {{#if i18n}}
53
65
  generateJson({ targetPath, name });
54
66
  {{/if}}
67
+ {{#if stylinNutin}}
68
+ if (doCreateStylesheet) generateStylesheet({ name, suffix: 'view' });
69
+ {{/if}}
55
70
  appendToIndex({name, targetPath, suffix: 'view' });
56
71
  } catch (err) {
57
72
  handleError("Failed to generate view", err);
@@ -63,7 +78,7 @@ const creators = {
63
78
  const create = creators[type.kebab];
64
79
 
65
80
  if (create) {
66
- create(name, targetPath);
81
+ {{#if stylinNutin}}await {{/if}}create(name, targetPath);
67
82
  print.boldSuccess(`\n${type.capitalized} ${name.capitalized} has been generated.\n`)
68
83
  } else {
69
84
  showUsageAndExit(`Unsupported type: '${type.kebab}'`);