@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,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 {
|
|
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(
|
|
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('
|
|
23
|
+
if (isVerbose) print.info('Copied: favicon.ico');
|
|
31
24
|
} catch {
|
|
32
|
-
print.boldError('⚠️ No favicon found at public/favicon.ico
|
|
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(
|
|
62
|
+
const files = await getFilesRecursive(PATHS.source, extension);
|
|
44
63
|
|
|
45
64
|
for (const file of files) {
|
|
46
|
-
const relative = path.relative(
|
|
47
|
-
const dest = path.join(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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 =
|
|
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
|
-
|
|
35
|
-
|
|
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('\
|
|
43
|
+
print.boldHead('\nWatching for changes...\n');
|
|
44
44
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
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 =
|
|
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}'`);
|