@simplysm/sd-cli 7.0.224 → 7.0.231

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.
@@ -1,736 +1,737 @@
1
- import { INpmConfig, ISdCliClientPackageConfig, ISdCliPackageBuildResult, ITsconfig } from "../commons";
2
- import { EventEmitter } from "events";
3
- import { FsUtil, Logger, PathUtil } from "@simplysm/sd-core-node";
4
- import webpack from "webpack";
5
- import path from "path";
6
- import ts from "typescript";
7
- import { SdCliBuildResultUtil } from "../utils/SdCliBuildResultUtil";
8
- import { NamedChunksPlugin } from "@angular-devkit/build-angular/src/webpack/plugins/named-chunks-plugin";
9
- import {
10
- DedupeModuleResolvePlugin,
11
- JavaScriptOptimizerPlugin,
12
- SuppressExtractedTextChunksWebpackPlugin
13
- } from "@angular-devkit/build-angular/src/webpack/plugins";
14
- import CopyWebpackPlugin from "copy-webpack-plugin";
15
- import MiniCssExtractPlugin from "mini-css-extract-plugin";
16
- import { AngularWebpackPlugin } from "@ngtools/webpack";
17
- import { IndexHtmlWebpackPlugin } from "@angular-devkit/build-angular/src/webpack/plugins/index-html-webpack-plugin";
18
- import { SassWorkerImplementation } from "@angular-devkit/build-angular/src/sass/sass-service";
19
- import { LicenseWebpackPlugin } from "license-webpack-plugin";
20
- import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
21
- import { createHash } from "crypto";
22
- import ESLintWebpackPlugin from "eslint-webpack-plugin";
23
- import os from "os";
24
- import { ESLint } from "eslint";
25
- import { TransferSizePlugin } from "@angular-devkit/build-angular/src/webpack/plugins/transfer-size-plugin";
26
- import { CssOptimizerPlugin } from "@angular-devkit/build-angular/src/webpack/plugins/css-optimizer-plugin";
27
- import browserslist from "browserslist";
28
- import { augmentAppWithServiceWorker } from "@angular-devkit/build-angular/src/utils/service-worker";
29
- import { SdCliNgModuleGenerator } from "../ng-tools/SdCliNgModuleGenerator";
30
- import { SdCliCordova } from "../build-tool/SdCliCordova";
31
- import { SdCliNpmConfigUtil } from "../utils/SdCliNpmConfigUtil";
32
- import electronBuilder from "electron-builder";
33
- import LintResult = ESLint.LintResult;
34
-
35
- export class SdCliClientBuilder extends EventEmitter {
36
- private readonly _logger: Logger;
37
-
38
- private readonly _tsconfigFilePath: string;
39
- private readonly _parsedTsconfig: ts.ParsedCommandLine;
40
- private readonly _npmConfigMap = new Map<string, INpmConfig>();
41
- private readonly _ngModuleGenerator: SdCliNgModuleGenerator;
42
-
43
- private readonly _cordova?: SdCliCordova;
44
-
45
- private readonly _hasAngularRoute: boolean;
46
-
47
- public constructor(private readonly _rootPath: string,
48
- private readonly _config: ISdCliClientPackageConfig,
49
- private readonly _workspaceRootPath: string) {
50
- super();
51
-
52
- const npmConfig = this._getNpmConfig(this._rootPath)!;
53
- this._logger = Logger.get(["simplysm", "sd-cli", this.constructor.name, npmConfig.name]);
54
-
55
- // tsconfig
56
- this._tsconfigFilePath = path.resolve(this._rootPath, "tsconfig-build.json");
57
- const tsconfig = FsUtil.readJson(this._tsconfigFilePath) as ITsconfig;
58
- this._parsedTsconfig = ts.parseJsonConfigFileContent(tsconfig, ts.sys, this._rootPath, tsconfig.angularCompilerOptions);
59
-
60
- // isAngular
61
- this._hasAngularRoute = SdCliNpmConfigUtil.getDependencies(npmConfig).defaults.includes("@angular/router");
62
-
63
- // NgModule 생성기 초기화
64
- this._ngModuleGenerator = new SdCliNgModuleGenerator(this._rootPath, [
65
- "controls",
66
- "directives",
67
- "guards",
68
- "modals",
69
- "providers",
70
- "app",
71
- "pages",
72
- "print-templates",
73
- "toasts",
74
- "AppPage"
75
- ], this._hasAngularRoute ? {
76
- glob: "**/*Page.ts",
77
- fileEndsWith: "Page",
78
- rootClassName: "AppPage"
79
- } : undefined);
80
-
81
- // CORDOVA
82
- if (this._config.builder?.cordova) {
83
- this._cordova = new SdCliCordova(this._rootPath, this._config.builder.cordova);
84
- }
85
- }
86
-
87
- public override on(event: "change", listener: () => void): this;
88
- public override on(event: "complete", listener: (results: ISdCliPackageBuildResult[]) => void): this;
89
- public override on(event: string | symbol, listener: (...args: any[]) => void): this {
90
- return super.on(event, listener);
91
- }
92
-
93
- public async watchAsync(): Promise<void> {
94
- // DIST 비우기
95
- await FsUtil.removeAsync(path.resolve(this._rootPath, ".electron"));
96
- await FsUtil.removeAsync(this._parsedTsconfig.options.outDir!);
97
-
98
- // NgModule 생성
99
- await this._ngModuleGenerator.runAsync();
100
-
101
- // CORDOVA 초기화
102
- if (this._cordova) {
103
- this._logger.debug("CORDOVA 구성...");
104
- await this._cordova.initializeAsync();
105
- }
106
-
107
- // 빌드 준비
108
- const webpackConfigs = (Object.keys(this._config.builder ?? { web: {} }) as ("web" | "cordova" | "electron")[])
109
- .map((builderType) => this._getWebpackConfig(true, builderType));
110
- const multiCompiler = webpack(webpackConfigs);
111
- await new Promise<void>((resolve, reject) => {
112
- multiCompiler.hooks.invalid.tap(this.constructor.name, (fileName) => {
113
- if (fileName != null) {
114
- this._logger.debug("파일변경 감지", fileName);
115
- // NgModule 캐시 삭제
116
- this._ngModuleGenerator.removeCaches([path.resolve(fileName)]);
117
- }
118
- });
119
-
120
- multiCompiler.hooks.watchRun.tapAsync(this.constructor.name, async (args, callback) => {
121
- this.emit("change");
122
-
123
- // NgModule 생성
124
- await this._ngModuleGenerator.runAsync();
125
-
126
- callback();
127
-
128
- this._logger.debug("Webpack 빌드 수행...");
129
- });
130
-
131
- multiCompiler.watch({}, async (err, multiStats) => {
132
- if (err != null || multiStats == null) {
133
- this.emit("complete", [{
134
- filePath: undefined,
135
- line: undefined,
136
- char: undefined,
137
- code: undefined,
138
- severity: "error",
139
- message: err?.stack ?? "알 수 없는 오류 (multiStats=null)"
140
- }]);
141
- reject(err);
142
- return;
143
- }
144
-
145
- // 결과 반환
146
- const results = multiStats.stats.mapMany((stats) => SdCliBuildResultUtil.convertFromWebpackStats(stats));
147
-
148
- // .config.json 파일 쓰기
149
- const npmConfig = this._getNpmConfig(this._rootPath)!;
150
- const packageKey = npmConfig.name.split("/").last()!;
151
-
152
- const configDistPath = typeof this._config.server === "string"
153
- ? path.resolve(this._workspaceRootPath, "packages", this._config.server, "dist/www", packageKey, ".config.json")
154
- : path.resolve(this._parsedTsconfig.options.outDir!, ".config.json");
155
- await FsUtil.writeFileAsync(configDistPath, JSON.stringify(this._config.configs ?? {}, undefined, 2));
156
-
157
- // 마무리
158
- this._logger.debug("Webpack 빌드 완료");
159
- resolve();
160
-
161
- this.emit("complete", results);
162
- });
163
- });
164
- }
165
-
166
- public async buildAsync(): Promise<ISdCliPackageBuildResult[]> {
167
- // DIST 비우기
168
- await FsUtil.removeAsync(path.resolve(this._rootPath, ".electron"));
169
- await FsUtil.removeAsync(this._parsedTsconfig.options.outDir!);
170
-
171
- // NgModule 생성
172
- await this._ngModuleGenerator.runAsync();
173
-
174
- // CORDOVA 초기화
175
- if (this._cordova) {
176
- this._logger.debug("CORDOVA 구성...");
177
- await this._cordova.initializeAsync();
178
- }
179
-
180
- // 빌드
181
- this._logger.debug("Webpack 빌드 수행...");
182
- const builderTypes = (Object.keys(this._config.builder ?? { web: {} }) as ("web" | "cordova" | "electron")[]);
183
- const webpackConfigs = builderTypes.map((builderType) => this._getWebpackConfig(false, builderType));
184
- const multipleCompiler = webpack(webpackConfigs);
185
- const buildResults = await new Promise<ISdCliPackageBuildResult[]>((resolve, reject) => {
186
- multipleCompiler.run((err, multiStats) => {
187
- if (err != null || multiStats == null) {
188
- reject(err);
189
- return;
190
- }
191
-
192
- // 결과 반환
193
- const results = multiStats.stats.mapMany((stats) => SdCliBuildResultUtil.convertFromWebpackStats(stats));
194
- resolve(results);
195
- });
196
- });
197
-
198
- // .config.json 파일 쓰기
199
- const targetPath = path.resolve(this._parsedTsconfig.options.outDir!, ".config.json");
200
- await FsUtil.writeFileAsync(targetPath, JSON.stringify(this._config.configs ?? {}, undefined, 2));
201
-
202
- // service-worker 처리
203
- if (builderTypes.includes("web") && FsUtil.exists(path.resolve(this._rootPath, "ngsw-config.json"))) {
204
- const packageKey = this._getNpmConfig(this._rootPath)!.name.split("/").last()!;
205
- await augmentAppWithServiceWorker(
206
- PathUtil.posix(path.relative(this._workspaceRootPath, this._rootPath)) as any,
207
- PathUtil.posix(path.relative(this._workspaceRootPath, path.resolve(this._parsedTsconfig.options.outDir!))) as any,
208
- `/${packageKey}/`,
209
- PathUtil.posix(path.relative(this._workspaceRootPath, path.resolve(this._rootPath, "ngsw-config.json")))
210
- );
211
- }
212
-
213
- // CORDOVA 빌드
214
- if (this._cordova) {
215
- this._logger.debug("CORDOVA 빌드...");
216
- await this._cordova.buildAsync(path.resolve(this._parsedTsconfig.options.outDir!, "cordova"));
217
- }
218
-
219
- // ELECTRON
220
- if (this._config.builder?.electron) {
221
- const npmConfig = this._getNpmConfig(this._rootPath)!;
222
-
223
- const electronVersion = npmConfig.dependencies?.["electron"];
224
- if (electronVersion === undefined) {
225
- throw new Error("ELECTRON 빌드 패키지의 'dependencies'에는 'electron'이 반드시 포함되어야 합니다.");
226
- }
227
-
228
- const dotenvVersion = npmConfig.dependencies?.["dotenv"];
229
- if (dotenvVersion === undefined) {
230
- throw new Error("ELECTRON 빌드 패키지의 'dependencies'에는 'dotenv'가 반드시 포함되어야 합니다.");
231
- }
232
-
233
- const remoteVersion = npmConfig.dependencies?.["@electron/remote"];
234
-
235
- const electronSrcPath = path.resolve(this._rootPath, `.electron/src`);
236
- const electronDistPath = path.resolve(this._rootPath, `.electron/dist`);
237
-
238
- await FsUtil.writeJsonAsync(path.resolve(electronSrcPath, `package.json`), {
239
- name: npmConfig.name,
240
- version: npmConfig.version,
241
- description: npmConfig.description,
242
- main: "electron.js",
243
- author: npmConfig.author,
244
- license: npmConfig.license,
245
- devDependencies: {
246
- "electron": electronVersion.replace("^", "")
247
- },
248
- dependencies: {
249
- "dotenv": dotenvVersion,
250
- ...remoteVersion !== undefined ? {
251
- "@electron/remote": remoteVersion
252
- } : {}
253
- }
254
- });
255
-
256
- await FsUtil.writeFileAsync(path.resolve(electronSrcPath, `.env`), [
257
- "NODE_ENV=production",
258
- `SD_VERSION=${npmConfig.version}`,
259
- (this._config.builder.electron.icon !== undefined) ? `SD_ELECTRON_ICON=${this._config.builder.electron.icon}` : `SD_ELECTRON_ICON=favicon.ico`,
260
- ...(this._config.env !== undefined) ? Object.keys(this._config.env).map((key) => `${key}=${this._config.env![key]}`) : []
261
- ].filterExists().join("\n"));
262
-
263
- let electronTsFileContent = await FsUtil.readFileAsync(path.resolve(this._rootPath, `src/electron.ts`));
264
- electronTsFileContent = "require(\"dotenv\").config({ path: `${__dirname}\\\\.env` });\n" + electronTsFileContent;
265
- const result = ts.transpileModule(electronTsFileContent, { compilerOptions: { module: ts.ModuleKind.CommonJS } });
266
- await FsUtil.writeFileAsync(path.resolve(electronSrcPath, "electron.js"), result.outputText);
267
-
268
- await electronBuilder.build({
269
- targets: electronBuilder.Platform.WINDOWS.createTarget(),
270
- config: {
271
- appId: this._config.builder.electron.appId,
272
- productName: npmConfig.description,
273
- // asar: false,
274
- win: {
275
- target: "nsis"
276
- },
277
- nsis: {},
278
- directories: {
279
- app: electronSrcPath,
280
- output: electronDistPath
281
- },
282
- ...this._config.builder.electron.installerIcon !== undefined ? {
283
- icon: path.resolve(this._rootPath, "src", this._config.builder.electron.installerIcon)
284
- } : {}
285
- }
286
- });
287
-
288
- await FsUtil.copyAsync(
289
- path.resolve(this._rootPath, `.electron/dist/${npmConfig.description} Setup ${npmConfig.version}.exe`),
290
- path.resolve(this._parsedTsconfig.options.outDir!, `electron/${npmConfig.description}-latest.exe`)
291
- );
292
-
293
- await FsUtil.copyAsync(
294
- path.resolve(this._rootPath, `.electron/dist/${npmConfig.description} Setup ${npmConfig.version}.exe`),
295
- path.resolve(this._parsedTsconfig.options.outDir!, `electron/updates/${npmConfig.version}.exe`)
296
- );
297
- }
298
-
299
- // 마무리
300
- this._logger.debug("Webpack 빌드 완료");
301
- return buildResults;
302
- }
303
-
304
- private _getInternalModuleCachePaths(workspaceName: string): string[] {
305
- return [
306
- ...FsUtil.findAllParentChildDirPaths("node_modules/*/package.json", this._rootPath, this._workspaceRootPath),
307
- ...FsUtil.findAllParentChildDirPaths(`node_modules/!(@simplysm|@${workspaceName})/*/package.json`, this._rootPath, this._workspaceRootPath),
308
- ].map((p) => path.dirname(p));
309
- }
310
-
311
- private _getWebpackConfig(watch: boolean, builderType: "web" | "cordova" | "electron"): webpack.Configuration {
312
- const workspaceNpmConfig = this._getNpmConfig(this._workspaceRootPath)!;
313
- const workspaceName = workspaceNpmConfig.name;
314
-
315
- const internalModuleCachePaths = watch ? this._getInternalModuleCachePaths(workspaceName) : undefined;
316
-
317
- const npmConfig = this._getNpmConfig(this._rootPath)!;
318
-
319
- const workspacePkgLockContent = FsUtil.readFile(path.resolve(this._workspaceRootPath, "package-lock.json"));
320
-
321
- const pkgKey = npmConfig.name.split("/").last()!;
322
- const publicPath = builderType === "web" ? `/${pkgKey}/` : watch ? `/${pkgKey}/${builderType}/` : ``;
323
-
324
- const cacheBasePath = path.resolve(this._rootPath, ".cache");
325
-
326
- const distPath = (builderType === "cordova" && !watch) ? path.resolve(this._cordova!.cordovaPath, "www")
327
- : (builderType === "electron" && !watch) ? path.resolve(this._rootPath, ".electron/src")
328
- : builderType === "web" ? this._parsedTsconfig.options.outDir
329
- : `${this._parsedTsconfig.options.outDir}/${builderType}`;
330
-
331
- const sassImplementation = new SassWorkerImplementation();
332
-
333
- const mainFilePath = path.resolve(this._rootPath, "src/main.ts");
334
- const polyfillsFilePath = path.resolve(this._rootPath, "src/polyfills.ts");
335
- const stylesFilePath = path.resolve(this._rootPath, "src/styles.scss");
336
-
337
- let prevProgressMessage = "";
338
- return {
339
- mode: watch ? "development" : "production",
340
- devtool: false,
341
- target: builderType === "electron" ? ["electron-renderer", "es2015"] : ["web", "es2015"],
342
- profile: false,
343
- resolve: {
344
- roots: [this._rootPath],
345
- extensions: [".ts", ".tsx", ".mjs", ".cjs", ".js", ".jsx"],
346
- symlinks: true,
347
- modules: [this._workspaceRootPath, "node_modules"],
348
- mainFields: ["es2015", "browser", "module", "main"],
349
- conditionNames: ["es2015", "..."],
350
- },
351
- resolveLoader: {
352
- symlinks: true
353
- },
354
- context: this._workspaceRootPath,
355
- entry: {
356
- main: [mainFilePath],
357
- ...FsUtil.exists(polyfillsFilePath) ? { polyfills: [polyfillsFilePath] } : {},
358
- ...FsUtil.exists(stylesFilePath) ? { styles: [stylesFilePath] } : {}
359
- },
360
- output: {
361
- uniqueName: pkgKey,
362
- hashFunction: "xxhash64",
363
- clean: true,
364
- path: distPath,
365
- publicPath,
366
- filename: "[name].js",
367
- chunkFilename: "[name].js",
368
- libraryTarget: undefined,
369
- crossOriginLoading: false,
370
- trustedTypes: "angular#bundler",
371
- scriptType: "module"
372
- },
373
- watch: false,
374
- watchOptions: {
375
- poll: undefined,
376
- ignored: undefined
377
- },
378
- performance: { hints: false },
379
- ignoreWarnings: [
380
- /Failed to parse source map from/,
381
- /Add postcss as project dependency/,
382
- /"@charset" must be the first rule in the file/
383
- ],
384
- experiments: { backCompat: false, syncWebAssembly: true, asyncWebAssembly: true },
385
- infrastructureLogging: { level: "error" },
386
- stats: "errors-warnings",
387
- cache: {
388
- type: "filesystem",
389
- profile: watch ? undefined : false,
390
- cacheDirectory: path.resolve(cacheBasePath, "angular-webpack"),
391
- maxMemoryGenerations: 1,
392
- name: createHash("sha1")
393
- .update(workspacePkgLockContent)
394
- .update(JSON.stringify(this._parsedTsconfig.options))
395
- .update(JSON.stringify(this._config))
396
- .update(watch.toString())
397
- .digest("hex")
398
- },
399
- ...watch ? {
400
- snapshot: {
401
- immutablePaths: internalModuleCachePaths,
402
- managedPaths: internalModuleCachePaths
403
- }
404
- } : {},
405
- node: false,
406
- optimization: {
407
- minimizer: watch ? [] : [
408
- new JavaScriptOptimizerPlugin({
409
- define: {
410
- ngDevMode: false,
411
- ngI18nClosureMode: false,
412
- ngJitMode: false
413
- },
414
- sourcemap: false,
415
- target: ts.ScriptTarget.ES2017,
416
- keepIdentifierNames: true,
417
- keepNames: true,
418
- removeLicenses: true,
419
- advanced: true
420
- }),
421
- new TransferSizePlugin(),
422
- new CssOptimizerPlugin({
423
- supportedBrowsers: browserslist([
424
- "last 1 Chrome versions",
425
- "last 2 Edge major versions"
426
- ], { path: this._workspaceRootPath })
427
- })
428
- ] as any[],
429
- moduleIds: "deterministic",
430
- chunkIds: watch ? "named" : "deterministic",
431
- emitOnErrors: watch,
432
- runtimeChunk: "single",
433
- splitChunks: {
434
- maxAsyncRequests: Infinity,
435
- cacheGroups: {
436
- default: {
437
- chunks: "async",
438
- minChunks: 2,
439
- priority: 10
440
- },
441
- common: {
442
- name: "common",
443
- chunks: "async",
444
- minChunks: 2,
445
- enforce: true,
446
- priority: 5
447
- },
448
- vendors: false,
449
- defaultVendors: watch ? {
450
- name: "vendor",
451
- chunks: (chunk) => chunk.name === "main",
452
- enforce: true,
453
- test: /[\\/]node_modules[\\/]/
454
- } : false
455
- }
456
- }
457
- },
458
- module: {
459
- strictExportPresence: true,
460
- parser: { javascript: { url: false, worker: false } },
461
- rules: [
462
- {
463
- test: /\.?(svg|html)$/,
464
- resourceQuery: /\?ngResource/,
465
- type: "asset/source"
466
- },
467
- {
468
- test: /[/\\]rxjs[/\\]add[/\\].+\.js$/,
469
- sideEffects: true
470
- },
471
- {
472
- test: /\.[cm]?[tj]sx?$/,
473
- resolve: { fullySpecified: false },
474
- exclude: [/[/\\](?:core-js|@babel|tslib|web-animations-js|web-streams-polyfill)[/\\]/],
475
- use: [
476
- {
477
- loader: "@angular-devkit/build-angular/src/babel/webpack-loader",
478
- options: {
479
- cacheDirectory: path.resolve(cacheBasePath, "babel-webpack"),
480
- scriptTarget: ts.ScriptTarget.ES2017,
481
- aot: true,
482
- optimize: !watch,
483
- instrumentCode: undefined
484
- }
485
- }
486
- ]
487
- },
488
- ...watch ? [
489
- {
490
- test: /\.[cm]?jsx?$/,
491
- enforce: "pre" as const,
492
- loader: "source-map-loader",
493
- options: {
494
- filterSourceMappingUrl: (mapUri: string, resourcePath: string) => {
495
- const workspaceRegex = new RegExp(`node_modules[\\\\/]@${workspaceName}[\\\\/]`);
496
- return !resourcePath.includes("node_modules")
497
- || (/node_modules[\\/]@simplysm[\\/]/).test(resourcePath)
498
- || workspaceRegex.test(resourcePath);
499
- }
500
- }
501
- }
502
- ] : [],
503
- {
504
- test: /\.[cm]?tsx?$/,
505
- loader: "@ngtools/webpack",
506
- exclude: [/[/\\](?:css-loader|mini-css-extract-plugin|webpack)[/\\]/]
507
- },
508
- {
509
- test: /\.css$/i,
510
- type: "asset/source"
511
- },
512
- {
513
- test: /\.scss$/i,
514
- rules: [
515
- {
516
- oneOf: [
517
- {
518
- use: [
519
- {
520
- loader: MiniCssExtractPlugin.loader
521
- },
522
- {
523
- loader: "css-loader",
524
- options: { url: false, sourceMap: watch }
525
- }
526
- ],
527
- include: [stylesFilePath],
528
- resourceQuery: { not: [/\?ngResource/] }
529
- },
530
- {
531
- type: "asset/source",
532
- resourceQuery: /\?ngResource/
533
- }
534
- ]
535
- },
536
- {
537
- use: [
538
- {
539
- loader: "resolve-url-loader",
540
- options: { sourceMap: watch }
541
- },
542
- {
543
- loader: "sass-loader",
544
- options: {
545
- implementation: sassImplementation,
546
- sourceMap: true,
547
- sassOptions: {
548
- fiber: false,
549
- precision: 8,
550
- includePaths: [],
551
- outputStyle: "expanded",
552
- quietDeps: true,
553
- verbose: watch ? undefined : false
554
- }
555
- }
556
- }
557
- ]
558
- }
559
- ]
560
- },
561
- {
562
- test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico|otf|xlsx?|pptx?|docx?|zip|pfx|pkl)$/,
563
- type: "asset/resource"
564
- }
565
- ]
566
- },
567
- plugins: [
568
- new NodePolyfillPlugin({
569
- excludeAliases: builderType === "electron" ? ["process"] : []
570
- }),
571
- new NamedChunksPlugin(),
572
- new DedupeModuleResolvePlugin(),
573
- new webpack.ProgressPlugin({
574
- handler: (per: number, msg: string, ...args: string[]) => {
575
- const phaseText = msg ? ` - phase: ${msg}` : "";
576
- const argsText = args.length > 0 ? ` - args: [${args.join(", ")}]` : "";
577
- const progressMessage = `Webpack 빌드 수행중...(${Math.round(per * 100)}%)${phaseText}${argsText}`;
578
- if (progressMessage !== prevProgressMessage) {
579
- prevProgressMessage = progressMessage;
580
- this._logger.debug(progressMessage);
581
- }
582
- }
583
- }),
584
- ...watch ? [] : [
585
- new LicenseWebpackPlugin({
586
- stats: { warnings: false, errors: false },
587
- perChunkOutput: false,
588
- outputFilename: "3rdpartylicenses.txt",
589
- skipChildCompilers: true
590
- })
591
- ],
592
- new CopyWebpackPlugin({
593
- patterns: [
594
- ...["favicon.ico", "assets/", "manifest.json"].map((item) => ({
595
- context: this._rootPath,
596
- to: item,
597
- from: `src/${item}`,
598
- noErrorOnMissing: true,
599
- force: true,
600
- globOptions: {
601
- dot: true,
602
- followSymbolicLinks: false,
603
- ignore: [
604
- ".gitkeep",
605
- "**/.DS_Store",
606
- "**/Thumbs.db"
607
- ].map((i) => PathUtil.posix(this._rootPath, i))
608
- },
609
- priority: 0
610
- })),
611
- ...builderType === "cordova" && watch ? this._cordova!.platforms.mapMany((platform) => [
612
- {
613
- context: this._cordova!.cordovaPath,
614
- to: `cordova-${platform}/plugins`,
615
- from: `platforms/${platform}/platform_www/plugins`,
616
- noErrorOnMissing: true
617
- },
618
- {
619
- context: this._cordova!.cordovaPath,
620
- to: `cordova-${platform}/cordova.js`,
621
- from: `platforms/${platform}/platform_www/cordova.js`
622
- },
623
- {
624
- context: this._cordova!.cordovaPath,
625
- to: `cordova-${platform}/cordova_plugins.js`,
626
- from: `platforms/${platform}/platform_www/cordova_plugins.js`,
627
- noErrorOnMissing: true
628
- },
629
- {
630
- context: this._cordova!.cordovaPath,
631
- to: `cordova-${platform}/config.xml`,
632
- from: `platforms/${platform}/www/config.xml`,
633
- noErrorOnMissing: true
634
- }
635
- ]) : []
636
- ]
637
- }),
638
- ...watch ? [
639
- new webpack.SourceMapDevToolPlugin({
640
- filename: "[file].map",
641
- include: [/js$/, /css$/],
642
- sourceRoot: "webpack:///",
643
- moduleFilenameTemplate: "[resource-path]",
644
- append: undefined
645
- })
646
- ] : [],
647
- new AngularWebpackPlugin({
648
- tsconfig: this._tsconfigFilePath,
649
- compilerOptions: {
650
- sourceMap: watch,
651
- declaration: false,
652
- declarationMap: false,
653
- preserveSymlinks: false
654
- },
655
- jitMode: false,
656
- emitNgModuleScope: watch,
657
- inlineStyleFileExtension: "scss"
658
- }),
659
- {
660
- apply: (compiler: webpack.Compiler) => {
661
- compiler.hooks.shutdown.tap("sass-worker", () => {
662
- sassImplementation.close();
663
- });
664
- }
665
- },
666
- new MiniCssExtractPlugin({ filename: "[name].css" }),
667
- new SuppressExtractedTextChunksWebpackPlugin(),
668
- new IndexHtmlWebpackPlugin({
669
- indexPath: path.resolve(this._rootPath, "src/index.html"),
670
- outputPath: "index.html",
671
- baseHref: publicPath,
672
- entrypoints: [
673
- ["runtime", !watch],
674
- ["polyfills", true],
675
- ["styles", false],
676
- ["vendor", true],
677
- ["main", true]
678
- ],
679
- deployUrl: undefined,
680
- sri: false,
681
- cache: {
682
- enabled: true,
683
- basePath: cacheBasePath,
684
- path: path.resolve(cacheBasePath, "index-webpack")
685
- },
686
- postTransform: undefined,
687
- optimization: {
688
- scripts: !watch,
689
- styles: { minify: !watch, inlineCritical: !watch },
690
- fonts: { inline: !watch }
691
- },
692
- crossOrigin: "none",
693
- lang: undefined
694
- }),
695
- new webpack.EnvironmentPlugin({
696
- SD_VERSION: this._getNpmConfig(this._rootPath)!.version,
697
- ...this._config.env
698
- }),
699
- new ESLintWebpackPlugin({
700
- context: this._rootPath,
701
- eslintPath: path.resolve(this._workspaceRootPath, "node_modules", "eslint"),
702
- exclude: ["node_modules"],
703
- extensions: ["ts", "js", "mjs", "cjs"],
704
- fix: false,
705
- threads: false,
706
- formatter: (results: LintResult[]) => {
707
- const resultMessages: string[] = [];
708
- for (const result of results) {
709
- for (const msg of result.messages) {
710
- const severity = msg.severity === 1 ? "warning" : msg.severity === 2 ? "error" : undefined;
711
- if (severity === undefined) continue;
712
-
713
- resultMessages.push(SdCliBuildResultUtil.getMessage({
714
- filePath: result.filePath,
715
- line: msg.line,
716
- char: msg.column,
717
- code: msg.ruleId?.toString(),
718
- severity,
719
- message: msg.message
720
- }));
721
- }
722
- }
723
- return resultMessages.join(os.EOL);
724
- }
725
- })
726
- ] as any[]
727
- };
728
- }
729
-
730
- private _getNpmConfig(pkgPath: string): INpmConfig | undefined {
731
- if (!this._npmConfigMap.has(pkgPath)) {
732
- this._npmConfigMap.set(pkgPath, FsUtil.readJson(path.resolve(pkgPath, "package.json")));
733
- }
734
- return this._npmConfigMap.get(pkgPath);
735
- }
736
- }
1
+ import { INpmConfig, ISdCliClientPackageConfig, ISdCliPackageBuildResult, ITsconfig } from "../commons";
2
+ import { EventEmitter } from "events";
3
+ import { FsUtil, Logger, PathUtil } from "@simplysm/sd-core-node";
4
+ import webpack from "webpack";
5
+ import path from "path";
6
+ import ts from "typescript";
7
+ import { SdCliBuildResultUtil } from "../utils/SdCliBuildResultUtil";
8
+ import { NamedChunksPlugin } from "@angular-devkit/build-angular/src/webpack/plugins/named-chunks-plugin";
9
+ import {
10
+ DedupeModuleResolvePlugin,
11
+ JavaScriptOptimizerPlugin,
12
+ SuppressExtractedTextChunksWebpackPlugin
13
+ } from "@angular-devkit/build-angular/src/webpack/plugins";
14
+ import CopyWebpackPlugin from "copy-webpack-plugin";
15
+ import MiniCssExtractPlugin from "mini-css-extract-plugin";
16
+ import { AngularWebpackPlugin } from "@ngtools/webpack";
17
+ import { IndexHtmlWebpackPlugin } from "@angular-devkit/build-angular/src/webpack/plugins/index-html-webpack-plugin";
18
+ import { SassWorkerImplementation } from "@angular-devkit/build-angular/src/sass/sass-service";
19
+ import { LicenseWebpackPlugin } from "license-webpack-plugin";
20
+ import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
21
+ import { createHash } from "crypto";
22
+ import ESLintWebpackPlugin from "eslint-webpack-plugin";
23
+ import os from "os";
24
+ import { ESLint } from "eslint";
25
+ import { TransferSizePlugin } from "@angular-devkit/build-angular/src/webpack/plugins/transfer-size-plugin";
26
+ import { CssOptimizerPlugin } from "@angular-devkit/build-angular/src/webpack/plugins/css-optimizer-plugin";
27
+ import browserslist from "browserslist";
28
+ import { augmentAppWithServiceWorker } from "@angular-devkit/build-angular/src/utils/service-worker";
29
+ import { SdCliNgModuleGenerator } from "../ng-tools/SdCliNgModuleGenerator";
30
+ import { SdCliCordova } from "../build-tool/SdCliCordova";
31
+ import { SdCliNpmConfigUtil } from "../utils/SdCliNpmConfigUtil";
32
+ import electronBuilder from "electron-builder";
33
+ import LintResult = ESLint.LintResult;
34
+
35
+ export class SdCliClientBuilder extends EventEmitter {
36
+ private readonly _logger: Logger;
37
+
38
+ private readonly _tsconfigFilePath: string;
39
+ private readonly _parsedTsconfig: ts.ParsedCommandLine;
40
+ private readonly _npmConfigMap = new Map<string, INpmConfig>();
41
+ private readonly _ngModuleGenerator: SdCliNgModuleGenerator;
42
+
43
+ private readonly _cordova?: SdCliCordova;
44
+
45
+ private readonly _hasAngularRoute: boolean;
46
+
47
+ public constructor(private readonly _rootPath: string,
48
+ private readonly _config: ISdCliClientPackageConfig,
49
+ private readonly _workspaceRootPath: string) {
50
+ super();
51
+
52
+ const npmConfig = this._getNpmConfig(this._rootPath)!;
53
+ this._logger = Logger.get(["simplysm", "sd-cli", this.constructor.name, npmConfig.name]);
54
+
55
+ // tsconfig
56
+ this._tsconfigFilePath = path.resolve(this._rootPath, "tsconfig-build.json");
57
+ const tsconfig = FsUtil.readJson(this._tsconfigFilePath) as ITsconfig;
58
+ this._parsedTsconfig = ts.parseJsonConfigFileContent(tsconfig, ts.sys, this._rootPath, tsconfig.angularCompilerOptions);
59
+
60
+ // isAngular
61
+ this._hasAngularRoute = SdCliNpmConfigUtil.getDependencies(npmConfig).defaults.includes("@angular/router");
62
+
63
+ // NgModule 생성기 초기화
64
+ this._ngModuleGenerator = new SdCliNgModuleGenerator(this._rootPath, [
65
+ "controls",
66
+ "directives",
67
+ "guards",
68
+ "modals",
69
+ "providers",
70
+ "app",
71
+ "pages",
72
+ "print-templates",
73
+ "toasts",
74
+ "AppPage"
75
+ ], this._hasAngularRoute ? {
76
+ glob: "**/*Page.ts",
77
+ fileEndsWith: "Page",
78
+ rootClassName: "AppPage"
79
+ } : undefined);
80
+
81
+ // CORDOVA
82
+ if (this._config.builder?.cordova) {
83
+ this._cordova = new SdCliCordova(this._rootPath, this._config.builder.cordova);
84
+ }
85
+ }
86
+
87
+ public override on(event: "change", listener: () => void): this;
88
+ public override on(event: "complete", listener: (results: ISdCliPackageBuildResult[]) => void): this;
89
+ public override on(event: string | symbol, listener: (...args: any[]) => void): this {
90
+ return super.on(event, listener);
91
+ }
92
+
93
+ public async watchAsync(): Promise<void> {
94
+ // DIST 비우기
95
+ await FsUtil.removeAsync(path.resolve(this._rootPath, ".electron"));
96
+ await FsUtil.removeAsync(this._parsedTsconfig.options.outDir!);
97
+
98
+ // NgModule 생성
99
+ await this._ngModuleGenerator.runAsync();
100
+
101
+ // CORDOVA 초기화
102
+ if (this._cordova) {
103
+ this._logger.debug("CORDOVA 구성...");
104
+ await this._cordova.initializeAsync();
105
+ }
106
+
107
+ // 빌드 준비
108
+ const webpackConfigs = (Object.keys(this._config.builder ?? { web: {} }) as ("web" | "cordova" | "electron")[])
109
+ .map((builderType) => this._getWebpackConfig(true, builderType));
110
+ const multiCompiler = webpack(webpackConfigs);
111
+ await new Promise<void>((resolve, reject) => {
112
+ multiCompiler.hooks.invalid.tap(this.constructor.name, (fileName) => {
113
+ if (fileName != null) {
114
+ this._logger.debug("파일변경 감지", fileName);
115
+ // NgModule 캐시 삭제
116
+ this._ngModuleGenerator.removeCaches([path.resolve(fileName)]);
117
+ }
118
+ });
119
+
120
+ multiCompiler.hooks.watchRun.tapAsync(this.constructor.name, async (args, callback) => {
121
+ this.emit("change");
122
+
123
+ // NgModule 생성
124
+ await this._ngModuleGenerator.runAsync();
125
+
126
+ callback();
127
+
128
+ this._logger.debug("Webpack 빌드 수행...");
129
+ });
130
+
131
+ multiCompiler.watch({}, async (err, multiStats) => {
132
+ if (err != null || multiStats == null) {
133
+ this.emit("complete", [{
134
+ filePath: undefined,
135
+ line: undefined,
136
+ char: undefined,
137
+ code: undefined,
138
+ severity: "error",
139
+ message: err?.stack ?? "알 수 없는 오류 (multiStats=null)"
140
+ }]);
141
+ reject(err);
142
+ return;
143
+ }
144
+
145
+ // 결과 반환
146
+ const results = multiStats.stats.mapMany((stats) => SdCliBuildResultUtil.convertFromWebpackStats(stats));
147
+
148
+ // .config.json 파일 쓰기
149
+ const npmConfig = this._getNpmConfig(this._rootPath)!;
150
+ const packageKey = npmConfig.name.split("/").last()!;
151
+
152
+ const configDistPath = typeof this._config.server === "string"
153
+ ? path.resolve(this._workspaceRootPath, "packages", this._config.server, "dist/www", packageKey, ".config.json")
154
+ : path.resolve(this._parsedTsconfig.options.outDir!, ".config.json");
155
+ await FsUtil.writeFileAsync(configDistPath, JSON.stringify(this._config.configs ?? {}, undefined, 2));
156
+
157
+ // 마무리
158
+ this._logger.debug("Webpack 빌드 완료");
159
+ resolve();
160
+
161
+ this.emit("complete", results);
162
+ });
163
+ });
164
+ }
165
+
166
+ public async buildAsync(): Promise<ISdCliPackageBuildResult[]> {
167
+ // DIST 비우기
168
+ await FsUtil.removeAsync(path.resolve(this._rootPath, ".electron"));
169
+ await FsUtil.removeAsync(this._parsedTsconfig.options.outDir!);
170
+
171
+ // NgModule 생성
172
+ await this._ngModuleGenerator.runAsync();
173
+
174
+ // CORDOVA 초기화
175
+ if (this._cordova) {
176
+ this._logger.debug("CORDOVA 구성...");
177
+ await this._cordova.initializeAsync();
178
+ }
179
+
180
+ // 빌드
181
+ this._logger.debug("Webpack 빌드 수행...");
182
+ const builderTypes = (Object.keys(this._config.builder ?? { web: {} }) as ("web" | "cordova" | "electron")[]);
183
+ const webpackConfigs = builderTypes.map((builderType) => this._getWebpackConfig(false, builderType));
184
+ const multipleCompiler = webpack(webpackConfigs);
185
+ const buildResults = await new Promise<ISdCliPackageBuildResult[]>((resolve, reject) => {
186
+ multipleCompiler.run((err, multiStats) => {
187
+ if (err != null || multiStats == null) {
188
+ reject(err);
189
+ return;
190
+ }
191
+
192
+ // 결과 반환
193
+ const results = multiStats.stats.mapMany((stats) => SdCliBuildResultUtil.convertFromWebpackStats(stats));
194
+ resolve(results);
195
+ });
196
+ });
197
+
198
+ // .config.json 파일 쓰기
199
+ const targetPath = path.resolve(this._parsedTsconfig.options.outDir!, ".config.json");
200
+ await FsUtil.writeFileAsync(targetPath, JSON.stringify(this._config.configs ?? {}, undefined, 2));
201
+
202
+ // service-worker 처리
203
+ if (builderTypes.includes("web") && FsUtil.exists(path.resolve(this._rootPath, "ngsw-config.json"))) {
204
+ const packageKey = this._getNpmConfig(this._rootPath)!.name.split("/").last()!;
205
+ await augmentAppWithServiceWorker(
206
+ PathUtil.posix(path.relative(this._workspaceRootPath, this._rootPath)) as any,
207
+ PathUtil.posix(path.relative(this._workspaceRootPath, path.resolve(this._parsedTsconfig.options.outDir!))) as any,
208
+ `/${packageKey}/`,
209
+ PathUtil.posix(path.relative(this._workspaceRootPath, path.resolve(this._rootPath, "ngsw-config.json")))
210
+ );
211
+ }
212
+
213
+ // CORDOVA 빌드
214
+ if (this._cordova) {
215
+ this._logger.debug("CORDOVA 빌드...");
216
+ await this._cordova.buildAsync(path.resolve(this._parsedTsconfig.options.outDir!, "cordova"));
217
+ }
218
+
219
+ // ELECTRON
220
+ if (this._config.builder?.electron) {
221
+ const npmConfig = this._getNpmConfig(this._rootPath)!;
222
+
223
+ const electronVersion = npmConfig.dependencies?.["electron"];
224
+ if (electronVersion === undefined) {
225
+ throw new Error("ELECTRON 빌드 패키지의 'dependencies'에는 'electron'이 반드시 포함되어야 합니다.");
226
+ }
227
+
228
+ const dotenvVersion = npmConfig.dependencies?.["dotenv"];
229
+ if (dotenvVersion === undefined) {
230
+ throw new Error("ELECTRON 빌드 패키지의 'dependencies'에는 'dotenv'가 반드시 포함되어야 합니다.");
231
+ }
232
+
233
+ const remoteVersion = npmConfig.dependencies?.["@electron/remote"];
234
+
235
+ const electronSrcPath = path.resolve(this._rootPath, `.electron/src`);
236
+ const electronDistPath = path.resolve(this._rootPath, `.electron/dist`);
237
+
238
+ await FsUtil.writeJsonAsync(path.resolve(electronSrcPath, `package.json`), {
239
+ name: npmConfig.name,
240
+ version: npmConfig.version,
241
+ description: npmConfig.description,
242
+ main: "electron.js",
243
+ author: npmConfig.author,
244
+ license: npmConfig.license,
245
+ devDependencies: {
246
+ "electron": electronVersion.replace("^", "")
247
+ },
248
+ dependencies: {
249
+ "dotenv": dotenvVersion,
250
+ ...remoteVersion !== undefined ? {
251
+ "@electron/remote": remoteVersion
252
+ } : {}
253
+ }
254
+ }, { space: 2 });
255
+ await FsUtil.writeFileAsync(path.resolve(electronSrcPath, "yarn.lock"), "");
256
+
257
+ await FsUtil.writeFileAsync(path.resolve(electronSrcPath, `.env`), [
258
+ "NODE_ENV=production",
259
+ `SD_VERSION=${npmConfig.version}`,
260
+ (this._config.builder.electron.icon !== undefined) ? `SD_ELECTRON_ICON=${this._config.builder.electron.icon}` : `SD_ELECTRON_ICON=favicon.ico`,
261
+ ...(this._config.env !== undefined) ? Object.keys(this._config.env).map((key) => `${key}=${this._config.env![key]}`) : []
262
+ ].filterExists().join("\n"));
263
+
264
+ let electronTsFileContent = await FsUtil.readFileAsync(path.resolve(this._rootPath, `src/electron.ts`));
265
+ electronTsFileContent = "require(\"dotenv\").config({ path: `${__dirname}\\\\.env` });\n" + electronTsFileContent;
266
+ const result = ts.transpileModule(electronTsFileContent, { compilerOptions: { module: ts.ModuleKind.CommonJS } });
267
+ await FsUtil.writeFileAsync(path.resolve(electronSrcPath, "electron.js"), result.outputText);
268
+
269
+ await electronBuilder.build({
270
+ targets: electronBuilder.Platform.WINDOWS.createTarget(),
271
+ config: {
272
+ appId: this._config.builder.electron.appId,
273
+ productName: npmConfig.description,
274
+ // asar: false,
275
+ win: {
276
+ target: "nsis"
277
+ },
278
+ nsis: {},
279
+ directories: {
280
+ app: electronSrcPath,
281
+ output: electronDistPath
282
+ },
283
+ ...this._config.builder.electron.installerIcon !== undefined ? {
284
+ icon: path.resolve(this._rootPath, "src", this._config.builder.electron.installerIcon)
285
+ } : {}
286
+ }
287
+ });
288
+
289
+ await FsUtil.copyAsync(
290
+ path.resolve(this._rootPath, `.electron/dist/${npmConfig.description} Setup ${npmConfig.version}.exe`),
291
+ path.resolve(this._parsedTsconfig.options.outDir!, `electron/${npmConfig.description}-latest.exe`)
292
+ );
293
+
294
+ await FsUtil.copyAsync(
295
+ path.resolve(this._rootPath, `.electron/dist/${npmConfig.description} Setup ${npmConfig.version}.exe`),
296
+ path.resolve(this._parsedTsconfig.options.outDir!, `electron/updates/${npmConfig.version}.exe`)
297
+ );
298
+ }
299
+
300
+ // 마무리
301
+ this._logger.debug("Webpack 빌드 완료");
302
+ return buildResults;
303
+ }
304
+
305
+ private _getInternalModuleCachePaths(workspaceName: string): string[] {
306
+ return [
307
+ ...FsUtil.findAllParentChildDirPaths("node_modules/*/package.json", this._rootPath, this._workspaceRootPath),
308
+ ...FsUtil.findAllParentChildDirPaths(`node_modules/!(@simplysm|@${workspaceName})/*/package.json`, this._rootPath, this._workspaceRootPath),
309
+ ].map((p) => path.dirname(p));
310
+ }
311
+
312
+ private _getWebpackConfig(watch: boolean, builderType: "web" | "cordova" | "electron"): webpack.Configuration {
313
+ const workspaceNpmConfig = this._getNpmConfig(this._workspaceRootPath)!;
314
+ const workspaceName = workspaceNpmConfig.name;
315
+
316
+ const internalModuleCachePaths = watch ? this._getInternalModuleCachePaths(workspaceName) : undefined;
317
+
318
+ const npmConfig = this._getNpmConfig(this._rootPath)!;
319
+
320
+ const workspacePkgLockContent = FsUtil.readFile(path.resolve(this._workspaceRootPath, "yarn.lock"));
321
+
322
+ const pkgKey = npmConfig.name.split("/").last()!;
323
+ const publicPath = builderType === "web" ? `/${pkgKey}/` : watch ? `/${pkgKey}/${builderType}/` : ``;
324
+
325
+ const cacheBasePath = path.resolve(this._rootPath, ".cache");
326
+
327
+ const distPath = (builderType === "cordova" && !watch) ? path.resolve(this._cordova!.cordovaPath, "www")
328
+ : (builderType === "electron" && !watch) ? path.resolve(this._rootPath, ".electron/src")
329
+ : builderType === "web" ? this._parsedTsconfig.options.outDir
330
+ : `${this._parsedTsconfig.options.outDir}/${builderType}`;
331
+
332
+ const sassImplementation = new SassWorkerImplementation();
333
+
334
+ const mainFilePath = path.resolve(this._rootPath, "src/main.ts");
335
+ const polyfillsFilePath = path.resolve(this._rootPath, "src/polyfills.ts");
336
+ const stylesFilePath = path.resolve(this._rootPath, "src/styles.scss");
337
+
338
+ let prevProgressMessage = "";
339
+ return {
340
+ mode: watch ? "development" : "production",
341
+ devtool: false,
342
+ target: builderType === "electron" ? ["electron-renderer", "es2015"] : ["web", "es2015"],
343
+ profile: false,
344
+ resolve: {
345
+ roots: [this._rootPath],
346
+ extensions: [".ts", ".tsx", ".mjs", ".cjs", ".js", ".jsx"],
347
+ symlinks: true,
348
+ modules: [this._workspaceRootPath, "node_modules"],
349
+ mainFields: ["es2015", "browser", "module", "main"],
350
+ conditionNames: ["es2015", "..."],
351
+ },
352
+ resolveLoader: {
353
+ symlinks: true
354
+ },
355
+ context: this._workspaceRootPath,
356
+ entry: {
357
+ main: [mainFilePath],
358
+ ...FsUtil.exists(polyfillsFilePath) ? { polyfills: [polyfillsFilePath] } : {},
359
+ ...FsUtil.exists(stylesFilePath) ? { styles: [stylesFilePath] } : {}
360
+ },
361
+ output: {
362
+ uniqueName: pkgKey,
363
+ hashFunction: "xxhash64",
364
+ clean: true,
365
+ path: distPath,
366
+ publicPath,
367
+ filename: "[name].js",
368
+ chunkFilename: "[name].js",
369
+ libraryTarget: undefined,
370
+ crossOriginLoading: false,
371
+ trustedTypes: "angular#bundler",
372
+ scriptType: "module"
373
+ },
374
+ watch: false,
375
+ watchOptions: {
376
+ poll: undefined,
377
+ ignored: undefined
378
+ },
379
+ performance: { hints: false },
380
+ ignoreWarnings: [
381
+ /Failed to parse source map from/,
382
+ /Add postcss as project dependency/,
383
+ /"@charset" must be the first rule in the file/
384
+ ],
385
+ experiments: { backCompat: false, syncWebAssembly: true, asyncWebAssembly: true },
386
+ infrastructureLogging: { level: "error" },
387
+ stats: "errors-warnings",
388
+ cache: {
389
+ type: "filesystem",
390
+ profile: watch ? undefined : false,
391
+ cacheDirectory: path.resolve(cacheBasePath, "angular-webpack"),
392
+ maxMemoryGenerations: 1,
393
+ name: createHash("sha1")
394
+ .update(workspacePkgLockContent)
395
+ .update(JSON.stringify(this._parsedTsconfig.options))
396
+ .update(JSON.stringify(this._config))
397
+ .update(watch.toString())
398
+ .digest("hex")
399
+ },
400
+ ...watch ? {
401
+ snapshot: {
402
+ immutablePaths: internalModuleCachePaths,
403
+ managedPaths: internalModuleCachePaths
404
+ }
405
+ } : {},
406
+ node: false,
407
+ optimization: {
408
+ minimizer: watch ? [] : [
409
+ new JavaScriptOptimizerPlugin({
410
+ define: {
411
+ ngDevMode: false,
412
+ ngI18nClosureMode: false,
413
+ ngJitMode: false
414
+ },
415
+ sourcemap: false,
416
+ target: ts.ScriptTarget.ES2017,
417
+ keepIdentifierNames: true,
418
+ keepNames: true,
419
+ removeLicenses: true,
420
+ advanced: true
421
+ }),
422
+ new TransferSizePlugin(),
423
+ new CssOptimizerPlugin({
424
+ supportedBrowsers: browserslist([
425
+ "last 1 Chrome versions",
426
+ "last 2 Edge major versions"
427
+ ], { path: this._workspaceRootPath })
428
+ })
429
+ ] as any[],
430
+ moduleIds: "deterministic",
431
+ chunkIds: watch ? "named" : "deterministic",
432
+ emitOnErrors: watch,
433
+ runtimeChunk: "single",
434
+ splitChunks: {
435
+ maxAsyncRequests: Infinity,
436
+ cacheGroups: {
437
+ default: {
438
+ chunks: "async",
439
+ minChunks: 2,
440
+ priority: 10
441
+ },
442
+ common: {
443
+ name: "common",
444
+ chunks: "async",
445
+ minChunks: 2,
446
+ enforce: true,
447
+ priority: 5
448
+ },
449
+ vendors: false,
450
+ defaultVendors: watch ? {
451
+ name: "vendor",
452
+ chunks: (chunk) => chunk.name === "main",
453
+ enforce: true,
454
+ test: /[\\/]node_modules[\\/]/
455
+ } : false
456
+ }
457
+ }
458
+ },
459
+ module: {
460
+ strictExportPresence: true,
461
+ parser: { javascript: { url: false, worker: false } },
462
+ rules: [
463
+ {
464
+ test: /\.?(svg|html)$/,
465
+ resourceQuery: /\?ngResource/,
466
+ type: "asset/source"
467
+ },
468
+ {
469
+ test: /[/\\]rxjs[/\\]add[/\\].+\.js$/,
470
+ sideEffects: true
471
+ },
472
+ {
473
+ test: /\.[cm]?[tj]sx?$/,
474
+ resolve: { fullySpecified: false },
475
+ exclude: [/[/\\](?:core-js|@babel|tslib|web-animations-js|web-streams-polyfill)[/\\]/],
476
+ use: [
477
+ {
478
+ loader: "@angular-devkit/build-angular/src/babel/webpack-loader",
479
+ options: {
480
+ cacheDirectory: path.resolve(cacheBasePath, "babel-webpack"),
481
+ scriptTarget: ts.ScriptTarget.ES2017,
482
+ aot: true,
483
+ optimize: !watch,
484
+ instrumentCode: undefined
485
+ }
486
+ }
487
+ ]
488
+ },
489
+ ...watch ? [
490
+ {
491
+ test: /\.[cm]?jsx?$/,
492
+ enforce: "pre" as const,
493
+ loader: "source-map-loader",
494
+ options: {
495
+ filterSourceMappingUrl: (mapUri: string, resourcePath: string) => {
496
+ const workspaceRegex = new RegExp(`node_modules[\\\\/]@${workspaceName}[\\\\/]`);
497
+ return !resourcePath.includes("node_modules")
498
+ || (/node_modules[\\/]@simplysm[\\/]/).test(resourcePath)
499
+ || workspaceRegex.test(resourcePath);
500
+ }
501
+ }
502
+ }
503
+ ] : [],
504
+ {
505
+ test: /\.[cm]?tsx?$/,
506
+ loader: "@ngtools/webpack",
507
+ exclude: [/[/\\](?:css-loader|mini-css-extract-plugin|webpack)[/\\]/]
508
+ },
509
+ {
510
+ test: /\.css$/i,
511
+ type: "asset/source"
512
+ },
513
+ {
514
+ test: /\.scss$/i,
515
+ rules: [
516
+ {
517
+ oneOf: [
518
+ {
519
+ use: [
520
+ {
521
+ loader: MiniCssExtractPlugin.loader
522
+ },
523
+ {
524
+ loader: "css-loader",
525
+ options: { url: false, sourceMap: watch }
526
+ }
527
+ ],
528
+ include: [stylesFilePath],
529
+ resourceQuery: { not: [/\?ngResource/] }
530
+ },
531
+ {
532
+ type: "asset/source",
533
+ resourceQuery: /\?ngResource/
534
+ }
535
+ ]
536
+ },
537
+ {
538
+ use: [
539
+ {
540
+ loader: "resolve-url-loader",
541
+ options: { sourceMap: watch }
542
+ },
543
+ {
544
+ loader: "sass-loader",
545
+ options: {
546
+ implementation: sassImplementation,
547
+ sourceMap: true,
548
+ sassOptions: {
549
+ fiber: false,
550
+ precision: 8,
551
+ includePaths: [],
552
+ outputStyle: "expanded",
553
+ quietDeps: true,
554
+ verbose: watch ? undefined : false
555
+ }
556
+ }
557
+ }
558
+ ]
559
+ }
560
+ ]
561
+ },
562
+ {
563
+ test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico|otf|xlsx?|pptx?|docx?|zip|pfx|pkl)$/,
564
+ type: "asset/resource"
565
+ }
566
+ ]
567
+ },
568
+ plugins: [
569
+ new NodePolyfillPlugin({
570
+ excludeAliases: builderType === "electron" ? ["process"] : []
571
+ }),
572
+ new NamedChunksPlugin(),
573
+ new DedupeModuleResolvePlugin(),
574
+ new webpack.ProgressPlugin({
575
+ handler: (per: number, msg: string, ...args: string[]) => {
576
+ const phaseText = msg ? ` - phase: ${msg}` : "";
577
+ const argsText = args.length > 0 ? ` - args: [${args.join(", ")}]` : "";
578
+ const progressMessage = `Webpack 빌드 수행중...(${Math.round(per * 100)}%)${phaseText}${argsText}`;
579
+ if (progressMessage !== prevProgressMessage) {
580
+ prevProgressMessage = progressMessage;
581
+ this._logger.debug(progressMessage);
582
+ }
583
+ }
584
+ }),
585
+ ...watch ? [] : [
586
+ new LicenseWebpackPlugin({
587
+ stats: { warnings: false, errors: false },
588
+ perChunkOutput: false,
589
+ outputFilename: "3rdpartylicenses.txt",
590
+ skipChildCompilers: true
591
+ })
592
+ ],
593
+ new CopyWebpackPlugin({
594
+ patterns: [
595
+ ...["favicon.ico", "assets/", "manifest.json"].map((item) => ({
596
+ context: this._rootPath,
597
+ to: item,
598
+ from: `src/${item}`,
599
+ noErrorOnMissing: true,
600
+ force: true,
601
+ globOptions: {
602
+ dot: true,
603
+ followSymbolicLinks: false,
604
+ ignore: [
605
+ ".gitkeep",
606
+ "**/.DS_Store",
607
+ "**/Thumbs.db"
608
+ ].map((i) => PathUtil.posix(this._rootPath, i))
609
+ },
610
+ priority: 0
611
+ })),
612
+ ...builderType === "cordova" && watch ? this._cordova!.platforms.mapMany((platform) => [
613
+ {
614
+ context: this._cordova!.cordovaPath,
615
+ to: `cordova-${platform}/plugins`,
616
+ from: `platforms/${platform}/platform_www/plugins`,
617
+ noErrorOnMissing: true
618
+ },
619
+ {
620
+ context: this._cordova!.cordovaPath,
621
+ to: `cordova-${platform}/cordova.js`,
622
+ from: `platforms/${platform}/platform_www/cordova.js`
623
+ },
624
+ {
625
+ context: this._cordova!.cordovaPath,
626
+ to: `cordova-${platform}/cordova_plugins.js`,
627
+ from: `platforms/${platform}/platform_www/cordova_plugins.js`,
628
+ noErrorOnMissing: true
629
+ },
630
+ {
631
+ context: this._cordova!.cordovaPath,
632
+ to: `cordova-${platform}/config.xml`,
633
+ from: `platforms/${platform}/www/config.xml`,
634
+ noErrorOnMissing: true
635
+ }
636
+ ]) : []
637
+ ]
638
+ }),
639
+ ...watch ? [
640
+ new webpack.SourceMapDevToolPlugin({
641
+ filename: "[file].map",
642
+ include: [/js$/, /css$/],
643
+ sourceRoot: "webpack:///",
644
+ moduleFilenameTemplate: "[resource-path]",
645
+ append: undefined
646
+ })
647
+ ] : [],
648
+ new AngularWebpackPlugin({
649
+ tsconfig: this._tsconfigFilePath,
650
+ compilerOptions: {
651
+ sourceMap: watch,
652
+ declaration: false,
653
+ declarationMap: false,
654
+ preserveSymlinks: false
655
+ },
656
+ jitMode: false,
657
+ emitNgModuleScope: watch,
658
+ inlineStyleFileExtension: "scss"
659
+ }),
660
+ {
661
+ apply: (compiler: webpack.Compiler) => {
662
+ compiler.hooks.shutdown.tap("sass-worker", () => {
663
+ sassImplementation.close();
664
+ });
665
+ }
666
+ },
667
+ new MiniCssExtractPlugin({ filename: "[name].css" }),
668
+ new SuppressExtractedTextChunksWebpackPlugin(),
669
+ new IndexHtmlWebpackPlugin({
670
+ indexPath: path.resolve(this._rootPath, "src/index.html"),
671
+ outputPath: "index.html",
672
+ baseHref: publicPath,
673
+ entrypoints: [
674
+ ["runtime", !watch],
675
+ ["polyfills", true],
676
+ ["styles", false],
677
+ ["vendor", true],
678
+ ["main", true]
679
+ ],
680
+ deployUrl: undefined,
681
+ sri: false,
682
+ cache: {
683
+ enabled: true,
684
+ basePath: cacheBasePath,
685
+ path: path.resolve(cacheBasePath, "index-webpack")
686
+ },
687
+ postTransform: undefined,
688
+ optimization: {
689
+ scripts: !watch,
690
+ styles: { minify: !watch, inlineCritical: !watch },
691
+ fonts: { inline: !watch }
692
+ },
693
+ crossOrigin: "none",
694
+ lang: undefined
695
+ }),
696
+ new webpack.EnvironmentPlugin({
697
+ SD_VERSION: this._getNpmConfig(this._rootPath)!.version,
698
+ ...this._config.env
699
+ }),
700
+ new ESLintWebpackPlugin({
701
+ context: this._rootPath,
702
+ eslintPath: path.resolve(this._workspaceRootPath, "node_modules", "eslint"),
703
+ exclude: ["node_modules"],
704
+ extensions: ["ts", "js", "mjs", "cjs"],
705
+ fix: false,
706
+ threads: false,
707
+ formatter: (results: LintResult[]) => {
708
+ const resultMessages: string[] = [];
709
+ for (const result of results) {
710
+ for (const msg of result.messages) {
711
+ const severity = msg.severity === 1 ? "warning" : msg.severity === 2 ? "error" : undefined;
712
+ if (severity === undefined) continue;
713
+
714
+ resultMessages.push(SdCliBuildResultUtil.getMessage({
715
+ filePath: result.filePath,
716
+ line: msg.line,
717
+ char: msg.column,
718
+ code: msg.ruleId?.toString(),
719
+ severity,
720
+ message: msg.message
721
+ }));
722
+ }
723
+ }
724
+ return resultMessages.join(os.EOL);
725
+ }
726
+ })
727
+ ] as any[]
728
+ };
729
+ }
730
+
731
+ private _getNpmConfig(pkgPath: string): INpmConfig | undefined {
732
+ if (!this._npmConfigMap.has(pkgPath)) {
733
+ this._npmConfigMap.set(pkgPath, FsUtil.readJson(path.resolve(pkgPath, "package.json")));
734
+ }
735
+ return this._npmConfigMap.get(pkgPath);
736
+ }
737
+ }