@nutin/cli 1.1.0 → 1.2.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 (48) hide show
  1. package/CHANGELOG.md +32 -2
  2. package/README.md +47 -1
  3. package/lib/index.mjs +6 -6
  4. package/lib/src/file-generator.mjs +7 -5
  5. package/lib/src/package-manager.mjs +54 -45
  6. package/lib/src/prompts.mjs +11 -11
  7. package/lib/src/utils.mjs +2 -1
  8. package/package.json +1 -1
  9. package/templates/base/.dockerignore.hbs +10 -1
  10. package/templates/base/Dockerfile.hbs +28 -8
  11. package/templates/base/README.md.hbs +23 -0
  12. package/templates/base/nginx.conf.hbs +43 -0
  13. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +2 -1
  14. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +11 -11
  15. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +3 -2
  16. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +55 -27
  17. package/templates/base/src/core/services/http-client/http-client.ts.hbs +24 -17
  18. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +25 -10
  19. package/templates/base/src/core/services/router/router.ts.hbs +1 -0
  20. package/templates/base/src/index.html.hbs +1 -2
  21. package/templates/base/src/libs/popover/popover.ts.hbs +2 -1
  22. package/templates/base/src/styles/main.scss.hbs +2 -1
  23. package/templates/base/tools/builder/builder.js.hbs +17 -27
  24. package/templates/base/tools/builder/core/copy-static.js.hbs +16 -20
  25. package/templates/base/tools/builder/core/esbuild.js.hbs +36 -0
  26. package/templates/base/tools/builder/core/finalize-build.js.hbs +24 -0
  27. package/templates/base/tools/builder/core/minify-html.js.hbs +60 -0
  28. package/templates/base/tools/builder/core/paths.js.hbs +19 -0
  29. package/templates/base/tools/builder/core/sass.config.js.hbs +16 -9
  30. package/templates/base/tools/builder/core/tsc.js.hbs +8 -0
  31. package/templates/base/tools/builder/core/validate-html.js.hbs +26 -11
  32. package/templates/base/tools/dev.js.hbs +43 -0
  33. package/templates/base/tools/generator/generator.js.hbs +9 -2
  34. package/templates/base/tools/generator/handle-file.js.hbs +23 -0
  35. package/templates/base/tools/generator/templates/index.js.hbs +3 -0
  36. package/templates/base/tools/serve.js.hbs +23 -0
  37. package/templates/base/tools/utils/export-flags.js.hbs +5 -0
  38. package/templates/base/tools/utils/get-files-recursive.js.hbs +1 -3
  39. package/templates/base/tools/utils/index.js.hbs +5 -1
  40. package/templates/base/tools/utils/prompt-boolean.js.hbs +15 -0
  41. package/templates/base/tools/utils/run-command.js.hbs +5 -0
  42. package/templates/base/tools/utils/run-script.js.hbs +22 -0
  43. package/templates/base/tools/watcher.js.hbs +7 -7
  44. package/templates/features/i18n/tools/builder/core/build-i18n.js.hbs +11 -16
  45. package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +1 -1
  46. package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +3 -0
  47. package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +4 -0
  48. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +13 -26
@@ -1,18 +1,35 @@
1
- import { readFile } from 'fs/promises';
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ import { JSDOM } from 'jsdom';
2
3
  import { exit } from 'process';
3
- import { print } from '../../utils/index.js';
4
+ import { print, isProd } from '../../utils/index.js';
5
+ import { PATHS } from './paths.js';
6
+ import path from 'path';
4
7
 
5
8
  function errorExit(message) {
6
9
  print.boldError(`[ERROR] ${message}`);
7
10
  exit(1);
8
11
  }
9
12
 
