@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.
- package/CHANGELOG.md +32 -2
- package/README.md +47 -1
- package/lib/index.mjs +6 -6
- package/lib/src/file-generator.mjs +7 -5
- package/lib/src/package-manager.mjs +54 -45
- package/lib/src/prompts.mjs +11 -11
- package/lib/src/utils.mjs +2 -1
- package/package.json +1 -1
- package/templates/base/.dockerignore.hbs +10 -1
- package/templates/base/Dockerfile.hbs +28 -8
- package/templates/base/README.md.hbs +23 -0
- package/templates/base/nginx.conf.hbs +43 -0
- 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/token.helper.ts.hbs +3 -2
- 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/view-render-manager.helper.ts.hbs +25 -10
- package/templates/base/src/core/services/router/router.ts.hbs +1 -0
- package/templates/base/src/index.html.hbs +1 -2
- package/templates/base/src/libs/popover/popover.ts.hbs +2 -1
- package/templates/base/src/styles/main.scss.hbs +2 -1
- package/templates/base/tools/builder/builder.js.hbs +17 -27
- package/templates/base/tools/builder/core/copy-static.js.hbs +16 -20
- package/templates/base/tools/builder/core/esbuild.js.hbs +36 -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.config.js.hbs +16 -9
- package/templates/base/tools/builder/core/tsc.js.hbs +8 -0
- package/templates/base/tools/builder/core/validate-html.js.hbs +26 -11
- package/templates/base/tools/dev.js.hbs +43 -0
- package/templates/base/tools/generator/generator.js.hbs +9 -2
- package/templates/base/tools/generator/handle-file.js.hbs +23 -0
- package/templates/base/tools/generator/templates/index.js.hbs +3 -0
- package/templates/base/tools/serve.js.hbs +23 -0
- package/templates/base/tools/utils/export-flags.js.hbs +5 -0
- package/templates/base/tools/utils/get-files-recursive.js.hbs +1 -3
- package/templates/base/tools/utils/index.js.hbs +5 -1
- package/templates/base/tools/utils/prompt-boolean.js.hbs +15 -0
- package/templates/base/tools/utils/run-command.js.hbs +5 -0
- package/templates/base/tools/utils/run-script.js.hbs +22 -0
- package/templates/base/tools/watcher.js.hbs +7 -7
- package/templates/features/i18n/tools/builder/core/build-i18n.js.hbs +11 -16
- package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +1 -1
- package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +3 -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
|
@@ -1,63 +1,91 @@
|
|
|
1
1
|
import { Service } from "../../index.js";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
IEventBus is a type alias for the instance of EventBus, not a true interface
|
|
5
|
-
*/
|
|
6
3
|
export type IEventBus = InstanceType<typeof EventBus>;
|
|
7
4
|
|
|
5
|
+
type Subscription<K extends EventKey = EventKey> = {
|
|
6
|
+
event: K;
|
|
7
|
+
callback: (data: EventMap[K]) => void;
|
|
8
|
+
once?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
8
11
|
export class EventBus extends Service<EventBus> {
|
|
9
|
-
private _subscriptions:
|
|
10
|
-
|
|
12
|
+
private _subscriptions: Subscription[] = [];
|
|
13
|
+
|
|
11
14
|
constructor() {
|
|
12
15
|
super();
|
|
13
16
|
this.registerCleanup(this.cleanupEventListeners);
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
private handlers: {
|
|
17
|
-
[K in EventKey]?: Array<(data: EventMap[K]) => void
|
|
20
|
+
[K in EventKey]?: Array<(data: EventMap[K]) => void>;
|
|
18
21
|
} = {};
|
|
19
|
-
|
|
20
|
-
public subscribe<K extends EventKey>(
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
public subscribe<K extends EventKey>(
|
|
24
|
+
event: K,
|
|
25
|
+
callback: (data: EventMap[K]) => void
|
|
26
|
+
): void {
|
|
27
|
+
this.addHandler(event, callback, false);
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
public once<K extends EventKey>(
|
|
31
|
+
event: K,
|
|
32
|
+
callback: (data: EventMap[K]) => void
|
|
33
|
+
): void {
|
|
34
|
+
this.addHandler(event, callback, true);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private addHandler<K extends EventKey>(
|
|
38
|
+
event: K,
|
|
39
|
+
callback: (data: EventMap[K]) => void,
|
|
40
|
+
once: boolean
|
|
41
|
+
) {
|
|
26
42
|
if (!this.handlers[event]) {
|
|
27
43
|
this.handlers[event] = [];
|
|
28
44
|
}
|
|
29
45
|
this.handlers[event]!.push(callback);
|
|
46
|
+
this._subscriptions.push({ event, callback, once });
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
public emit<K extends EventKey>(event: K, data: EventMap[K]): void;
|
|
33
|
-
public emit<K extends EventKey>(event: K): void;
|
|
34
49
|
public emit<K extends EventKey>(event: K, data?: EventMap[K]): void {
|
|
35
|
-
this._subscriptions.push(event);
|
|
36
50
|
const callbacks = this.handlers[event];
|
|
37
|
-
if (callbacks)
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
if (!callbacks) return;
|
|
52
|
+
|
|
53
|
+
callbacks.slice().forEach(callback => {
|
|
54
|
+
callback(data!);
|
|
55
|
+
|
|
56
|
+
const subIndex = this._subscriptions.findIndex(
|
|
57
|
+
sub => sub.event === event && sub.callback === callback && sub.once
|
|
58
|
+
);
|
|
59
|
+
if (subIndex > -1) {
|
|
60
|
+
this.off(event, callback);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
40
63
|
}
|
|
41
64
|
|
|
42
|
-
public off<K extends EventKey>(
|
|
65
|
+
public off<K extends EventKey>(
|
|
66
|
+
event: K,
|
|
67
|
+
callback?: (data: EventMap[K]) => void
|
|
68
|
+
): void {
|
|
43
69
|
const handlers = this.handlers[event];
|
|
44
70
|
if (!handlers) return;
|
|
45
71
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
if (callback) {
|
|
73
|
+
this.handlers[event] = handlers.filter(h => h !== callback);
|
|
74
|
+
} else {
|
|
75
|
+
delete this.handlers[event];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this._subscriptions = this._subscriptions.filter(
|
|
79
|
+
sub => sub.event !== event || (callback && sub.callback !== callback)
|
|
80
|
+
);
|
|
49
81
|
}
|
|
50
82
|
|
|
51
83
|
private cleanupEventListeners = () => {
|
|
52
|
-
this._subscriptions.forEach(event => {
|
|
53
|
-
|
|
84
|
+
this._subscriptions.forEach(({ event, callback }) => {
|
|
85
|
+
this.off(event, callback);
|
|
54
86
|
});
|
|
55
87
|
this._subscriptions = [];
|
|
56
88
|
};
|
|
57
|
-
|
|
58
|
-
onDestroy<T extends Service<T>>(this: () => T): void {
|
|
59
|
-
|
|
60
|
-
}
|
|
61
89
|
}
|
|
62
90
|
|
|
63
91
|
export const AppEventBus = EventBus.getInstance();
|
|
@@ -2,27 +2,34 @@ import { Service } from "../../index.js";
|
|
|
2
2
|
import { HttpBuilder, HttpMethod, IRequestConfig } from "./helpers/http-builder.helper.js";
|
|
3
3
|
import { HttpManager } from "./helpers/http-manager.helper.js";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* IHttpClient is a type alias for the instance of HttpClient, not a true interface
|
|
7
|
-
*/
|
|
8
5
|
export type IHttpClient = InstanceType<typeof HttpClient>;
|
|
9
6
|
|
|
10
7
|
export class HttpClient extends Service<HttpClient> {
|
|
11
8
|
private _baseUrl: string;
|
|
12
9
|
private _defaultHeaders: Record<string, string>;
|
|
13
10
|
|
|
11
|
+
private _requestInterceptors: Array<(url: string, options: RequestInit) => void> = [];
|
|
12
|
+
private _responseInterceptors: Array<(response: Response) => void> = [];
|
|
13
|
+
|
|
14
14
|
constructor(baseUrl: string = '', defaultHeaders: Record<string, string> = {}) {
|
|
15
15
|
super();
|
|
16
16
|
this._baseUrl = baseUrl;
|
|
17
|
-
this._defaultHeaders = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
this._defaultHeaders = { 'Content-Type': 'application/json', ...defaultHeaders };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public addRequestInterceptor(fn: (url: string, options: RequestInit) => void) {
|
|
21
|
+
this._requestInterceptors.push(fn);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public addResponseInterceptor(fn: (response: Response) => void) {
|
|
25
|
+
this._responseInterceptors.push(fn);
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
onDestroy(): void {
|
|
24
29
|
this._baseUrl = '';
|
|
25
30
|
this._defaultHeaders = {};
|
|
31
|
+
this._requestInterceptors = [];
|
|
32
|
+
this._responseInterceptors = [];
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
private async request<T>(
|
|
@@ -39,8 +46,12 @@ export class HttpClient extends Service<HttpClient> {
|
|
|
39
46
|
const url = HttpBuilder.buildRequestUrl(fullUrl, config?.queryParams);
|
|
40
47
|
const requestOptions = HttpBuilder.buildRequestOptions(method, data, mergedConfig, controller.signal);
|
|
41
48
|
|
|
49
|
+
this._requestInterceptors.forEach(fn => fn(url.toString(), requestOptions));
|
|
50
|
+
|
|
42
51
|
const response = await fetch(url.toString(), requestOptions);
|
|
43
|
-
|
|
52
|
+
|
|
53
|
+
this._responseInterceptors.forEach(fn => fn(response));
|
|
54
|
+
|
|
44
55
|
await HttpManager.validateResponse(response);
|
|
45
56
|
return await HttpManager.parseSuccessResponse<T>(response);
|
|
46
57
|
} catch (error) {
|
|
@@ -50,23 +61,19 @@ export class HttpClient extends Service<HttpClient> {
|
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
public
|
|
64
|
+
public get<T = unknown>(endpoint: string, config?: IRequestConfig) {
|
|
54
65
|
return this.request<T>('GET', endpoint, null, config);
|
|
55
66
|
}
|
|
56
|
-
|
|
57
|
-
public async post<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig): Promise<T> {
|
|
67
|
+
public post<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig) {
|
|
58
68
|
return this.request<T>('POST', endpoint, data, config);
|
|
59
69
|
}
|
|
60
|
-
|
|
61
|
-
public async put<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig): Promise<T> {
|
|
70
|
+
public put<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig) {
|
|
62
71
|
return this.request<T>('PUT', endpoint, data, config);
|
|
63
72
|
}
|
|
64
|
-
|
|
65
|
-
public async patch<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig): Promise<T> {
|
|
73
|
+
public patch<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig) {
|
|
66
74
|
return this.request<T>('PATCH', endpoint, data, config);
|
|
67
75
|
}
|
|
68
|
-
|
|
69
|
-
public async delete<T = unknown>(endpoint: string, config?: IRequestConfig): Promise<T> {
|
|
76
|
+
public delete<T = unknown>(endpoint: string, config?: IRequestConfig) {
|
|
70
77
|
return this.request<T>('DELETE', endpoint, undefined, config);
|
|
71
78
|
}
|
|
72
79
|
}
|
|
@@ -56,25 +56,40 @@ export class ViewRenderManager {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
static cleanupOptionalContent() {
|
|
59
|
-
const isEmpty = (
|
|
60
|
-
|
|
61
|
-
return false;
|
|
62
|
-
};
|
|
59
|
+
const isEmpty = (el: HTMLElement): boolean => {
|
|
60
|
+
const attrName = el.dataset.optional?.trim();
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
if (attrName && attrName !== "") {
|
|
63
|
+
const attrValue = el.getAttribute(attrName);
|
|
64
|
+
return !attrValue || attrValue.trim() === "" || attrValue === "undefined";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (el instanceof HTMLImageElement) {
|
|
68
|
+
return !el.src || el.src.trim() === "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
72
|
+
return !el.value?.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (el instanceof HTMLMediaElement || el instanceof HTMLSourceElement) {
|
|
76
|
+
return !el.getAttribute("src");
|
|
69
77
|
}
|
|
78
|
+
|
|
79
|
+
const content = el.textContent?.trim();
|
|
80
|
+
return !content || content === "undefined";
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
document.querySelectorAll<HTMLElement>("[data-optional]").forEach(el => {
|
|
84
|
+
if (isEmpty(el)) el.remove();
|
|
70
85
|
});
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
static emitEvent(event: EventKey, viewName: string): void {
|
|
74
89
|
AppEventBus.emit(event, viewName);
|
|
75
90
|
}
|
|
76
|
-
|
|
77
91
|
{{#if transition}}
|
|
92
|
+
|
|
78
93
|
private static delay(ms: number): Promise<void> {
|
|
79
94
|
return new Promise(res => setTimeout(res, ms));
|
|
80
95
|
}
|
|
@@ -5,6 +5,7 @@ import { NavigationManager } from './helpers/navigation-manager.helper.js';
|
|
|
5
5
|
|
|
6
6
|
// centralized export
|
|
7
7
|
export { Routes, RouteGuard } from './helpers/route-guard-manager.helper.js';
|
|
8
|
+
export { ViewRenderManager } from './helpers/view-render-manager.helper.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* IRouter is a type alias for the instance of Router, not a true interface
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { View, ButtonManager, BaseButton } from '../../core/index.js';
|
|
1
|
+
import { View, ButtonManager, BaseButton, ViewRenderManager } from '../../core/index.js';
|
|
2
2
|
|
|
3
3
|
export interface PopoverButton extends BaseButton {
|
|
4
4
|
// if needed for Popover-specific properties
|
|
@@ -59,6 +59,7 @@ export class PopoverView extends View {
|
|
|
59
59
|
private createContent(): HTMLElement {
|
|
60
60
|
const content = super.render();
|
|
61
61
|
content.classList.add('popover-content');
|
|
62
|
+
ViewRenderManager.cleanupOptionalContent();
|
|
62
63
|
return content;
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -1,37 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { spawnSync } from 'child_process';
|
|
6
|
-
import process from 'process';
|
|
7
|
-
import { print } from '../utils/index.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { print, isProd, runScript } from '../utils/index.js';
|
|
8
5
|
|
|
9
|
-
const
|
|
10
|
-
const __dirname = dirname(__filename);
|
|
6
|
+
const scriptsDir = path.resolve(process.cwd(),'tools', 'builder', 'core');
|
|
11
7
|
|
|
12
|
-
|
|
8
|
+
print.boldHead(`Starting build...`);
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
runScript(path.join(scriptsDir, 'copy-static.js'), 'Copying files...');
|
|
11
|
+
if(!isProd) runScript(path.join(scriptsDir, 'tsc.js'), 'Running TypeScript compiler...');
|
|
16
12
|
|
|
17
|
-
function runScript(path, message) {
|
|
18
|
-
print.boldSection(`\n${message}`);
|
|
19
|
-
const result = spawnSync('node', [resolve(__dirname, path)], { stdio: 'inherit' });
|
|
20
|
-
|
|
21
|
-
if (result.status !== 0) {
|
|
22
|
-
print.boldError(`\nScript ${path} failed.`);
|
|
23
|
-
process.exit(result.status ?? 1);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
runScript('./core/copy-static.js', 'Copying files...');
|
|
28
|
-
{{#if i18n}}
|
|
29
|
-
runScript('./core/build-i18n.js', 'Combining locales for production...');
|
|
30
|
-
{{/if}}
|
|
31
13
|
{{#if template}}
|
|
32
|
-
runScript('
|
|
14
|
+
runScript(path.join(scriptsDir, 'merge-templates.js'), 'Merging HTML templates in JavaScript files...');
|
|
15
|
+
{{else}}
|
|
16
|
+
runScript(path.join(scriptsDir, 'minify-html.js'), 'Minifying inline templates...');
|
|
33
17
|
{{/if}}
|
|
34
|
-
|
|
35
|
-
runScript('
|
|
18
|
+
|
|
19
|
+
runScript(path.join(scriptsDir, 'build-i18n.js'), 'Combining locales for production...');
|
|
20
|
+
runScript(path.join(scriptsDir, 'sass.config.js'), 'Compiling styles from main.scss...');
|
|
21
|
+
runScript(path.join(scriptsDir, 'validate-html.js'), 'Validating tags in index.html...');
|
|
22
|
+
if (isProd) runScript(path.join(scriptsDir, 'esbuild.js'), 'Running esbuild...');
|
|
23
|
+
runScript(path.join(scriptsDir, 'finalize-build.js'), 'Finalizing build...')
|
|
36
24
|
|
|
37
25
|
print.boldSuccess(`\nBuild successful!\n`);
|
|
26
|
+
|
|
27
|
+
if (!isProd) print.boldInfo('For production, use build:prod.');
|
|
@@ -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,36 @@ 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('✅ Copied: favicon.ico');
|
|
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
|
|
|
36
29
|
async function copyStatic() {
|
|
37
30
|
const extensions = [...BINARY_EXTENSIONS, 'html'];
|
|
31
|
+
if (isProd) extensions.push('.ts');
|
|
32
|
+
|
|
33
|
+
print.info(`File types that will be copied: ${Array.from(extensions)}`);
|
|
38
34
|
|
|
39
|
-
await ensureDir(
|
|
35
|
+
await ensureDir(PATHS.tempSource);
|
|
40
36
|
await copyFavicon();
|
|
41
37
|
|
|
42
38
|
for (const extension of extensions) {
|
|
43
|
-
const files = await getFilesRecursive(
|
|
39
|
+
const files = await getFilesRecursive(PATHS.source, extension);
|
|
44
40
|
|
|
45
41
|
for (const file of files) {
|
|
46
|
-
const relative = path.relative(
|
|
47
|
-
const dest = path.join(
|
|
42
|
+
const relative = path.relative(PATHS.source, file);
|
|
43
|
+
const dest = path.join(PATHS.tempSource, relative);
|
|
48
44
|
|
|
49
45
|
try {
|
|
50
46
|
await copyFile(file, dest);
|
|
51
|
-
print.info(`✅ Copied: ${file} → ${dest}`);
|
|
47
|
+
if (isVerbose) print.info(`✅ Copied: ${file} → ${dest}`);
|
|
52
48
|
} catch (err) {
|
|
53
49
|
print.error(`❌ Failed to copy: ${file}. ${err.message}`);
|
|
54
50
|
exit(1);
|
|
@@ -56,7 +52,7 @@ async function copyStatic() {
|
|
|
56
52
|
}
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
await copyJsonFiles(
|
|
55
|
+
if (isProd) await copyJsonFiles(PATHS.source, PATHS.tempSource);
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
async function copyJsonFiles(sourceDir, destDir) {
|
|
@@ -72,7 +68,7 @@ async function copyJsonFiles(sourceDir, destDir) {
|
|
|
72
68
|
} else if (item.isFile() && path.extname(item.name) === '.json') {
|
|
73
69
|
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
74
70
|
await fs.copyFile(sourcePath, destPath);
|
|
75
|
-
|
|
71
|
+
if (isVerbose) print.info(`Copied JSON: ${sourcePath} -> ${destPath}`);
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
74
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import esbuild from 'esbuild';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { exit } from 'process';
|
|
4
|
+
import { print } from '../../utils/index.js';
|
|
5
|
+
import { PATHS } from './paths.js';
|
|
6
|
+
|
|
7
|
+
const ENTRY_FILE = path.join(PATHS.tempApp, 'main.ts');
|
|
8
|
+
const OUT_FILE = path.join(PATHS.tempSource, 'bundle.js');
|
|
9
|
+
|
|
10
|
+
async function build() {
|
|
11
|
+
await esbuild.build({
|
|
12
|
+
entryPoints: [ENTRY_FILE],
|
|
13
|
+
outfile: OUT_FILE,
|
|
14
|
+
bundle: true,
|
|
15
|
+
minify: true,
|
|
16
|
+
keepNames: true, // View names are used to update meta.title
|
|
17
|
+
sourcemap: false,
|
|
18
|
+
platform: 'browser',
|
|
19
|
+
format: 'esm',
|
|
20
|
+
target: ['es2015'],
|
|
21
|
+
legalComments: 'none',
|
|
22
|
+
loader: {
|
|
23
|
+
'.json': 'json',
|
|
24
|
+
},
|
|
25
|
+
define: {
|
|
26
|
+
'process.env.NODE_ENV': '"production"',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
print.info(`ESBuild complete.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
build().catch((err) => {
|
|
34
|
+
print.boldError(`ESBuild failed: ${err.message}`);
|
|
35
|
+
exit(1);
|
|
36
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { print, isProd } 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
|
+
print.info('Build finalized.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
finalizeBuild();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { minify } from 'html-minifier-terser';
|
|
2
|
+
{{#unless template}}
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { exit } from 'process';
|
|
6
|
+
import { getFilesRecursive, print, isProd, isVerbose } from '../../utils/index.js';
|
|
7
|
+
import { PATHS } from './paths.js';
|
|
8
|
+
{{/unless}}
|
|
9
|
+
|
|
10
|
+
{{#if template}}export {{/if}}async function minifyHTML(html) {
|
|
11
|
+
return minify(html, {
|
|
12
|
+
collapseWhitespace: true,
|
|
13
|
+
removeComments: true,
|
|
14
|
+
removeRedundantAttributes: true,
|
|
15
|
+
removeScriptTypeAttributes: true,
|
|
16
|
+
removeStyleLinkTypeAttributes: true,
|
|
17
|
+
useShortDoctype: true,
|
|
18
|
+
keepClosingSlash: true,
|
|
19
|
+
minifyCSS: true,
|
|
20
|
+
minifyJS: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
{{#unless template}}
|
|
25
|
+
const TEMPLATE_REGEX =
|
|
26
|
+
/const\s+(template|templateFn)\s*=?\s*(?:\(.*?\)\s*=>\s*)?`([\s\S]*?)`/gm;
|
|
27
|
+
|
|
28
|
+
async function minifyInlineHTML() {
|
|
29
|
+
const extension = isProd ? '.ts' : '.js';
|
|
30
|
+
const jsFiles = getFilesRecursive(PATHS.tempApp, extension);
|
|
31
|
+
|
|
32
|
+
for (const file of jsFiles) {
|
|
33
|
+
let code = fs.readFileSync(file, 'utf8');
|
|
34
|
+
let hasChanges = false;
|
|
35
|
+
|
|
36
|
+
const matches = [...code.matchAll(TEMPLATE_REGEX)];
|
|
37
|
+
|
|
38
|
+
if (matches.length) {
|
|
39
|
+
for (const match of matches) {
|
|
40
|
+
const [full, , html] = match;
|
|
41
|
+
const minified = await minifyHTML(html);
|
|
42
|
+
const minifiedBlock = full.replace(html, minified);
|
|
43
|
+
code = code.replace(full, minifiedBlock);
|
|
44
|
+
hasChanges = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (hasChanges) {
|
|
49
|
+
fs.writeFileSync(file, code, 'utf8');
|
|
50
|
+
if (isVerbose) print.info(`Minified inline HTML in: ${file}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
print.info('Inline templates minified.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
minifyInlineHTML().catch((err) => {
|
|
57
|
+
print.boldError(`Unexpected error: ${err.message}`);
|
|
58
|
+
exit(1);
|
|
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
|
+
};
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import * as sass from 'sass';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
3
4
|
import { print } from '../../utils/index.js';
|
|
4
|
-
{
|
|
5
|
-
const stylesOutput = 'dist-build/src'
|
|
6
|
-
{{else}}
|
|
7
|
-
const stylesOutput = 'dist/src';
|
|
8
|
-
{{/if}}
|
|
9
|
-
if (!fs.existsSync(stylesOutput)) fs.mkdirSync(stylesOutput);
|
|
5
|
+
import { PATHS } from './paths.js';
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const stylesInput = path.join(PATHS.source, 'styles');
|
|
8
|
+
const stylesOutput = path.join(PATHS.tempSource, 'main.css');
|
|
9
|
+
|
|
10
|
+
const scssPath = (origin) => path.join(stylesInput, origin);
|
|
11
|
+
const scssOrigins = [
|
|
12
|
+
'base',
|
|
13
|
+
'core'{{#if stylinNutin}},
|
|
14
|
+
'components'{{/if}}
|
|
15
|
+
];
|
|
16
|
+
const pathsToLoad = scssOrigins.map(origin => scssPath(origin));
|
|
17
|
+
|
|
18
|
+
sass.compileAsync(path.join(stylesInput, 'main.scss'), {
|
|
19
|
+
loadPaths: [ ...pathsToLoad ],
|
|
13
20
|
style: 'compressed'
|
|
14
21
|
}).then(result => {
|
|
22
|
+
fs.writeFileSync(stylesOutput, result.css);
|
|
15
23
|
print.info('Global styles compiled.');
|
|
16
|
-
fs.writeFileSync(`${stylesOutput}/main.css`, result.css);
|
|
17
24
|
});
|