@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.
- package/CHANGELOG.md +69 -6
- package/README.md +49 -2
- package/lib/index.mjs +9 -9
- package/lib/src/file-generator.mjs +24 -9
- package/lib/src/package-manager.mjs +72 -51
- package/lib/src/prompts.mjs +10 -10
- package/lib/src/utils.mjs +4 -1
- package/package.json +4 -3
- package/templates/base/.dockerignore.hbs +12 -2
- package/templates/base/README.md.hbs +23 -0
- package/templates/base/builder.config.js.hbs +28 -0
- package/templates/base/src/app/components/common/anchor/anchor.component.ts.hbs +11 -12
- package/templates/base/src/app/dom.test.js.hbs +8 -1
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +2 -1
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +11 -11
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +3 -0
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +3 -0
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +5 -4
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +55 -27
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +24 -17
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +3 -3
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +2 -2
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +25 -10
- package/templates/base/src/core/services/router/router.ts.hbs +1 -0
- package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +4 -3
- package/templates/base/src/core/utils/button-manager/button-manager.utils.test.js.hbs +1 -1
- package/templates/base/src/index.html.hbs +2 -2
- package/templates/base/src/libs/popover/popover.ts.hbs +2 -1
- package/templates/base/src/styles/main.scss.hbs +4 -1
- package/templates/base/tools/builder/builder.js.hbs +19 -26
- package/templates/base/tools/builder/core/copy-static.js.hbs +41 -20
- package/templates/base/tools/builder/core/esbuild.js.hbs +26 -0
- package/templates/base/tools/builder/core/finalize-build.js.hbs +24 -0
- package/templates/base/tools/builder/core/minify-html.js.hbs +60 -0
- package/templates/base/tools/builder/core/paths.js.hbs +19 -0
- package/templates/base/tools/builder/core/sass.js.hbs +22 -0
- package/templates/base/tools/builder/core/tsc.js.hbs +12 -0
- package/templates/base/tools/builder/core/validate-html.js.hbs +32 -12
- package/templates/base/tools/deployment/Dockerfile.hbs +22 -0
- package/templates/base/tools/deployment/gzip.conf.hbs +19 -0
- package/templates/base/tools/deployment/nginx.conf.hbs +78 -0
- package/templates/base/tools/dev/dev-serve.js.hbs +32 -0
- package/templates/base/tools/dev/serve.js.hbs +23 -0
- package/templates/base/tools/{watcher.js.hbs → dev/watcher.js.hbs} +8 -8
- package/templates/base/tools/generator/generator.js.hbs +21 -6
- package/templates/base/tools/generator/handle-file.js.hbs +32 -2
- package/templates/base/tools/generator/templates/index.js.hbs +3 -0
- package/templates/base/tools/utils/export-flags.js.hbs +6 -0
- package/templates/base/tools/utils/get-files-recursive.js.hbs +1 -3
- package/templates/base/tools/utils/index.js.hbs +8 -1
- package/templates/base/tools/utils/prompt-boolean.js.hbs +15 -0
- package/templates/base/tools/utils/run-command.js.hbs +12 -0
- package/templates/base/tools/utils/run-script.js.hbs +22 -0
- package/templates/features/i18n/src/core/config.ts.hbs +3 -0
- package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +2 -0
- package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +10 -1
- package/templates/features/i18n/src/core/services/i18n/languages.ts.hbs +7 -0
- package/templates/features/i18n/tools/builder/core/build-i18n.js.hbs +12 -17
- package/templates/features/i18n/tools/utils/languages.js.hbs +7 -0
- package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +1 -1
- package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -0
- package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
- package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +4 -0
- package/templates/features/template/tools/builder/core/merge-templates.js.hbs +13 -26
- package/templates/features/testinNutin/{test → testin-nutin}/core/index.js.hbs +1 -1
- package/templates/features/testinNutin/testin-nutin/core/src/globals/assertion-lib.js.hbs +136 -0
- package/templates/features/testinNutin/testin-nutin/core/src/globals/jsdom-setup.js.hbs +101 -0
- package/templates/features/testinNutin/{test → testin-nutin}/core/src/globals/register-test-globals.js.hbs +15 -1
- package/templates/features/testinNutin/testin-nutin/core/src/globals/spyon.js.hbs +59 -0
- package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/print.js.hbs +3 -1
- package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/test-discovery.js.hbs +5 -1
- package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/test-queue.js.hbs +3 -1
- package/templates/features/testinNutin/{test → testin-nutin}/core/tests/assertions.test.js.hbs +26 -2
- package/templates/features/testinNutin/testin-nutin/runner.js.hbs +35 -0
- package/templates/features/testinNutin/{test → testin-nutin}/watch-tests.js.hbs +1 -1
- package/templates/features/testinNutin/testin-nutin.config.js.hbs +14 -0
- package/templates/base/Dockerfile.hbs +0 -13
- package/templates/base/tools/builder/core/sass.config.js.hbs +0 -17
- package/templates/base/tools/builder/docs.md.hbs +0 -9
- package/templates/features/testinNutin/test/core/src/globals/assertion-lib.js.hbs +0 -118
- package/templates/features/testinNutin/test/core/src/globals/spyon.js.hbs +0 -22
- package/templates/features/testinNutin/test/core/src/jsdom-setup.js.hbs +0 -12
- package/templates/features/testinNutin/test/docs.md.hbs +0 -39
- package/templates/features/testinNutin/test/runner.js.hbs +0 -36
- /package/templates/features/testinNutin/{test → testin-nutin}/core/src/utils/queue.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/core/tests/globals-each-all.test.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/create-mock-method.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-event-bus.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-http-client.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-i18n.js.hbs +0 -0
- /package/templates/features/testinNutin/{test → testin-nutin}/mocks/mock-router.js.hbs +0 -0
- /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}}
|
|
@@ -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(
|
|
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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_LANGUAGE, LANGUAGES, Language, Translations } from '
|
|
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
|
|
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(
|
|
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
|
-
}
|
|
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.
|
|
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 !==
|
|
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.
|
|
124
|
+
if (isVerbose) print.boldInfo(`Json files for locale ${locale} merged successfully`);
|
|
130
125
|
}
|
|
131
|
-
removeJsonFiles(
|
|
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;
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
|
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$/,
|
|
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
|
|
36
|
-
|
|
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(`
|
|
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,
|
|
32
|
+
jsContent = jsContent.replace(PLACEHOLDER, minifiedHtml);
|
|
48
33
|
await fs.writeFile(jsPath, jsContent);
|
|
49
|
-
|
|
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) => {
|
|
@@ -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
|
}
|