13
+ async function addEntrypoint(htmlContent, filePath) {
14
+ print.info('Adding script in HTML...');
15
+
16
+ const dom = new JSDOM(htmlContent);
17
+ const document = dom.window.document;
18
+
19
+ const newScript = document.createElement("script");
20
+ newScript.type = "module";
21
+ newScript.src = isProd ? "/bundle.js" : "/app/main.js";
22
+
23
+ document.body.appendChild(newScript);
24
+ const serialized = dom.serialize().replace(/>\s+</g, '><')
25
+
26
+ writeFile(filePath, serialized, "utf8");
27
+
28
+ return htmlContent;
29
+ }
30
+
10
31
  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}}
32
+ const filePath = path.join(PATHS.tempSource, 'index.html');
16
33
 
17
34
  let htmlContent;
18
35
  try {
@@ -25,14 +42,12 @@ async function validateHtml() {
25
42
  errorExit('Missing #app container in HTML');
26
43
  }
27
44
 
28
- if (!/<script[^>]*type=["']module["']/.test(htmlContent)) {
29
- errorExit('No module scripts found in HTML');
30
- }
31
-
32
45
  if (!/<link[^>]*rel=["']stylesheet["']/.test(htmlContent)) {
33
46
  errorExit('No stylesheet found in HTML');
34
47
  }
35
48
 
49
+ await addEntrypoint(htmlContent, filePath);
50
+
36
51
  print.info('HTML validation passed');
37
52
  }
38
53
 
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'child_process';
4
+ import { print, runCommand } from './utils/index.js';
5
+
6
+ function run(command, args = [], label = '') {
7
+ return new Promise((resolve, reject) => {
8
+ print.info(`\n▶ ${label || command}...`);
9
+
10
+ const child = spawn(command, args, { stdio: 'inherit', shell: true });
11
+
12
+ child.on('close', (code) => {
13
+ if (code === 0) resolve();
14
+ else reject(new Error(`${command} exited with code ${code}`));
15
+ });
16
+ });
17
+ }
18
+
19
+ async function startDev() {
20
+ try {
21
+ console.clear();
22
+ print.boldHead('🚀 Starting Dev Environment...');
23
+
24
+ await run('{{packageManager}}', ['run', 'build'], 'Running initial build');
25
+
26
+ const serve = spawn('{{packageManager}}', ['run', 'serve:only'], { stdio: 'inherit', shell: true });
27
+ const watcher = spawn('node', ['tools/watcher.js'], { stdio: 'inherit', shell: true });
28
+
29
+ print.boldSuccess('\nDev server and watcher running!\n');
30
+
31
+ serve.on('close', (code) => {
32
+ print.error(`live-server exited with code ${code}`);
33
+ process.exit(code);
34
+ });
35
+ watcher.on('close', (code) => {
36
+ print.error(`watcher exited with code ${code}`);
37
+ });
38
+ } catch (err) {
39
+ print.boldError(`Dev startup failed: ${err.message}`);
40
+ }
41
+ }
42
+
43
+ startDev();
@@ -1,7 +1,7 @@
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 { allFormats, getLastWord, print{{#if stylinNutin}}, promptBoolean{{/if}} } from "../utils/index.js";
4
+ import { generateFile, appendToIndex{{#if i18n}}, generateJson{{/if}}{{#if stylinNutin}}, generateStylesheet{{/if}} } from "./handle-file.js";
5
5
  import { serviceTemplate, componentTemplate, viewTemplate{{#if template}}, htmlTemplate{{/if}} } from "./templates/index.js";
6
6
 
7
7
  // Constants and Setup
@@ -29,6 +29,10 @@ const creators = {
29
29
  },
30
30
  component: (name, targetPath) => {
31
31
  print.section(`Creating component: ${name.capitalized}`);
32
+ {{#if stylinNutin}}
33
+ const doCreateStylesheet = await promptBoolean('Create component\'s stylesheet in styles/components ?');
34
+ {{/if}}
35
+
32
36
  try {
33
37
  generateFile({ name, targetPath, templateFn: componentTemplate, suffix: 'component' });
34
38
  {{#if template}}
@@ -37,6 +41,9 @@ const creators = {
37
41
  {{#if i18n}}
38
42
  generateJson({ targetPath, name });
39
43
  {{/if}}
44
+ {{#if stylinNutin}}
45
+ if (doCreateStylesheet) generateStylesheet(name);
46
+ {{/if}}
40
47
  appendToIndex({ name, targetPath, suffix: 'component' });
41
48
  } catch (err) {
42
49
  handleError("Failed to generate component", err);
@@ -59,4 +59,27 @@ export function generateJson({ targetPath, name }) {
59
59
  });
60
60
  }
61
61
 
62
+ {{/if}}
63
+ {{#if stylinNutin}}
64
+ export function generateStylesheet(name) {
65
+ const template = scssTemplate();
66
+ const componentsStylePath = path.join('src', 'styles', 'components');
67
+ const filePath = path.join(componentsStylePath, `_${name.kebab}.scss`);
68
+ const indexPath = path.join(componentsStylePath, `_index.scss`);
69
+
70
+ if (fs.existsSync(filePath)) {
71
+ print.boldError('A file with this name already exists');
72
+ process.exit(1);
73
+ }
74
+ fs.writeFileSync(filePath, template);
75
+
76
+ const lineToAppend = `@forward "${name.kebab}";\n`;
77
+
78
+ try {
79
+ fs.appendFileSync(indexPath, lineToAppend, 'utf8');
80
+ } catch (err) {
81
+ print.error(`Error appending line: ${err}`);
82
+ }
83
+ }
84
+
62
85
  {{/if}}
@@ -7,3 +7,6 @@ export * from './html.template.js';
7
7
  {{#if i18n}}
8
8
  export * from './json.template.js';
9
9
  {{/if}}
10
+ {{#if stylinNutin}}
11
+ export * from './scss.template.js';
12
+ {{/if}}
@@ -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);
@@ -0,0 +1,5 @@
1
+ import process from 'process';
2
+
3
+ export const isProd = process.env.npm_config_bundle || process.env.NODE_ENV === 'production';
4
+
5
+ export const isVerbose = process.env.npm_config_log;
@@ -2,7 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
 
4
4
  export function getFilesRecursive(dir, extension) {
5
- const fullDir = path.resolve(process.cwd(), dir);
5
+ const fullDir = path.resolve(dir);
6
6
 
7
7
  if (!fs.existsSync(fullDir)) return [];
8
8
 
@@ -14,10 +14,8 @@ export function getFilesRecursive(dir, extension) {
14
14
  const fullPath = path.join(currentDir, item.name);
15
15
 
16
16
  if (item.isDirectory()) {
17
- // Recursively search subdirectories
18
17
  files.push(...findFilesRecursively(fullPath));
19
18
  } else if (item.isFile() && item.name.endsWith(extension)) {
20
- // Add files to the results
21
19
  files.push(fullPath);
22
20
  }
23
21
  }
@@ -1,3 +1,7 @@
1
1
  export * from './print.js';
2
2
  export * from './format.js';
3
- export * from './get-files-recursive.js';
3
+ export * from './get-files-recursive.js';
4
+ export * from './run-command.js';
5
+ export * from './run-script.js';
6
+ export * from './export-flags.js';
7
+ export * from './prompt-boolean.js';
@@ -0,0 +1,15 @@
1
+ import readline from 'readline';
2
+
3
+ export const promptBoolean = (question) => {
4
+ const rl = readline.createInterface({
5
+ input: process.stdin,
6
+ output: process.stdout
7
+ });
8
+
9
+ return new Promise((resolve) => {
10
+ rl.question(`${question} (y/n): `, (answer) => {
11
+ rl.close();
12
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
13
+ });
14
+ });
15
+ };
@@ -0,0 +1,5 @@
1
+ export function runCommand(cmd) {
2
+ import('child_process').then(({ execSync }) => {
3
+ execSync(cmd, { stdio: 'inherit' });
4
+ });
5
+ }
@@ -0,0 +1,22 @@
1
+ import { resolve } from 'path';
2
+ import { spawnSync } from 'child_process';
3
+ import process from 'process';
4
+ import { print } from './index.js';
5
+
6
+ export function runScript(path, message) {
7
+ print.boldSection(`\n${message}`);
8
+
9
+ const args = [resolve(path)];
10
+
11
+ const result = spawnSync('node', args,
12
+ {
13
+ stdio: 'inherit',
14
+ env: { ...process.env },
15
+ }
16
+ );
17
+
18
+ if (result.status !== 0) {
19
+ print.boldError(`\nScript ${path} failed.`);
20
+ process.exit(result.status ?? 1);
21
+ }
22
+ }
@@ -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,19 +1,13 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { exit } from 'process';
4
- import { getFilesRecursive, print } from '../../utils/index.js';
4
+ import { getFilesRecursive, print, isVerbose } from '../../utils/index.js';
5
+ import { PATHS } from './paths.js';
5
6
 
6
- const sourceRoot = './src/app';
7
- {{#if template}}
8
- const destDir = './dist-build/src/locales';
9
- const cleanupDir = './dist-build/src/app';
10
- {{else}}
11
- const destDir = './dist/src/locales';
12
- const cleanupDir = './dist/src/app';
13
- {{/if}}
7
+ const destDir = path.join(PATHS.tempSource, 'locales');
14
8
 
15
9
  function findLocaleFiles() {
16
- const allFiles = getFilesRecursive(sourceRoot, '.json');
10
+ const allFiles = getFilesRecursive(PATHS.sourceApp, '.json');
17
11
  const localeFileMap = {};
18
12
 
19
13
  allFiles.forEach(file => {
@@ -66,18 +60,19 @@ async function removeJsonFiles(directory) {
66
60
  if (file.includes(`${path.sep}locales${path.sep}`)) {
67
61
  try {
68
62
  await fs.promises.unlink(file);
69
- print.info(`Deleted locale JSON file: ${file}`);
63
+ if (isVerbose) print.info(`Deleted locale JSON file: ${file}`);
70
64
  } catch (err) {
71
65
  print.error(`Failed to remove ${file}: ${err.message}`);
72
66
  }
73
- } else {
67
+ }
68
+ else if (isVerbose) {
74
69
  print.info(`Skipping non-locale JSON file: ${file}`);
75
70
  }
76
71
  }
77
72
 
78
73
  await removeEmptyDirectories(directory);
79
74
 
80
- print.info(`✅ JSON files cleanup complete.`);
75
+ if (isVerbose) print.info(`✅ JSON files cleanup complete.`);
81
76
  }
82
77
 
83
78
  async function removeEmptyDirectories(directory) {
@@ -102,12 +97,12 @@ async function removeEmptyDirectories(directory) {
102
97
  }
103
98
  }
104
99
 
105
- if (directory !== cleanupDir) {
100
+ if (directory !== PATHS.tempApp) {
106
101
  try {
107
102
  const updatedItems = await fs.promises.readdir(directory);
108
103
  if (updatedItems.length === 0) {
109
104
  await fs.promises.rmdir(directory, { recursive: false });
110
- print.info(`📁 Removed empty directory: ${directory}`);
105
+ if (isVerbose) print.info(`📁 Removed empty directory: ${directory}`);
111
106
  }
112
107
  } catch (err) {
113
108
  print.error(`Failed to remove empty directory ${directory}: ${err.message}`);
@@ -128,7 +123,7 @@ async function mergeJson() {
128
123
  );
129
124
  print.info(`✅ Json files for locale ${locale} merged successfully`);
130
125
  }
131
- removeJsonFiles(cleanupDir);
126
+ removeJsonFiles(PATHS.tempApp);
132
127
  }
133
128
 
134
129
  mergeJson().catch((err) => {
@@ -5,5 +5,5 @@
5
5
  /*
6
6
  Add your classes here
7
7
  Custom classes prefix : 'c-'
8
- Component- or view-specific classes prefix : '/name/__'
8
+ ( prefix view-specific classes with viewname__ )
9
9
  */
@@ -0,0 +1,3 @@
1
+ // global components style export
2
+
3
+ // @forward "my-component";
@@ -0,0 +1,4 @@
1
+ export const scssTemplate = () => `@use "sass:map";
2
+ @use "variables" as *;
3
+ @use "mixins";
4
+ `;
@@ -1,52 +1,38 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { exit } from 'process';
4
- import { print } from '../../utils/index.js';
4
+ import { getFilesRecursive, print, isVerbose, isProd } from '../../utils/index.js';
5
+ import { minifyHTML } from './minify-html.js';
6
+ import { PATHS } from './paths.js';
5
7
 
6
- const JS_DIR = 'dist-build/src/app';
7
8
  const PLACEHOLDER = '__TEMPLATE_PLACEHOLDER__';
8
9
 
9
- async function findHtmlFiles(dir) {
10
- const entries = await fs.readdir(dir, { withFileTypes: true });
11
- const files = await Promise.all(
12
- entries.map(async (entry) => {
13
- const fullPath = path.join(dir, entry.name);
14
- if (entry.isDirectory()) {
15
- return findHtmlFiles(fullPath);
16
- } else if (entry.isFile() && fullPath.endsWith('.html')) {
17
- return [fullPath];
18
- }
19
- return [];
20
- })
21
- );
22
- return files.flat();
23
- }
24
-
25
10
  async function mergeTemplates() {
26
- const htmlFiles = await findHtmlFiles(JS_DIR);
11
+ const htmlFiles = await getFilesRecursive(PATHS.tempApp, 'html');
12
+ const fileExtension = isProd ? '.ts' : '.js';
27
13
 
28
14
  for (const htmlPath of htmlFiles) {
29
- const jsPath = htmlPath.replace(/\.html$/, '.js');
15
+ const jsPath = htmlPath.replace(/\.html$/, fileExtension);
30
16
  const htmlFilename = path.basename(htmlPath);
31
17
  const jsFilename = path.basename(jsPath);
32
18
 
33
19
  try {
34
20
  const htmlContent = await fs.readFile(htmlPath, 'utf-8');
35
- const minified = htmlContent.replace(/\n/g, '');
36
- const escaped = minified.replace(/([\\/&`"])/g, '\\$1');
21
+ const minifiedHtml = await minifyHTML(htmlContent);
22
+ if (isVerbose) print.info(`Minified HTML: ${file}`);
37
23
 
38
24
  try {
39
25
  let jsContent = await fs.readFile(jsPath, 'utf-8');
40
26
  if (!jsContent.includes(PLACEHOLDER)) {
41
27
  print.boldError(`WARNING: No matching placeholder in ${jsFilename}`);
42
- print.error(`If you're using external templates, make sure the template const is \`${PLACEHOLDER}\`.`);
43
- print.error(`If using inline templates, remove ${htmlFilename}.`);
28
+ print.error(`Make sure the template const is \`${PLACEHOLDER}\`.`);
44
29
  exit(1);
45
30
  }
46
31
 
47
- jsContent = jsContent.replace(PLACEHOLDER, escaped);
32
+ jsContent = jsContent.replace(PLACEHOLDER, minifiedHtml);
48
33
  await fs.writeFile(jsPath, jsContent);
49
- print.info(`✅ Updated ${jsFilename} with template from ${htmlFilename}.`);
34
+ await fs.unlink(htmlPath);
35
+ if (isVerbose) print.info(`✅ Updated ${jsFilename} with template from ${htmlFilename}.`);
50
36
  } catch (err) {
51
37
  print.boldError(`ERROR: Cannot update ${jsFilename}. ${err.message}`);
52
38
  }
@@ -54,6 +40,7 @@ async function mergeTemplates() {
54
40
  print.boldError(`ERROR: Failed to read ${htmlFilename}. ${err.message}`);
55
41
  }
56
42
  }
43
+ print.info(`HTML templates minified and merged in scripts.`)
57
44
  }
58
45
 
59
46
  mergeTemplates().catch((err) => {