@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,10 +1,12 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { print } from '../utils/index.js';
3
+ import { print{{#if i18n}}, LANGUAGES{{/if}} } from '../utils/index.js';
4
4
  {{#if i18n}}
5
- import { LANGUAGES } from '../../src/app/languages.js';
6
5
  import { jsonTemplate } from './templates/index.js';
7
6
  {{/if}}
7
+ {{#if stylinNutin}}
8
+ import { scssTemplate } from './templates/index.js';
9
+ {{/if}}
8
10
 
9
11
  export function generateFile({
10
12
  name,
@@ -59,4 +61,32 @@ export function generateJson({ targetPath, name }) {
59
61
  });
60
62
  }
61
63
 
64
+ {{/if}}
65
+ {{#if stylinNutin}}
66
+ export function generateStylesheet({ name, suffix }) {
67
+ const template = scssTemplate();
68
+ const stylesPath = path.join('src', 'styles', `${suffix}s`);
69
+ const filePath = path.join(stylesPath, `_${name.kebab}.${suffix}.scss`);
70
+ const indexPath = path.join(stylesPath, `_index.scss`);
71
+
72
+ if (!fs.existsSync(stylesPath) || !fs.existsSync(indexPath)) {
73
+ print.boldError('Non existing path');
74
+ process.exit(1);
75
+ }
76
+ if (fs.existsSync(filePath)) {
77
+ print.boldError('A file with this name already exists');
78
+ process.exit(1);
79
+ }
80
+
81
+ fs.writeFileSync(filePath, template);
82
+
83
+ const lineToAppend = `@forward "${name.kebab}.${suffix}";\n`;
84
+
85
+ try {
86
+ fs.appendFileSync(indexPath, lineToAppend, 'utf8');
87
+ } catch (err) {
88
+ print.error(`Error appending line: ${err}`);
89
+ }
90
+ }
91
+
62
92
  {{/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,6 @@
1
+ import process from 'process';
2
+ import builderConfig from '../../builder.config.js';
3
+
4
+ export const isProd = process.env.npm_config_bundle || process.env.NODE_ENV === 'production';
5
+
6
+ export const isVerbose = process.env.npm_config_log || builderConfig.verbose;
@@ -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,10 @@
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';
8
+ {{#if i18n}}
9
+ export * from './languages.js';
10
+ {{/if}}
@@ -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,12 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ export function runCommand(command, args = []) {
4
+ return new Promise((resolve, reject) => {
5
+ const child = spawn(command, args, { stdio: 'inherit', shell: true });
6
+
7
+ child.on('close', (code) => {
8
+ if (code === 0) resolve();
9
+ else reject(new Error(`${command} exited with code ${code}`));
10
+ });
11
+ });
12
+ }
@@ -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(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
+ }
@@ -0,0 +1,3 @@
1
+ import langs from '../../config/languages.json' with { type: 'json' };
2
+
3
+ export const CONFIG = { langs } as const;
@@ -65,6 +65,8 @@ describe('i18n module', async () => {
65
65
  configurable: true,
66
66
  value: "en-US"
67
67
  });
68
+ I18nService['_LANGUAGES'] = [ 'en', 'fr' ];
69
+ I18nService['_DEFAULT_LANGUAGE'] = 'en';
68
70
  });
69
71
  afterAll(() => {
70
72
  Object.defineProperty(navigator, 'language', {
@@ -1,4 +1,4 @@
1
- import { DEFAULT_LANGUAGE, LANGUAGES, Language, Translations } from '../../../app/languages.js';
1
+ import { DEFAULT_LANGUAGE, LANGUAGES, Language, Translations } from './languages.js';
2
2
  import { Service } from '../../index.js';
3
3
 
4
4
  export class I18n extends Service<I18n> {
@@ -11,6 +11,7 @@ export class I18n extends Service<I18n> {
11
11
  constructor() {
12
12
  super();
13
13
  this.registerCleanup(this.resetTranslations);
14
+ this._currentLanguage = this.getBrowserLanguage();
14
15
  }
15
16
 
16
17
  private getBrowserLanguage() {
@@ -89,6 +90,14 @@ export class I18n extends Service<I18n> {
89
90
  return this._currentLanguage;
90
91
  }
91
92
 
93
+ public get defaultLanguage(): Language {
94
+ return this._DEFAULT_LANGUAGE;
95
+ }
96
+
97
+ public get languages(): Language[] {
98
+ return this._LANGUAGES;
99
+ }
100
+
92
101
  public async initTranslations(): Promise<void> {
93
102
  const browserLang = this.getBrowserLanguage();
94
103
  const lang = this._LANGUAGES.includes(browserLang) ? browserLang : this._DEFAULT_LANGUAGE;
@@ -0,0 +1,7 @@
1
+ import { CONFIG } from '../../config.js';
2
+
3
+ export const LANGUAGES = CONFIG.langs.languages;
4
+ export const DEFAULT_LANGUAGE = CONFIG.langs.defaultLanguage as (typeof LANGUAGES)[number];
5
+
6
+ export type Language = typeof LANGUAGES[number];
7
+ export type Translations = Record<string, any>;
@@ -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.boldInfo(`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}`);
@@ -126,9 +121,9 @@ async function mergeJson() {
126
121
  `${destDir}/${locale}.json`,
127
122
  JSON.stringify(combined, null, 2)
128
123
  );
129
- print.info(`✅ Json files for locale ${locale} merged successfully`);
124
+ if (isVerbose) print.boldInfo(`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) => {
@@ -0,0 +1,7 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const langsPath = path.join(process.cwd(), 'config', 'languages.json');
5
+ const langs = JSON.parse(fs.readFileSync(langsPath, 'utf-8'));
6
+ export const LANGUAGES = langs.languages;
7
+ export const DEFAULT_LANGUAGE = langs.defaultLanguage;
@@ -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,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
+ if (isVerbose) print.info(`HTML templates minified and merged in scripts.\n`)
57
44
  }
58
45
 
59
46
  mergeTemplates().catch((err) => {
@@ -1,4 +1,4 @@
1
- export * from './src/jsdom-setup.js';
1
+ export * from './src/globals/jsdom-setup.js';
2
2
  export * from './src/globals/register-test-globals.js';
3
3
  export * from './src/utils/print.js';
4
4
  export * from './src/utils/test-discovery.js';
@@ -0,0 +1,136 @@
1
+ global.expect = (actual) => {
2
+ const expectObj = {
3
+ toBe(expected) {
4
+ if (actual !== expected) {
5
+ throw new Error(`Expected ${actual} to be strictly equal to ${expected}`);
6
+ }
7
+ },
8
+ toEqual(expected) {
9
+ const deepEqual = (a, b) => {
10
+ if (a === b) return true;
11
+ if (typeof a !== typeof b) return false;
12
+ if (Array.isArray(a) && Array.isArray(b)) {
13
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
14
+ }
15
+ if (a && b && typeof a === 'object') {
16
+ const keysA = Object.keys(a);
17
+ const keysB = Object.keys(b);
18
+ if (keysA.length !== keysB.length) return false;
19
+ return keysA.every(k => deepEqual(a[k], b[k]));
20
+ }
21
+ return false;
22
+ };
23
+
24
+ if (!deepEqual(actual, expected)) {
25
+ throw new Error(`Expected ${JSON.stringify(actual)} to deeply equal ${JSON.stringify(expected)}`);
26
+ }
27
+ },
28
+ toContain(substring) {
29
+ if (!actual.includes(substring)) {
30
+ throw new Error(`Expected '${actual}' to contain '${substring}'`);
31
+ }
32
+ },
33
+ toBeTruthy() {
34
+ if (!actual) {
35
+ throw new Error(`Expected ${actual} to be truthy`);
36
+ }
37
+ },
38
+ toBeFalsy() {
39
+ if (actual) {
40
+ throw new Error(`Expected ${actual} to be falsy`);
41
+ }
42
+ },
43
+ toBeDefined() {
44
+ if (actual === null || actual === undefined) {
45
+ throw new Error(`Expected value to be defined, but got ${actual}`);
46
+ }
47
+ },
48
+ toBeUndefined() {
49
+ if (actual !== null && actual !== undefined) {
50
+ throw new Error(`Expected value to be undefined, but got ${actual}`);
51
+ }
52
+ },
53
+ toBeInstanceOf(constructor) {
54
+ if (!(actual instanceof constructor)) {
55
+ throw new Error(`Expected ${actual} to be instance of ${constructor.name}`);
56
+ }
57
+ },
58
+ toHaveBeenCalled() {
59
+ if (!actual?.calls || actual.calls.length === 0) {
60
+ throw new Error(`Expected function to have been called, but it was not`);
61
+ }
62
+ },
63
+ toHaveBeenCalledWith(...expectedArgs) {
64
+ const deepEqual = (a, b) => {
65
+ if (a === b) return true;
66
+ if (typeof a !== typeof b) return false;
67
+
68
+ if (Array.isArray(a) && Array.isArray(b)) {
69
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
70
+ }
71
+
72
+ if (a && b && typeof a === 'object') {
73
+ const keysA = Object.keys(a);
74
+ const keysB = Object.keys(b);
75
+ if (keysA.length !== keysB.length) return false;
76
+ return keysA.every(k => deepEqual(a[k], b[k]));
77
+ }
78
+
79
+ return false;
80
+ };
81
+
82
+ const matched = actual?.calls?.some(args =>
83
+ args.length === expectedArgs.length &&
84
+ args.every((arg, i) => deepEqual(arg, expectedArgs[i]))
85
+ );
86
+
87
+ if (!matched) {
88
+ throw new Error(
89
+ `Expected function to have been called with ${JSON.stringify(expectedArgs)}, but it was not.\n` +
90
+ `Actual calls:\n${JSON.stringify(actual?.calls, null, 2)}`
91
+ );
92
+ }
93
+ },
94
+ toBeLessThan(expected) {
95
+ if (actual > expected) {
96
+ throw new Error(`Expected ${actual} to be less than ${expected}`)
97
+ }
98
+ },
99
+ toBeGreaterThan(expected) {
100
+ if (actual < expected) {
101
+ throw new Error(`Expected ${actual} to be greater than ${expected}`)
102
+ }
103
+ },
104
+ toThrow(expectedMessage) {
105
+ let threw = false;
106
+ try {
107
+ actual();
108
+ } catch (err) {
109
+ threw = true;
110
+ if (expectedMessage && !err.message.includes(expectedMessage)) {
111
+ throw new Error(`Expected error message to include "${expectedMessage}", but got "${err.message}"`);
112
+ }
113
+ }
114
+ if (!threw) {
115
+ throw new Error(`Expected function to throw, but it did not`);
116
+ }
117
+ },
118
+ };
119
+
120
+ // 'not' variants
121
+ expectObj.not = {};
122
+ for (const [name, fn] of Object.entries(expectObj)) {
123
+ if (name === 'not') continue;
124
+ if (typeof fn !== 'function') continue;
125
+
126
+ expectObj.not[name] = (...args) => {
127
+ try {
128
+ fn(...args);
129
+ } catch {
130
+ return;
131
+ }
132
+ throw new Error(`Expected NOT: ${name}(${JSON.stringify(args)})`);
133
+ };
134
+ }
135
+ return expectObj;
136
+ };
@@ -0,0 +1,101 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { JSDOM } from 'jsdom';
4
+ import config from '#root/testin-nutin.config.js';
5
+ let currentDom = null;
6
+
7
+ export function setupJsdom(htmlPath = 'dist/src/index.html') {
8
+ const html = fs.readFileSync(path.resolve(process.cwd(), htmlPath), 'utf8');
9
+
10
+ const {
11
+ runScripts,
12
+ resources,
13
+ freezeGlobals,
14
+ pretendToBeVisual,
15
+ } = config.jsdomOptions;
16
+
17
+ const dom = new JSDOM(html, {
18
+ url: 'http://localhost',
19
+ runScripts: runScripts ? 'dangerously' : undefined,
20
+ resources: resources ? 'usable' : undefined,
21
+ pretendToBeVisual,
22
+ });
23
+
24
+ currentDom = dom;
25
+
26
+ global.window = dom.window;
27
+ global.document = dom.window.document;
28
+
29
+ // DOM globals
30
+ Object.getOwnPropertyNames(dom.window)
31
+ .filter(prop => !(prop in global))
32
+ .forEach(prop => {
33
+ global[prop] = dom.window[prop];
34
+ });
35
+
36
+ global.localStorage = dom.window.localStorage;
37
+
38
+ window.matchMedia ??= () => ({
39
+ matches: false,
40
+ addEventListener: () => {},
41
+ removeEventListener: () => {},
42
+ });
43
+
44
+ window.scrollTo ??= () => {};
45
+
46
+ window.requestAnimationFrame ??= (cb) => setTimeout(cb, 0);
47
+ window.cancelAnimationFrame ??= (id) => clearTimeout(id);
48
+
49
+ window.IntersectionObserver ??= class {
50
+ constructor() {}
51
+ observe() {}
52
+ unobserve() {}
53
+ disconnect() {}
54
+ };
55
+
56
+ window.ResizeObserver ??= class {
57
+ constructor() {}
58
+ observe() {}
59
+ unobserve() {}
60
+ disconnect() {}
61
+ };
62
+
63
+ if (freezeGlobals) {
64
+ Object.freeze(global.window);
65
+ Object.freeze(global.document);
66
+ }
67
+
68
+ return dom;
69
+ }
70
+
71
+ export function teardownJsdom() {
72
+ if (currentDom) {
73
+ try {
74
+ currentDom.window.close();
75
+ } catch {}
76
+ }
77
+
78
+ currentDom = null;
79
+
80
+ delete global.window;
81
+ delete global.document;
82
+ delete global.localStorage;
83
+ }
84
+
85
+ export function resetDom() {
86
+ if (!global.document) return;
87
+
88
+ document.body.innerHTML = '';
89
+ document.head.innerHTML = '<meta charset="UTF-8">';
90
+
91
+ if (global.localStorage) {
92
+ localStorage.clear();
93
+ }
94
+ }
95
+
96
+ export function flushPromises() {
97
+ return new Promise(setImmediate);
98
+ }
99
+
100
+ export const $ = (selector) => document.querySelector(selector);
101
+ export const $$ = (selector) => [...document.querySelectorAll(selector)];
@@ -1,4 +1,6 @@
1
1
  import { print, addTest } from '../../index.js';
2
+ import config from '#root/testin-nutin.config.js';
3
+ import { setupJsdom, teardownJsdom, resetDom, flushPromises } from './jsdom-setup.js';
2
4
  import './assertion-lib.js';
3
5
  import './spyon.js';
4
6
 
@@ -14,7 +16,7 @@ export function registerTestGlobals() {
14
16
  afterEach: null,
15
17
  afterAll: null
16
18
  };
17
- print.boldSuccess('📂 ' + name);
19
+ if (config.verbose) print.boldSuccess('📂 ' + name);
18
20
  fn();
19
21
  };
20
22
 
@@ -42,4 +44,16 @@ export function registerTestGlobals() {
42
44
  global.afterEach = (fn) => {
43
45
  currentSuiteHooks.afterEach = fn;
44
46
  };
47
+
48
+ global.setupJsdom = setupJsdom;
49
+ global.teardownJsdom = teardownJsdom;
50
+ global.resetDom = resetDom;
51
+ global.flushPromises = flushPromises;
52
+
53
+ global.$ = (selector) => document.querySelector(selector);
54
+ global.$$ = (selector) => [...document.querySelectorAll(selector)];
55
+
56
+ global.click = (el) => el.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
57
+ global.type = (el, text) => { el.value = text; el.dispatchEvent(new window.InputEvent("input", { bubbles: true })); };
58
+
45
59
  }