@simplysm/sd-cli 7.0.237 → 7.0.241

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,455 +1,455 @@
1
- import { FsUtil, Logger, PathUtil, SdProcess } from "@simplysm/sd-core-node";
2
- import path from "path";
3
- import { INpmConfig, ISdCliConfig, ISdCliPackageBuildResult } from "../commons";
4
- import { SdCliPackage } from "../packages/SdCliPackage";
5
- import { Uuid, Wait } from "@simplysm/sd-core-common";
6
- import os from "os";
7
- import { SdCliBuildResultUtil } from "../utils/SdCliBuildResultUtil";
8
- import semver from "semver/preload";
9
- import { SdCliConfigUtil } from "../utils/SdCliConfigUtil";
10
- import { SdServiceServer } from "@simplysm/sd-service-server";
11
- import { SdCliLocalUpdate } from "./SdCliLocalUpdate";
12
- import url from "url";
13
- import mime from "mime";
14
- import { NextHandleFunction } from "connect";
15
-
16
- export class SdCliWorkspace {
17
- private readonly _logger = Logger.get(["simplysm", "sd-cli", this.constructor.name]);
18
-
19
- private readonly _npmConfig: INpmConfig;
20
- private readonly _serverInfoMap = new Map<string, IServerInfo>();
21
-
22
- public constructor(private readonly _rootPath: string) {
23
- const npmConfigFilePath = path.resolve(this._rootPath, "package.json");
24
- this._npmConfig = FsUtil.readJson(npmConfigFilePath);
25
- }
26
-
27
- public async watchAsync(opt: { confFileRelPath: string; optNames: string[]; pkgs: string[] }): Promise<void> {
28
- this._logger.debug("프로젝트 설정 가져오기...");
29
- const config = await SdCliConfigUtil.loadConfigAsync(path.resolve(this._rootPath, opt.confFileRelPath), true, opt.optNames);
30
-
31
- if (config.localUpdates) {
32
- this._logger.debug("로컬 라이브러리 업데이트 변경감지 시작...");
33
- await new SdCliLocalUpdate(this._rootPath).watchAsync({ confFileRelPath: opt.confFileRelPath });
34
- }
35
-
36
- this._logger.debug("패키지 목록 구성...");
37
- const pkgs = await this._getPackagesAsync(config, opt.pkgs);
38
-
39
- this._logger.debug("패키지 이벤트 설정...");
40
- let changeCount = 0;
41
- let changePkgs: SdCliPackage[] = [];
42
- const totalResultMap = new Map<string, ISdCliPackageBuildResult[]>();
43
- for (const pkg of pkgs) {
44
- pkg
45
- .on("change", () => {
46
- if (changeCount === 0) {
47
- this._logger.log("빌드를 시작합니다...");
48
- }
49
- changeCount++;
50
- this._logger.debug(`[${pkg.name}] 빌드를 시작합니다...`);
51
- })
52
- .on("complete", (results) => {
53
- changePkgs.push(pkg);
54
-
55
- this._logger.debug(`[${pkg.name}] 빌드가 완료되었습니다.`);
56
- totalResultMap.set(pkg.name, results);
57
-
58
- setTimeout(async () => {
59
- changeCount--;
60
- if (changeCount === 0) {
61
- const currChangePkgs = [...changePkgs].distinct(true);
62
- changePkgs = [];
63
-
64
- for (const changePkg of currChangePkgs) {
65
- if (changePkg.config.type === "server" && !results.some((item) => item.severity === "error")) {
66
- await this._restartServerAsync(changePkg);
67
- }
68
-
69
- if (changePkg.config.type === "client") {
70
- if (typeof changePkg.config.server === "string") {
71
- const serverInfo = this._serverInfoMap.get(changePkg.config.server);
72
- serverInfo?.server?.broadcastReload();
73
- }
74
- else {
75
- const serverInfo = this._serverInfoMap.get("PORT:" + changePkg.config.server.port);
76
- serverInfo?.server?.broadcastReload();
77
- }
78
- }
79
- }
80
-
81
- this._loggingResults(totalResultMap);
82
- this._loggingOpenClientHrefs();
83
- this._logger.info("모든 빌드가 완료되었습니다.");
84
- }
85
- }, 500);
86
- });
87
- }
88
-
89
-
90
- if (changeCount === 0) {
91
- this._logger.log("빌드를 시작합니다...");
92
- }
93
- changeCount++;
94
-
95
- try {
96
- const pkgNames = pkgs.map((item) => item.name);
97
- const buildCompletedPackageNames: string[] = [];
98
- await pkgs.parallelAsync(async (pkg) => {
99
- await Wait.until(() => !pkg.allDependencies.some((dep) => pkgNames.includes(dep) && !buildCompletedPackageNames.includes(dep)));
100
- if (pkg.config.type === "client") {
101
- await pkg.watchAsync();
102
-
103
- const pkgMiddleware: NextHandleFunction = (req, res, next) => {
104
- if (req.method === "GET") {
105
- const urlObj = url.parse(req.url!, true, false);
106
- const urlPathChain = decodeURI(urlObj.pathname!.slice(1)).split("/");
107
- if (urlPathChain[0] === pkg.name.split("/").last()!) {
108
- let targetFilePath = path.resolve(pkg.rootPath, "dist", ...urlPathChain.slice(1));
109
- targetFilePath = FsUtil.exists(targetFilePath) && FsUtil.stat(targetFilePath).isDirectory() ? path.resolve(targetFilePath, "index.html") : targetFilePath;
110
-
111
- if (FsUtil.exists(targetFilePath) && !path.basename(targetFilePath).startsWith(".")) {
112
- const fileStream = FsUtil.createReadStream(targetFilePath);
113
- const targetFileSize = FsUtil.lstat(targetFilePath).size;
114
-
115
- fileStream.on("open", () => {
116
- res.setHeader("Content-Length", targetFileSize);
117
- res.setHeader("Content-Type", mime.getType(targetFilePath)!);
118
- res.writeHead(200);
119
- });
120
- fileStream.pipe(res);
121
- return;
122
- }
123
- }
124
- }
125
-
126
- next();
127
- };
128
-
129
- if (typeof pkg.config.server === "string") {
130
- const serverInfo = this._serverInfoMap.getOrCreate(pkg.config.server, { middlewares: [], clientInfos: [] });
131
- serverInfo.middlewares.push(pkgMiddleware);
132
- serverInfo.clientInfos.push({
133
- pkgKey: pkg.name.split("/").last()!,
134
- platforms: pkg.config.builder ? Object.keys(pkg.config.builder) : ["web"],
135
- cordovaTargets: pkg.config.builder?.cordova?.target ? Object.keys(pkg.config.builder.cordova.target) : ["browser"]
136
- });
137
- }
138
- else { // DEV SERVER
139
- const serverInfo = this._serverInfoMap.getOrCreate("PORT:" + pkg.config.server.port, {
140
- middlewares: [],
141
- clientInfos: []
142
- });
143
- if (serverInfo.server === undefined) {
144
- const server = new SdServiceServer({
145
- rootPath: process.cwd(),
146
- services: [],
147
- port: pkg.config.server.port
148
- });
149
- await server.listenAsync();
150
- serverInfo.server = server;
151
- serverInfo.server.devMiddlewares = [pkgMiddleware];
152
- serverInfo.clientInfos.push({
153
- pkgKey: pkg.name.split("/").last()!,
154
- platforms: pkg.config.builder ? Object.keys(pkg.config.builder) : ["web"],
155
- cordovaTargets: pkg.config.builder?.cordova?.target ? Object.keys(pkg.config.builder.cordova.target) : ["browser"]
156
- });
157
- }
158
- }
159
- }
160
- else {
161
- await pkg.watchAsync();
162
- }
163
- buildCompletedPackageNames.push(pkg.name);
164
- });
165
- }
166
- catch (err) {
167
- this._loggingResults(totalResultMap);
168
- this._loggingOpenClientHrefs();
169
- throw err;
170
- }
171
-
172
- changeCount--;
173
- if (changeCount === 0) {
174
- this._loggingResults(totalResultMap);
175
- this._loggingOpenClientHrefs();
176
- this._logger.info("모든 빌드가 완료되었습니다.");
177
- }
178
- }
179
-
180
- private _isServerRestarting = false;
181
-
182
- private async _restartServerAsync(pkg: SdCliPackage): Promise<void> {
183
- await Wait.until(() => !this._isServerRestarting);
184
-
185
- this._isServerRestarting = true;
186
- const entryFileRelPath = pkg.main;
187
- if (entryFileRelPath === undefined) {
188
- this._logger.error(`서버패키지(${pkg.name})의 'package.json'에서 'main'필드를 찾을 수 없습니다.`);
189
- this._isServerRestarting = false;
190
- return;
191
- }
192
- const entryFilePath = path.resolve(pkg.rootPath, entryFileRelPath);
193
-
194
- try {
195
- const serverInfo = this._serverInfoMap.getOrCreate(path.basename(pkg.rootPath), {
196
- middlewares: [],
197
- clientInfos: []
198
- });
199
- if (serverInfo.server) {
200
- this._logger.log(`[${pkg.name}] 기존 서버 중지...`);
201
- await serverInfo.server.closeAsync();
202
- delete serverInfo.server;
203
- }
204
-
205
- this._logger.log(`[${pkg.name}] 서버 시작중...`);
206
-
207
- const serverMainPath = "file:///" + PathUtil.posix(entryFilePath) + "?update=" + Uuid.new().toString().replace(/-/g, "");
208
- const serverModule = await import(serverMainPath);
209
- serverInfo.server = serverModule.default as SdServiceServer | undefined;
210
- if (!serverInfo.server) {
211
- this._logger.error(`${entryFilePath}(0, 0): 'SdServiceServer'를 'export'해야 합니다.`);
212
- this._isServerRestarting = false;
213
- return;
214
- }
215
- serverInfo.server.devMiddlewares = serverInfo.middlewares;
216
- await Wait.until(() => serverInfo.server!.isOpen);
217
-
218
- this._logger.log(`[${pkg.name}] 서버가 시작되었습니다.`);
219
- }
220
- catch (err) {
221
- this._logger.error(`서버(${pkg.name}) 재시작중 오류 발생`, err);
222
- }
223
-
224
- this._isServerRestarting = false;
225
- }
226
-
227
- public async buildAsync(opt: { confFileRelPath: string; optNames: string[]; pkgs: string[] }): Promise<void> {
228
- this._logger.debug("프로젝트 설정 가져오기...");
229
- const config = await SdCliConfigUtil.loadConfigAsync(path.resolve(this._rootPath, opt.confFileRelPath), false, opt.optNames);
230
-
231
- this._logger.debug("패키지 목록 구성...");
232
- const pkgs = await this._getPackagesAsync(config, opt.pkgs);
233
-
234
- this._logger.debug("프로젝트 및 패키지 버전 설정...");
235
- await this._upgradeVersionAsync(pkgs);
236
-
237
- // 빌드
238
- await this._buildPkgsAsync(pkgs);
239
- }
240
-
241
- private async _buildPkgsAsync(pkgs: SdCliPackage[]): Promise<void> {
242
- this._logger.debug("빌드를 시작합니다...");
243
- const pkgNames = pkgs.map((item) => item.name);
244
- const buildCompletedPackageNames: string[] = [];
245
- const totalResultMap = new Map<string, ISdCliPackageBuildResult[]>();
246
- try {
247
- await pkgs.parallelAsync(async (pkg) => {
248
- await Wait.until(() => !pkg.allDependencies.some((dep) => pkgNames.includes(dep) && !buildCompletedPackageNames.includes(dep)));
249
-
250
- this._logger.debug(`[${pkg.name}] 빌드를 시작합니다...`);
251
- totalResultMap.set(pkg.name, await pkg.buildAsync());
252
- this._logger.debug(`[${pkg.name}] 빌드가 완료되었습니다.`);
253
-
254
- buildCompletedPackageNames.push(pkg.name);
255
- });
256
-
257
- this._loggingResults(totalResultMap);
258
- if (Array.from(totalResultMap.values()).mapMany().some((item) => item.severity === "error")) {
259
- throw new Error("빌드중 오류가 발생하였습니다.");
260
- }
261
- else {
262
- this._logger.info("모든 빌드가 완료되었습니다.");
263
- }
264
- }
265
- catch (err) {
266
- this._loggingResults(totalResultMap);
267
- throw err;
268
- }
269
- }
270
-
271
- public async publishAsync(opt: { noBuild: boolean; confFileRelPath: string; optNames: string[]; pkgs: string[] }): Promise<void> {
272
- this._logger.debug("프로젝트 설정 가져오기...");
273
- const config = await SdCliConfigUtil.loadConfigAsync(path.resolve(this._rootPath, opt.confFileRelPath), false, opt.optNames);
274
-
275
- if (opt.noBuild) {
276
- this._logger.warn("빌드하지 않고, 배포하는것은 상당히 위험합니다.");
277
- await this._waitSecMessageAsync("프로세스를 중지하려면, 'CTRL+C'를 누르세요.", 5);
278
- }
279
-
280
- // GIT 사용중일 경우, 커밋되지 않은 수정사항이 있는지 확인
281
- if (FsUtil.exists(path.resolve(process.cwd(), ".git"))) {
282
- this._logger.debug("GIT 커밋여부 확인...");
283
- const gitStatusResult = await SdProcess.spawnAsync("git status");
284
- if (gitStatusResult.includes("Changes") || gitStatusResult.includes("Untracked")) {
285
- throw new Error("커밋되지 않은 정보가 있습니다.\n" + gitStatusResult);
286
- }
287
- }
288
-
289
- this._logger.debug("패키지 목록 구성...");
290
- const pkgs = await this._getPackagesAsync(config, opt.pkgs);
291
-
292
- this._logger.debug("프로젝트 및 패키지 버전 설정...");
293
- await this._upgradeVersionAsync(pkgs);
294
-
295
- // 빌드
296
- if (!opt.noBuild) {
297
- // this._logger.debug("노드패키지 업데이트...");
298
- // await new SdCliNpm(this._rootPath).updateAsync();
299
-
300
- this._logger.debug("빌드를 시작합니다...");
301
- await this._buildPkgsAsync(pkgs);
302
- }
303
-
304
- // GIT 사용중일경우, 새 버전 커밋 및 TAG 생성
305
- if (FsUtil.exists(path.resolve(process.cwd(), ".git"))) {
306
- this._logger.debug("새 버전 커밋 및 TAG 생성...");
307
- await SdProcess.spawnAsync("git add .");
308
- await SdProcess.spawnAsync(`git commit -m "v${this._npmConfig.version}"`);
309
- await SdProcess.spawnAsync(`git tag -a "v${this._npmConfig.version}" -m "v${this._npmConfig.version}"`);
310
-
311
- this._logger.debug("새 버전 푸쉬...");
312
- await SdProcess.spawnAsync("git push");
313
- await SdProcess.spawnAsync("git push --tags");
314
- }
315
-
316
- this._logger.debug("배포 시작...");
317
- await pkgs.parallelAsync(async (pkg) => {
318
- this._logger.debug(`[${pkg.name}] 배포를 시작합니다...`);
319
- await pkg.publishAsync();
320
- this._logger.debug(`[${pkg.name}] 배포가 완료되었습니다.`);
321
- });
322
- this._logger.info(`모든 배포가 완료되었습니다. (v${this._npmConfig.version})`);
323
- }
324
-
325
- private async _upgradeVersionAsync(pkgs: SdCliPackage[]): Promise<void> {
326
- // 작업공간 package.json 버전 설정
327
- const newVersion = semver.inc(this._npmConfig.version, "patch")!;
328
- this._npmConfig.version = newVersion;
329
-
330
- const pkgNames = pkgs.map((item) => item.name);
331
-
332
- const updateDepVersion = (deps: Record<string, string> | undefined): void => {
333
- if (!deps) return;
334
- for (const depName of Object.keys(deps)) {
335
- if (pkgNames.includes(depName)) {
336
- deps[depName] = newVersion;
337
- }
338
- }
339
- };
340
- updateDepVersion(this._npmConfig.dependencies);
341
- updateDepVersion(this._npmConfig.optionalDependencies);
342
- updateDepVersion(this._npmConfig.devDependencies);
343
- updateDepVersion(this._npmConfig.peerDependencies);
344
-
345
- const npmConfigFilePath = path.resolve(this._rootPath, "package.json");
346
- await FsUtil.writeJsonAsync(npmConfigFilePath, this._npmConfig, { space: 2 });
347
-
348
- // 각 패키지 package.json 버전 설정
349
- await pkgs.parallelAsync(async (pkg) => {
350
- await pkg.setNewVersionAsync(newVersion, pkgNames);
351
- });
352
- }
353
-
354
- private async _waitSecMessageAsync(msg: string, sec: number): Promise<void> {
355
- for (let i = sec; i > 0; i--) {
356
- if (i !== sec) {
357
- process.stdout.cursorTo(0);
358
- }
359
- process.stdout.write(`${msg} ${i}`);
360
- await Wait.time(1000);
361
- }
362
-
363
- process.stdout.cursorTo(0);
364
- process.stdout.clearLine(0);
365
- }
366
-
367
- private async _getPackagesAsync(conf: ISdCliConfig, pkgs: string[]): Promise<SdCliPackage[]> {
368
- const pkgRootPaths = await this._npmConfig.workspaces?.mapManyAsync(async (item) => await FsUtil.globAsync(item));
369
- if (!pkgRootPaths) {
370
- throw new Error("최상위 'package.json'에서 'workspaces'를 찾을 수 없습니다.");
371
- }
372
-
373
- return pkgRootPaths.map((pkgRootPath) => {
374
- if (pkgs.length > 0 && !pkgs.includes(path.basename(pkgRootPath))) return undefined;
375
- const pkgConfig = conf.packages[path.basename(pkgRootPath)];
376
- if (!pkgConfig) return undefined;
377
- return new SdCliPackage(this._rootPath, pkgRootPath, pkgConfig);
378
- }).filterExists();
379
- }
380
-
381
- private _loggingResults(totalResultMap: Map<string, ISdCliPackageBuildResult[]>): void {
382
- const totalResults = Array.from(totalResultMap.entries())
383
- .mapMany((item) => item[1].map((item1) => ({
384
- pkgName: item[0],
385
- ...item1
386
- })))
387
- .orderBy((item) => `${item.pkgName}_${item.filePath}`);
388
-
389
- const warnings = totalResults
390
- .filter((item) => item.severity === "warning")
391
- .map((item) => `${SdCliBuildResultUtil.getMessage(item)} (${item.pkgName})`)
392
- .distinct();
393
-
394
- const errors = totalResults
395
- .filter((item) => item.severity === "error")
396
- .map((item) => `${SdCliBuildResultUtil.getMessage(item)} (${item.pkgName})`)
397
- .distinct();
398
-
399
- if (warnings.length > 0) {
400
- this._logger.warn(`경고 발생${os.EOL}`, warnings.join(os.EOL));
401
- }
402
- if (errors.length > 0) {
403
- this._logger.error(`오류 발생${os.EOL}`, errors.join(os.EOL));
404
- }
405
-
406
- if (errors.length > 0) {
407
- this._logger.error(`경고: ${warnings.length}건, 오류: ${errors.length}건`);
408
- }
409
- else if (warnings.length > 0) {
410
- this._logger.warn(`경고: ${warnings.length}건, 오류: ${errors.length}건`);
411
- }
412
- }
413
-
414
- private _loggingOpenClientHrefs(): void {
415
- const clientHrefs: string[] = [];
416
-
417
- const serverInfos = Array.from(this._serverInfoMap.values());
418
- for (const serverInfo of serverInfos) {
419
- if (!serverInfo.server) continue;
420
-
421
- const protocolStr = serverInfo.server.options.ssl ? "https" : "http";
422
- const portStr = serverInfo.server.options.port.toString();
423
-
424
- for (const clientInfo of serverInfo.clientInfos) {
425
- for (const platform of clientInfo.platforms) {
426
- if (platform === "web") {
427
- clientHrefs.push(`${protocolStr}://localhost:${portStr}/${clientInfo.pkgKey}/`);
428
- }
429
- else if (platform === "electron") {
430
- clientHrefs.push(`sd-cli run-electron ${clientInfo.pkgKey} http://localhost:${portStr}`);
431
- }
432
- else if (platform === "cordova") {
433
- for (const target of clientInfo.cordovaTargets) {
434
- if (target === "browser") {
435
- clientHrefs.push(`${protocolStr}://localhost:${portStr}/${clientInfo.pkgKey}/${platform}/`);
436
- }
437
- else {
438
- clientHrefs.push(`sd-cli run-cordova ${clientInfo.pkgKey} http://[IP]:${portStr}`);
439
- }
440
- }
441
- }
442
- }
443
- }
444
- }
445
- if (clientHrefs.length > 0) {
446
- this._logger.log(`오픈된 클라이언트:\n${clientHrefs.join("\n")}`);
447
- }
448
- }
449
- }
450
-
451
- interface IServerInfo {
452
- server?: SdServiceServer;
453
- middlewares: NextHandleFunction[];
454
- clientInfos: { pkgKey: string; platforms: string[]; cordovaTargets: string[] }[];
455
- }
1
+ import { FsUtil, Logger, PathUtil, SdProcess } from "@simplysm/sd-core-node";
2
+ import path from "path";
3
+ import { INpmConfig, ISdCliConfig, ISdCliPackageBuildResult } from "../commons";
4
+ import { SdCliPackage } from "../packages/SdCliPackage";
5
+ import { Uuid, Wait } from "@simplysm/sd-core-common";
6
+ import os from "os";
7
+ import { SdCliBuildResultUtil } from "../utils/SdCliBuildResultUtil";
8
+ import semver from "semver/preload";
9
+ import { SdCliConfigUtil } from "../utils/SdCliConfigUtil";
10
+ import { SdServiceServer } from "@simplysm/sd-service-server";
11
+ import { SdCliLocalUpdate } from "./SdCliLocalUpdate";
12
+ import url from "url";
13
+ import mime from "mime";
14
+ import { NextHandleFunction } from "connect";
15
+
16
+ export class SdCliWorkspace {
17
+ private readonly _logger = Logger.get(["simplysm", "sd-cli", this.constructor.name]);
18
+
19
+ private readonly _npmConfig: INpmConfig;
20
+ private readonly _serverInfoMap = new Map<string, IServerInfo>();
21
+
22
+ public constructor(private readonly _rootPath: string) {
23
+ const npmConfigFilePath = path.resolve(this._rootPath, "package.json");
24
+ this._npmConfig = FsUtil.readJson(npmConfigFilePath);
25
+ }
26
+
27
+ public async watchAsync(opt: { confFileRelPath: string; optNames: string[]; pkgs: string[] }): Promise<void> {
28
+ this._logger.debug("프로젝트 설정 가져오기...");
29
+ const config = await SdCliConfigUtil.loadConfigAsync(path.resolve(this._rootPath, opt.confFileRelPath), true, opt.optNames);
30
+
31
+ if (config.localUpdates) {
32
+ this._logger.debug("로컬 라이브러리 업데이트 변경감지 시작...");
33
+ await new SdCliLocalUpdate(this._rootPath).watchAsync({ confFileRelPath: opt.confFileRelPath });
34
+ }
35
+
36
+ this._logger.debug("패키지 목록 구성...");
37
+ const pkgs = await this._getPackagesAsync(config, opt.pkgs);
38
+
39
+ this._logger.debug("패키지 이벤트 설정...");
40
+ let changeCount = 0;
41
+ let changePkgs: SdCliPackage[] = [];
42
+ const totalResultMap = new Map<string, ISdCliPackageBuildResult[]>();
43
+ for (const pkg of pkgs) {
44
+ pkg
45
+ .on("change", () => {
46
+ if (changeCount === 0) {
47
+ this._logger.log("빌드를 시작합니다...");
48
+ }
49
+ changeCount++;
50
+ this._logger.debug(`[${pkg.name}] 빌드를 시작합니다...`);
51
+ })
52
+ .on("complete", (results) => {
53
+ changePkgs.push(pkg);
54
+
55
+ this._logger.debug(`[${pkg.name}] 빌드가 완료되었습니다.`);
56
+ totalResultMap.set(pkg.name, results);
57
+
58
+ setTimeout(async () => {
59
+ changeCount--;
60
+ if (changeCount === 0) {
61
+ const currChangePkgs = [...changePkgs].distinct(true);
62
+ changePkgs = [];
63
+
64
+ for (const changePkg of currChangePkgs) {
65
+ if (changePkg.config.type === "server" && !results.some((item) => item.severity === "error")) {
66
+ await this._restartServerAsync(changePkg);
67
+ }
68
+
69
+ if (changePkg.config.type === "client") {
70
+ if (typeof changePkg.config.server === "string") {
71
+ const serverInfo = this._serverInfoMap.get(changePkg.config.server);
72
+ serverInfo?.server?.broadcastReload();
73
+ }
74
+ else {
75
+ const serverInfo = this._serverInfoMap.get("PORT:" + changePkg.config.server.port);
76
+ serverInfo?.server?.broadcastReload();
77
+ }
78
+ }
79
+ }
80
+
81
+ this._loggingResults(totalResultMap);
82
+ this._loggingOpenClientHrefs();
83
+ this._logger.info("모든 빌드가 완료되었습니다.");
84
+ }
85
+ }, 500);
86
+ });
87
+ }
88
+
89
+
90
+ if (changeCount === 0) {
91
+ this._logger.log("빌드를 시작합니다...");
92
+ }
93
+ changeCount++;
94
+
95
+ try {
96
+ const pkgNames = pkgs.map((item) => item.name);
97
+ const buildCompletedPackageNames: string[] = [];
98
+ await pkgs.parallelAsync(async (pkg) => {
99
+ await Wait.until(() => !pkg.allDependencies.some((dep) => pkgNames.includes(dep) && !buildCompletedPackageNames.includes(dep)));
100
+ if (pkg.config.type === "client") {
101
+ await pkg.watchAsync();
102
+
103
+ const pkgMiddleware: NextHandleFunction = (req, res, next) => {
104
+ if (req.method === "GET") {
105
+ const urlObj = url.parse(req.url!, true, false);
106
+ const urlPathChain = decodeURI(urlObj.pathname!.slice(1)).split("/");
107
+ if (urlPathChain[0] === pkg.name.split("/").last()!) {
108
+ let targetFilePath = path.resolve(pkg.rootPath, "dist", ...urlPathChain.slice(1));
109
+ targetFilePath = FsUtil.exists(targetFilePath) && FsUtil.stat(targetFilePath).isDirectory() ? path.resolve(targetFilePath, "index.html") : targetFilePath;
110
+
111
+ if (FsUtil.exists(targetFilePath) && !path.basename(targetFilePath).startsWith(".")) {
112
+ const fileStream = FsUtil.createReadStream(targetFilePath);
113
+ const targetFileSize = FsUtil.lstat(targetFilePath).size;
114
+
115
+ fileStream.on("open", () => {
116
+ res.setHeader("Content-Length", targetFileSize);
117
+ res.setHeader("Content-Type", mime.getType(targetFilePath)!);
118
+ res.writeHead(200);
119
+ });
120
+ fileStream.pipe(res);
121
+ return;
122
+ }
123
+ }
124
+ }
125
+
126
+ next();
127
+ };
128
+
129
+ if (typeof pkg.config.server === "string") {
130
+ const serverInfo = this._serverInfoMap.getOrCreate(pkg.config.server, { middlewares: [], clientInfos: [] });
131
+ serverInfo.middlewares.push(pkgMiddleware);
132
+ serverInfo.clientInfos.push({
133
+ pkgKey: pkg.name.split("/").last()!,
134
+ platforms: pkg.config.builder ? Object.keys(pkg.config.builder) : ["web"],
135
+ cordovaTargets: pkg.config.builder?.cordova?.target ? Object.keys(pkg.config.builder.cordova.target) : ["browser"]
136
+ });
137
+ }
138
+ else { // DEV SERVER
139
+ const serverInfo = this._serverInfoMap.getOrCreate("PORT:" + pkg.config.server.port, {
140
+ middlewares: [],
141
+ clientInfos: []
142
+ });
143
+ if (serverInfo.server === undefined) {
144
+ const server = new SdServiceServer({
145
+ rootPath: process.cwd(),
146
+ services: [],
147
+ port: pkg.config.server.port
148
+ });
149
+ await server.listenAsync();
150
+ serverInfo.server = server;
151
+ serverInfo.server.devMiddlewares = [pkgMiddleware];
152
+ serverInfo.clientInfos.push({
153
+ pkgKey: pkg.name.split("/").last()!,
154
+ platforms: pkg.config.builder ? Object.keys(pkg.config.builder) : ["web"],
155
+ cordovaTargets: pkg.config.builder?.cordova?.target ? Object.keys(pkg.config.builder.cordova.target) : ["browser"]
156
+ });
157
+ }
158
+ }
159
+ }
160
+ else {
161
+ await pkg.watchAsync();
162
+ }
163
+ buildCompletedPackageNames.push(pkg.name);
164
+ });
165
+ }
166
+ catch (err) {
167
+ this._loggingResults(totalResultMap);
168
+ this._loggingOpenClientHrefs();
169
+ throw err;
170
+ }
171
+
172
+ changeCount--;
173
+ if (changeCount === 0) {
174
+ this._loggingResults(totalResultMap);
175
+ this._loggingOpenClientHrefs();
176
+ this._logger.info("모든 빌드가 완료되었습니다.");
177
+ }
178
+ }
179
+
180
+ private _isServerRestarting = false;
181
+
182
+ private async _restartServerAsync(pkg: SdCliPackage): Promise<void> {
183
+ await Wait.until(() => !this._isServerRestarting);
184
+
185
+ this._isServerRestarting = true;
186
+ const entryFileRelPath = pkg.main;
187
+ if (entryFileRelPath === undefined) {
188
+ this._logger.error(`서버패키지(${pkg.name})의 'package.json'에서 'main'필드를 찾을 수 없습니다.`);
189
+ this._isServerRestarting = false;
190
+ return;
191
+ }
192
+ const entryFilePath = path.resolve(pkg.rootPath, entryFileRelPath);
193
+
194
+ try {
195
+ const serverInfo = this._serverInfoMap.getOrCreate(path.basename(pkg.rootPath), {
196
+ middlewares: [],
197
+ clientInfos: []
198
+ });
199
+ if (serverInfo.server) {
200
+ this._logger.log(`[${pkg.name}] 기존 서버 중지...`);
201
+ await serverInfo.server.closeAsync();
202
+ delete serverInfo.server;
203
+ }
204
+
205
+ this._logger.log(`[${pkg.name}] 서버 시작중...`);
206
+
207
+ const serverMainPath = "file:///" + PathUtil.posix(entryFilePath) + "?update=" + Uuid.new().toString().replace(/-/g, "");
208
+ const serverModule = await import(serverMainPath);
209
+ serverInfo.server = serverModule.default as SdServiceServer | undefined;
210
+ if (!serverInfo.server) {
211
+ this._logger.error(`${entryFilePath}(0, 0): 'SdServiceServer'를 'export'해야 합니다.`);
212
+ this._isServerRestarting = false;
213
+ return;
214
+ }
215
+ serverInfo.server.devMiddlewares = serverInfo.middlewares;
216
+ await Wait.until(() => serverInfo.server!.isOpen);
217
+
218
+ this._logger.log(`[${pkg.name}] 서버가 시작되었습니다.`);
219
+ }
220
+ catch (err) {
221
+ this._logger.error(`서버(${pkg.name}) 재시작중 오류 발생`, err);
222
+ }
223
+
224
+ this._isServerRestarting = false;
225
+ }
226
+
227
+ public async buildAsync(opt: { confFileRelPath: string; optNames: string[]; pkgs: string[] }): Promise<void> {
228
+ this._logger.debug("프로젝트 설정 가져오기...");
229
+ const config = await SdCliConfigUtil.loadConfigAsync(path.resolve(this._rootPath, opt.confFileRelPath), false, opt.optNames);
230
+
231
+ this._logger.debug("패키지 목록 구성...");
232
+ const pkgs = await this._getPackagesAsync(config, opt.pkgs);
233
+
234
+ this._logger.debug("프로젝트 및 패키지 버전 설정...");
235
+ await this._upgradeVersionAsync(pkgs);
236
+
237
+ // 빌드
238
+ await this._buildPkgsAsync(pkgs);
239
+ }
240
+
241
+ private async _buildPkgsAsync(pkgs: SdCliPackage[]): Promise<void> {
242
+ this._logger.debug("빌드를 시작합니다...");
243
+ const pkgNames = pkgs.map((item) => item.name);
244
+ const buildCompletedPackageNames: string[] = [];
245
+ const totalResultMap = new Map<string, ISdCliPackageBuildResult[]>();
246
+ try {
247
+ await pkgs.parallelAsync(async (pkg) => {
248
+ await Wait.until(() => !pkg.allDependencies.some((dep) => pkgNames.includes(dep) && !buildCompletedPackageNames.includes(dep)));
249
+
250
+ this._logger.debug(`[${pkg.name}] 빌드를 시작합니다...`);
251
+ totalResultMap.set(pkg.name, await pkg.buildAsync());
252
+ this._logger.debug(`[${pkg.name}] 빌드가 완료되었습니다.`);
253
+
254
+ buildCompletedPackageNames.push(pkg.name);
255
+ });
256
+
257
+ this._loggingResults(totalResultMap);
258
+ if (Array.from(totalResultMap.values()).mapMany().some((item) => item.severity === "error")) {
259
+ throw new Error("빌드중 오류가 발생하였습니다.");
260
+ }
261
+ else {
262
+ this._logger.info("모든 빌드가 완료되었습니다.");
263
+ }
264
+ }
265
+ catch (err) {
266
+ this._loggingResults(totalResultMap);
267
+ throw err;
268
+ }
269
+ }
270
+
271
+ public async publishAsync(opt: { noBuild: boolean; confFileRelPath: string; optNames: string[]; pkgs: string[] }): Promise<void> {
272
+ this._logger.debug("프로젝트 설정 가져오기...");
273
+ const config = await SdCliConfigUtil.loadConfigAsync(path.resolve(this._rootPath, opt.confFileRelPath), false, opt.optNames);
274
+
275
+ if (opt.noBuild) {
276
+ this._logger.warn("빌드하지 않고, 배포하는것은 상당히 위험합니다.");
277
+ await this._waitSecMessageAsync("프로세스를 중지하려면, 'CTRL+C'를 누르세요.", 5);
278
+ }
279
+
280
+ // GIT 사용중일 경우, 커밋되지 않은 수정사항이 있는지 확인
281
+ if (FsUtil.exists(path.resolve(process.cwd(), ".git"))) {
282
+ this._logger.debug("GIT 커밋여부 확인...");
283
+ const gitStatusResult = await SdProcess.spawnAsync("git status");
284
+ if (gitStatusResult.includes("Changes") || gitStatusResult.includes("Untracked")) {
285
+ throw new Error("커밋되지 않은 정보가 있습니다.\n" + gitStatusResult);
286
+ }
287
+ }
288
+
289
+ this._logger.debug("패키지 목록 구성...");
290
+ const pkgs = await this._getPackagesAsync(config, opt.pkgs);
291
+
292
+ this._logger.debug("프로젝트 및 패키지 버전 설정...");
293
+ await this._upgradeVersionAsync(pkgs);
294
+
295
+ // 빌드
296
+ if (!opt.noBuild) {
297
+ // this._logger.debug("노드패키지 업데이트...");
298
+ // await new SdCliNpm(this._rootPath).updateAsync();
299
+
300
+ this._logger.debug("빌드를 시작합니다...");
301
+ await this._buildPkgsAsync(pkgs);
302
+ }
303
+
304
+ // GIT 사용중일경우, 새 버전 커밋 및 TAG 생성
305
+ if (FsUtil.exists(path.resolve(process.cwd(), ".git"))) {
306
+ this._logger.debug("새 버전 커밋 및 TAG 생성...");
307
+ await SdProcess.spawnAsync("git add .");
308
+ await SdProcess.spawnAsync(`git commit -m "v${this._npmConfig.version}"`);
309
+ await SdProcess.spawnAsync(`git tag -a "v${this._npmConfig.version}" -m "v${this._npmConfig.version}"`);
310
+
311
+ this._logger.debug("새 버전 푸쉬...");
312
+ await SdProcess.spawnAsync("git push");
313
+ await SdProcess.spawnAsync("git push --tags");
314
+ }
315
+
316
+ this._logger.debug("배포 시작...");
317
+ await pkgs.parallelAsync(async (pkg) => {
318
+ this._logger.debug(`[${pkg.name}] 배포를 시작합니다...`);
319
+ await pkg.publishAsync();
320
+ this._logger.debug(`[${pkg.name}] 배포가 완료되었습니다.`);
321
+ });
322
+ this._logger.info(`모든 배포가 완료되었습니다. (v${this._npmConfig.version})`);
323
+ }
324
+
325
+ private async _upgradeVersionAsync(pkgs: SdCliPackage[]): Promise<void> {
326
+ // 작업공간 package.json 버전 설정
327
+ const newVersion = semver.inc(this._npmConfig.version, "patch")!;
328
+ this._npmConfig.version = newVersion;
329
+
330
+ const pkgNames = pkgs.map((item) => item.name);
331
+
332
+ const updateDepVersion = (deps: Record<string, string> | undefined): void => {
333
+ if (!deps) return;
334
+ for (const depName of Object.keys(deps)) {
335
+ if (pkgNames.includes(depName)) {
336
+ deps[depName] = newVersion;
337
+ }
338
+ }
339
+ };
340
+ updateDepVersion(this._npmConfig.dependencies);
341
+ updateDepVersion(this._npmConfig.optionalDependencies);
342
+ updateDepVersion(this._npmConfig.devDependencies);
343
+ updateDepVersion(this._npmConfig.peerDependencies);
344
+
345
+ const npmConfigFilePath = path.resolve(this._rootPath, "package.json");
346
+ await FsUtil.writeJsonAsync(npmConfigFilePath, this._npmConfig, { space: 2 });
347
+
348
+ // 각 패키지 package.json 버전 설정
349
+ await pkgs.parallelAsync(async (pkg) => {
350
+ await pkg.setNewVersionAsync(newVersion, pkgNames);
351
+ });
352
+ }
353
+
354
+ private async _waitSecMessageAsync(msg: string, sec: number): Promise<void> {
355
+ for (let i = sec; i > 0; i--) {
356
+ if (i !== sec) {
357
+ process.stdout.cursorTo(0);
358
+ }
359
+ process.stdout.write(`${msg} ${i}`);
360
+ await Wait.time(1000);
361
+ }
362
+
363
+ process.stdout.cursorTo(0);
364
+ process.stdout.clearLine(0);
365
+ }
366
+
367
+ private async _getPackagesAsync(conf: ISdCliConfig, pkgs: string[]): Promise<SdCliPackage[]> {
368
+ const pkgRootPaths = await this._npmConfig.workspaces?.mapManyAsync(async (item) => await FsUtil.globAsync(item));
369
+ if (!pkgRootPaths) {
370
+ throw new Error("최상위 'package.json'에서 'workspaces'를 찾을 수 없습니다.");
371
+ }
372
+
373
+ return pkgRootPaths.map((pkgRootPath) => {
374
+ if (pkgs.length > 0 && !pkgs.includes(path.basename(pkgRootPath))) return undefined;
375
+ const pkgConfig = conf.packages[path.basename(pkgRootPath)];
376
+ if (!pkgConfig) return undefined;
377
+ return new SdCliPackage(this._rootPath, pkgRootPath, pkgConfig);
378
+ }).filterExists();
379
+ }
380
+
381
+ private _loggingResults(totalResultMap: Map<string, ISdCliPackageBuildResult[]>): void {
382
+ const totalResults = Array.from(totalResultMap.entries())
383
+ .mapMany((item) => item[1].map((item1) => ({
384
+ pkgName: item[0],
385
+ ...item1
386
+ })))
387
+ .orderBy((item) => `${item.pkgName}_${item.filePath}`);
388
+
389
+ const warnings = totalResults
390
+ .filter((item) => item.severity === "warning")
391
+ .map((item) => `${SdCliBuildResultUtil.getMessage(item)} (${item.pkgName})`)
392
+ .distinct();
393
+
394
+ const errors = totalResults
395
+ .filter((item) => item.severity === "error")
396
+ .map((item) => `${SdCliBuildResultUtil.getMessage(item)} (${item.pkgName})`)
397
+ .distinct();
398
+
399
+ if (warnings.length > 0) {
400
+ this._logger.warn(`경고 발생${os.EOL}`, warnings.join(os.EOL));
401
+ }
402
+ if (errors.length > 0) {
403
+ this._logger.error(`오류 발생${os.EOL}`, errors.join(os.EOL));
404
+ }
405
+
406
+ if (errors.length > 0) {
407
+ this._logger.error(`경고: ${warnings.length}건, 오류: ${errors.length}건`);
408
+ }
409
+ else if (warnings.length > 0) {
410
+ this._logger.warn(`경고: ${warnings.length}건, 오류: ${errors.length}건`);
411
+ }
412
+ }
413
+
414
+ private _loggingOpenClientHrefs(): void {
415
+ const clientHrefs: string[] = [];
416
+
417
+ const serverInfos = Array.from(this._serverInfoMap.values());
418
+ for (const serverInfo of serverInfos) {
419
+ if (!serverInfo.server) continue;
420
+
421
+ const protocolStr = serverInfo.server.options.ssl ? "https" : "http";
422
+ const portStr = serverInfo.server.options.port.toString();
423
+
424
+ for (const clientInfo of serverInfo.clientInfos) {
425
+ for (const platform of clientInfo.platforms) {
426
+ if (platform === "web") {
427
+ clientHrefs.push(`${protocolStr}://localhost:${portStr}/${clientInfo.pkgKey}/`);
428
+ }
429
+ else if (platform === "electron") {
430
+ clientHrefs.push(`sd-cli run-electron ${clientInfo.pkgKey} http://localhost:${portStr}`);
431
+ }
432
+ else if (platform === "cordova") {
433
+ for (const target of clientInfo.cordovaTargets) {
434
+ if (target === "browser") {
435
+ clientHrefs.push(`${protocolStr}://localhost:${portStr}/${clientInfo.pkgKey}/${platform}/`);
436
+ }
437
+ else {
438
+ clientHrefs.push(`sd-cli run-cordova ${target} ${clientInfo.pkgKey} http://[IP]:${portStr}`);
439
+ }
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+ if (clientHrefs.length > 0) {
446
+ this._logger.log(`오픈된 클라이언트:\n${clientHrefs.join("\n")}`);
447
+ }
448
+ }
449
+ }
450
+
451
+ interface IServerInfo {
452
+ server?: SdServiceServer;
453
+ middlewares: NextHandleFunction[];
454
+ clientInfos: { pkgKey: string; platforms: string[]; cordovaTargets: string[] }[];
455
+ }