sonamu 0.7.1 → 0.7.2

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.
Files changed (35) hide show
  1. package/dist/ai/agents/types.d.ts +4 -3
  2. package/dist/ai/agents/types.d.ts.map +1 -1
  3. package/dist/ai/agents/types.js +1 -1
  4. package/dist/api/config.d.ts +4 -2
  5. package/dist/api/config.d.ts.map +1 -1
  6. package/dist/api/config.js +5 -2
  7. package/dist/api/decorators.d.ts.map +1 -1
  8. package/dist/api/decorators.js +3 -2
  9. package/dist/bin/cli.js +13 -13
  10. package/dist/bin/{hot-hook-register.d.ts → hmr-hook-register.d.ts} +3 -3
  11. package/dist/bin/hmr-hook-register.d.ts.map +1 -0
  12. package/dist/bin/{hot-hook-register.js → hmr-hook-register.js} +5 -5
  13. package/dist/bin/ts-loader-register.d.ts +2 -0
  14. package/dist/bin/ts-loader-register.d.ts.map +1 -0
  15. package/dist/bin/ts-loader-register.js +34 -0
  16. package/dist/database/base-model.d.ts +2 -34
  17. package/dist/database/base-model.d.ts.map +1 -1
  18. package/dist/database/base-model.js +3 -170
  19. package/dist/syncer/syncer.js +3 -3
  20. package/dist/template/zod-converter.js +4 -2
  21. package/dist/types/types.d.ts +2 -2
  22. package/package.json +8 -8
  23. package/src/ai/agents/types.ts +6 -3
  24. package/src/api/config.ts +16 -5
  25. package/src/api/decorators.ts +2 -1
  26. package/src/bin/cli.ts +13 -13
  27. package/src/bin/{hot-hook-register.ts → hmr-hook-register.ts} +4 -4
  28. package/src/bin/{loader-register.ts → ts-loader-register.ts} +2 -2
  29. package/src/database/base-model.ts +5 -236
  30. package/src/syncer/syncer.ts +2 -2
  31. package/src/template/zod-converter.ts +3 -1
  32. package/dist/bin/hot-hook-register.d.ts.map +0 -1
  33. package/dist/bin/loader-register.d.ts +0 -2
  34. package/dist/bin/loader-register.d.ts.map +0 -1
  35. package/dist/bin/loader-register.js +0 -34
package/src/bin/cli.ts CHANGED
@@ -100,12 +100,12 @@ async function sync() {
100
100
  * pnpm dev 하면 실행되는 함수입니다.
101
101
  * 프로젝트에 대해 HMR 지원하는 개발 서버를 띄워줍니다.
102
102
  *
103
- * TypeScript를 바로 실행할 수 있도록 @sonamu-kit/loader를,
104
- * HMR을 지원하기 위해 @sonamu-kit/hot-hook을 import하며,
103
+ * TypeScript를 바로 실행할 수 있도록 @sonamu-kit/ts-loader를,
104
+ * HMR을 지원하기 위해 @sonamu-kit/hmr-hook을 import하며,
105
105
  * 소스맵 지원을 위해 --enable-source-maps 플래그를 포함하여 실행합니다.
106
106
  *
107
- * 이때 @sonamu-kit/loader와 @sonamu-kit/hot-hook는 sonamu가 자체적으로 가지고 있는 dependency입니다.
108
- * 또한 실행에 사용하는 @sonamu-kit/hot-runner도 마찬가지로 sonamu가 자체적으로 가지고 있는 dependency입니다.
107
+ * 이때 @sonamu-kit/ts-loader와 @sonamu-kit/hmr-hook는 sonamu가 자체적으로 가지고 있는 dependency입니다.
108
+ * 또한 실행에 사용하는 @sonamu-kit/hmr-runner도 마찬가지로 sonamu가 자체적으로 가지고 있는 dependency입니다.
109
109
  * 따라서 사용자 프로젝트에서는 이 세 패키지를 직접 설치할 필요가 없습니다.
110
110
  *
111
111
  * Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
@@ -116,10 +116,10 @@ async function dev() {
116
116
 
117
117
  console.log(chalk.yellow.bold("🚀 Starting Sonamu dev server...\n"));
118
118
 
119
- // 이 sonamu 패키지가 dependencies로 가지고 있는 @sonamu-kit/hot-runner의 bin/run.js를 사용합니다.
120
- // 이 경로(/bin/run.js)는 @sonamu-kit/hot-runner의 package.json의 bin 필드에 명시되어 있는 그것과 같습니다.
119
+ // 이 sonamu 패키지가 dependencies로 가지고 있는 @sonamu-kit/hmr-runner의 bin/run.js를 사용합니다.
120
+ // 이 경로(/bin/run.js)는 @sonamu-kit/hmr-runner의 package.json의 bin 필드에 명시되어 있는 그것과 같습니다.
121
121
  const hotRunnerBinPath = createRequire(import.meta.url).resolve(
122
- "@sonamu-kit/hot-runner/bin/run.js",
122
+ "@sonamu-kit/hmr-runner/bin/run.js",
123
123
  );
124
124
 
125
125
  const serverProcess = spawn(
@@ -127,8 +127,8 @@ async function dev() {
127
127
  [
128
128
  hotRunnerBinPath, // 이렇게 해서 hot-runner를 실행하구요
129
129
  "--clear-screen=false", // 이하 hot-runner에게 넘겨줄 인자들입니다.
130
- "--node-args=--import=sonamu/loader-register", // TypeScript 서포트를 위한 로더,
131
- "--node-args=--import=sonamu/hot-hook-register", // HMR을 지원하기 위한 hot-hook,
130
+ "--node-args=--import=sonamu/ts-loader-register", // TypeScript 서포트를 위한 로더,
131
+ "--node-args=--import=sonamu/hmr-hook-register", // HMR을 지원하기 위한 hook,
132
132
  "--node-args=--enable-source-maps", // 그리고 소스맵 지원을 위한 플래그입니다.
133
133
  "--on-key=r:restart:Restart server", // r 누르면 서버 재시작하게 해줘요.
134
134
  `--on-key=f:shell(rm ${path.join(apiRoot, "sonamu.lock")}):restart:Force restart`, // f 누르면 sonamu.lock 파일을 지우고 서버 재시작하게 해줘요.
@@ -144,7 +144,7 @@ async function dev() {
144
144
  ...process.env,
145
145
  NODE_ENV: "development",
146
146
  HOT: "yes", // 얘가 있어야 HMR이 활성화됩니다.
147
- API_ROOT_PATH: apiRoot, // 이 경로가 hot-hook의 루트 디렉토리가 됩니다.
147
+ API_ROOT_PATH: apiRoot, // 이 경로가 hmr-hook의 루트 디렉토리가 됩니다.
148
148
  },
149
149
  },
150
150
  );
@@ -481,14 +481,14 @@ async function ui() {
481
481
  return;
482
482
  }
483
483
 
484
- // UI를 별도 프로세스로 실행 (hot-hook 활성화)
484
+ // UI를 별도 프로세스로 실행 (hmr-hook 활성화)
485
485
  const uiProcess = spawn(
486
486
  process.execPath,
487
487
  [
488
488
  "--import",
489
- "sonamu/loader-register",
489
+ "sonamu/ts-loader-register",
490
490
  "--import",
491
- "sonamu/hot-hook-register",
491
+ "sonamu/hmr-hook-register",
492
492
  "--enable-source-maps",
493
493
  "--no-warnings",
494
494
  uiNodePath,
@@ -1,22 +1,22 @@
1
1
  /**
2
- * hot-hook 초기화하는 모듈입니다.
2
+ * hmr-hook 초기화하는 모듈입니다.
3
3
  *
4
4
  * 이 파일은 --import 플래그로 프로세스 시작 시 로드되어야 합니다.
5
5
  *
6
6
  * 환경변수:
7
7
  * - API_ROOT_PATH: 사용자 프로젝트의 API 루트 경로
8
- * - HOT: 'yes'일 때만 hot-hook 활성화
8
+ * - HOT: 'yes'일 때만 hmr-hook 활성화
9
9
  */
10
10
 
11
11
  if (process.env.HOT === "yes" && process.env.API_ROOT_PATH) {
12
- const { hot } = await import("@sonamu-kit/hot-hook");
12
+ const { hot } = await import("@sonamu-kit/hmr-hook");
13
13
 
14
14
  await hot.init({
15
15
  rootDirectory: process.env.API_ROOT_PATH, // 이 친구가 프로젝트 API 경로로 잘 설정되어야 아래 바운더리가 작동합니다.
16
16
  boundaries: [`./src/**/*.ts`], // 프로젝트의 이 친구들이 바운더리가 됩니다.
17
17
  });
18
18
 
19
- console.log("🔥 Hot-hook initialized");
19
+ console.log("🔥 HMR-hook initialized");
20
20
  }
21
21
 
22
22
  export {};
@@ -4,7 +4,7 @@ import { exists } from "../utils/fs-utils.js";
4
4
  import { findApiRootPath } from "../utils/utils.js";
5
5
 
6
6
  /**
7
- * @sonamu-kit/loader/loader를 등록하는 스크립트입니다.
7
+ * @sonamu-kit/ts-loader/loader를 등록하는 스크립트입니다.
8
8
  * 이 스크립트는 sonamu cli로 dev 실행할 때 --import로 실행됩니다.
9
9
  */
10
10
  async function setupSwcConfig() {
@@ -33,6 +33,6 @@ async function setupSwcConfig() {
33
33
  // swc 설정 파일 경로를 환경 변수로 설정
34
34
  await setupSwcConfig();
35
35
 
36
- register("@sonamu-kit/loader/loader", {
36
+ register("@sonamu-kit/ts-loader/loader", {
37
37
  parentURL: import.meta.url,
38
38
  });
@@ -1,10 +1,9 @@
1
- import assert from "assert";
2
- import inflection from "inflection";
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: Puri의 타입은 개별 모델에서 확정되므로 BaseModel에서는 any를 허용함 */
2
+
3
3
  import type { Knex } from "knex";
4
- import { group, isObject, omit, set, unique } from "radashi";
4
+ import { group, isObject, omit, set } from "radashi";
5
5
  import { Sonamu } from "../api";
6
- import { type DatabaseSchemaExtend, isCustomJoinClause, type SubsetQuery } from "../types/types";
7
- import type { BaseListParams } from "../utils/model";
6
+ import type { DatabaseSchemaExtend } from "../types/types";
8
7
  import { getJoinTables, getTableNamesFromWhere } from "../utils/sql-parser";
9
8
  import { chunk } from "../utils/utils";
10
9
  import type {
@@ -56,7 +55,7 @@ export class BaseModelClass<
56
55
 
57
56
  // 트랜잭션이 없으면 새로운 PuriWrapper 반환
58
57
  const db = this.getDB(which);
59
- return new PuriWrapper(db, this.getUpsertBuilder());
58
+ return new PuriWrapper(db, new UpsertBuilder());
60
59
  }
61
60
 
62
61
  async destroy() {
@@ -365,236 +364,6 @@ export class BaseModelClass<
365
364
  return hydrated;
366
365
  }) as T[];
367
366
  }
368
-
369
- // Legacy SubsetQuery 실행 (Puri 도입 전 호환용)
370
- async runSubsetQuery<T extends BaseListParams, U extends string>({
371
- params,
372
- baseTable,
373
- subset,
374
- subsetQuery,
375
- build,
376
- afterBuild,
377
- debug,
378
- db: _db,
379
- optimizeCountQuery,
380
- }: {
381
- subset: U;
382
- params: T;
383
- subsetQuery: SubsetQuery;
384
- build: (buildParams: {
385
- qb: Knex.QueryBuilder;
386
- db: Knex;
387
- select: (string | Knex.Raw)[];
388
- joins: SubsetQuery["joins"];
389
- virtual: string[];
390
- }) => Knex.QueryBuilder;
391
- afterBuild?: (buildParams: {
392
- qb: Knex.QueryBuilder;
393
- db: Knex;
394
- select: (string | Knex.Raw)[];
395
- joins: SubsetQuery["joins"];
396
- virtual: string[];
397
- }) => Knex.QueryBuilder;
398
- baseTable?: string;
399
- debug?: boolean | "list" | "count";
400
- db?: Knex;
401
- optimizeCountQuery?: boolean;
402
- }): Promise<{
403
- // biome-ignore lint/suspicious/noExplicitAny: Puri 도입 전까지 any로 유지
404
- rows: any[];
405
- total?: number | undefined;
406
- subsetQuery: SubsetQuery;
407
- qb: Knex.QueryBuilder;
408
- }> {
409
- const chalk = (await import("chalk")).default;
410
- const SqlParser = (await import("node-sql-parser")).default;
411
- const { getTableName, getTableNamesFromWhere } = await import("../utils/sql-parser");
412
-
413
- const db = _db ?? this.getDB(subset.startsWith("A") ? "w" : "r");
414
- baseTable = baseTable ?? inflection.pluralize(inflection.underscore(this.modelName));
415
- const queryMode = params.queryMode ?? (params.id !== undefined ? "list" : "both");
416
-
417
- const { select, virtual, joins, loaders } = subsetQuery;
418
- const qb = build({
419
- qb: db.from(baseTable),
420
- db,
421
- select,
422
- joins,
423
- virtual,
424
- });
425
-
426
- const applyJoinClause = (qb: Knex.QueryBuilder, joins: SubsetQuery["joins"]) => {
427
- joins.forEach((join) => {
428
- if (join.join === "inner") {
429
- qb.innerJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
430
- } else if (join.join === "outer") {
431
- qb.leftOuterJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
432
- }
433
- });
434
- };
435
-
436
- // countQuery
437
- const total = await (async () => {
438
- if (queryMode === "list") {
439
- return undefined;
440
- }
441
-
442
- const clonedQb = qb.clone().clear("order").clear("offset").clear("limit");
443
- const parser = new SqlParser.Parser();
444
-
445
- if (optimizeCountQuery) {
446
- const parsedQuery = parser.astify(clonedQb.toQuery(), {
447
- database: Sonamu.config.database.database,
448
- });
449
- const tables = getTableNamesFromWhere(parsedQuery);
450
- const needToJoin = unique(
451
- tables.flatMap((table) => table.split("__").map((t) => inflection.pluralize(t))),
452
- );
453
- applyJoinClause(
454
- clonedQb,
455
- joins.filter((j) => needToJoin.includes(j.table)),
456
- );
457
- } else {
458
- applyJoinClause(clonedQb, joins);
459
- }
460
-
461
- const processedQb = afterBuild?.({ qb: clonedQb, db, select, joins, virtual }) ?? clonedQb;
462
-
463
- const parsedQuery = parser.astify(processedQb.toQuery(), {
464
- database: Sonamu.config.database.database,
465
- });
466
- const q = Array.isArray(parsedQuery) ? parsedQuery[0] : parsedQuery;
467
- if (q.type !== "select") {
468
- throw new Error("Invalid query");
469
- }
470
-
471
- const countQuery =
472
- q.distinct !== null
473
- ? clonedQb
474
- .clear("select")
475
- .select(
476
- db.raw(
477
- `COUNT(DISTINCT \`${getTableName(q.columns[0].expr)}\`.\`${q.columns[0].expr.column}\`) as total`,
478
- ),
479
- )
480
- .first()
481
- : clonedQb.clear("select").count("*", { as: "total" }).first();
482
- const countRow: { total?: number } = await countQuery;
483
-
484
- if (debug === true || debug === "count") {
485
- console.debug("DEBUG: count query", chalk.blue(countQuery.toQuery().toString()));
486
- }
487
-
488
- return countRow?.total ?? 0;
489
- })();
490
-
491
- // listQuery
492
- const rows = await (async () => {
493
- if (queryMode === "count") {
494
- return [];
495
- }
496
-
497
- if (params.num !== 0) {
498
- assert(params.num);
499
- qb.limit(params.num);
500
- qb.offset(params.num * ((params.page ?? 1) - 1));
501
- }
502
-
503
- const clonedQb = qb.clone().select(select);
504
- applyJoinClause(clonedQb, joins);
505
-
506
- const listQuery = afterBuild?.({ qb: clonedQb, db, select, joins, virtual }) ?? clonedQb;
507
-
508
- let rows = await listQuery;
509
- if (debug === true || debug === "list") {
510
- console.debug("DEBUG: list query", chalk.blue(listQuery.toQuery().toString()));
511
- }
512
-
513
- rows = await this.useLoaders(db, rows, loaders);
514
- rows = this.hydrate(rows);
515
- return rows;
516
- })();
517
-
518
- return { rows, total, subsetQuery, qb };
519
- }
520
-
521
- // Legacy Loader 처리 (Puri 도입 전 호환용)
522
- async useLoaders(db: Knex, rows: UnknownDBRecord[], loaders: SubsetQuery["loaders"]) {
523
- if (loaders.length === 0) {
524
- return rows;
525
- }
526
-
527
- for (const loader of loaders) {
528
- let subQ: Knex.QueryBuilder;
529
- let subRows: UnknownDBRecord[];
530
- let toCol: string;
531
-
532
- const fromIds = rows.map((row) => row[loader.manyJoin.idField]);
533
-
534
- if (loader.manyJoin.through === undefined) {
535
- // HasMany
536
- const idColumn = `${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`;
537
- subQ = db(loader.manyJoin.toTable)
538
- .whereIn(idColumn as string, fromIds as string[])
539
- .select([...loader.select, idColumn]);
540
-
541
- loader.oneJoins.forEach((join) => {
542
- if (join.join === "inner") {
543
- subQ.innerJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
544
- } else if (join.join === "outer") {
545
- subQ.leftOuterJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
546
- }
547
- });
548
- toCol = loader.manyJoin.toCol;
549
- } else {
550
- // ManyToMany
551
- const idColumn = `${loader.manyJoin.through.table}.${loader.manyJoin.through.fromCol}`;
552
- subQ = db(loader.manyJoin.through.table)
553
- .join(
554
- loader.manyJoin.toTable,
555
- `${loader.manyJoin.through.table}.${loader.manyJoin.through.toCol}`,
556
- `${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`,
557
- )
558
- .whereIn(idColumn as string, fromIds as string[])
559
- .select(unique([...loader.select, idColumn]));
560
-
561
- loader.oneJoins.forEach((join) => {
562
- if (join.join === "inner") {
563
- subQ.innerJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
564
- } else if (join.join === "outer") {
565
- subQ.leftOuterJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
566
- }
567
- });
568
- toCol = loader.manyJoin.through.fromCol;
569
- }
570
- subRows = await subQ;
571
-
572
- if (loader.loaders) {
573
- subRows = await this.useLoaders(db, subRows, loader.loaders);
574
- }
575
-
576
- const subRowGroups = group(subRows, (row) => row[toCol] as string);
577
- rows = rows.map((row) => {
578
- row[loader.as] = (subRowGroups[row[loader.manyJoin.idField] as string] ?? []).map((r) =>
579
- omit(r, [toCol]),
580
- );
581
- return row;
582
- });
583
- }
584
- return rows;
585
- }
586
-
587
- getJoinClause(db: Knex<any, unknown>, join: SubsetQuery["joins"][number]): Knex.Raw<any> {
588
- if (!isCustomJoinClause(join)) {
589
- return db.raw(`${join.from} = ${join.to}`);
590
- } else {
591
- return db.raw(join.custom);
592
- }
593
- }
594
-
595
- getUpsertBuilder(): UpsertBuilder {
596
- return new UpsertBuilder();
597
- }
598
367
  }
599
368
 
600
369
  /**
@@ -1,4 +1,4 @@
1
- import { hot } from "@sonamu-kit/hot-hook";
1
+ import { hot } from "@sonamu-kit/hmr-hook";
2
2
  import assert from "assert";
3
3
  import chalk from "chalk";
4
4
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -122,7 +122,7 @@ export class Syncer {
122
122
  }
123
123
 
124
124
  // 싱크 작업이 끝나면 모든 모듈을 로드합니다.
125
- // hot-hook에 의해 invalidate된 부분들이 아니라면 캐시 그대로 유지합니다.
125
+ // hmr-hook에 의해 invalidate된 부분들이 아니라면 캐시 그대로 유지합니다.
126
126
  await this.autoloadTypes();
127
127
  await this.autoloadModels();
128
128
  await this.autoloadApis();
@@ -342,7 +342,7 @@ export function zodTypeToTsTypeDef(zt: z.ZodType): string {
342
342
  // ZodType - 재귀적으로 변환
343
343
  if (part && typeof part === "object" && (part as z.ZodType)._zod) {
344
344
  const innerType = zodTypeToTsTypeDef(part as z.ZodType);
345
- return `\${${innerType}}`;
345
+ return `$\{${innerType}}`;
346
346
  }
347
347
 
348
348
  // 폴백
@@ -571,6 +571,8 @@ function resolveRenderType(key: string, zodType: z.ZodTypeAny): RenderingNode["r
571
571
  return "string-plain";
572
572
  } else if (zodType instanceof z.ZodLiteral) {
573
573
  return "string-plain";
574
+ } else if (zodType instanceof z.ZodTemplateLiteral) {
575
+ return "string-plain";
574
576
  } else {
575
577
  throw new Error(`타입 파싱 불가 ${key} ${zodType.def.type}`);
576
578
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"hot-hook-register.d.ts","sourceRoot":"","sources":["../../src/bin/hot-hook-register.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,EAAE,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=loader-register.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"loader-register.d.ts","sourceRoot":"","sources":["../../src/bin/loader-register.ts"],"names":[],"mappings":""}
@@ -1,34 +0,0 @@
1
- import { register } from "node:module";
2
- import * as path from "node:path";
3
- import { exists } from "../utils/fs-utils.js";
4
- import { findApiRootPath } from "../utils/utils.js";
5
- /**
6
- * @sonamu-kit/loader/loader를 등록하는 스크립트입니다.
7
- * 이 스크립트는 sonamu cli로 dev 실행할 때 --import로 실행됩니다.
8
- */ async function setupSwcConfig() {
9
- try {
10
- const apiRoot = findApiRootPath();
11
- // 프로젝트 루트에서 .swcrc 찾기
12
- const projectSwcrcPath = path.join(apiRoot, ".swcrc");
13
- if (await exists(projectSwcrcPath)) {
14
- // 사용자 프로젝트에 .swcrc가 있으면 우선으로 사용합니다.
15
- process.env.SWCRC_PATH = projectSwcrcPath;
16
- return;
17
- }
18
- // 아니라면 sonamu가 관리하는 .swcrc.project-default를 가져다 씁니다.
19
- const sonamuSwcrcPath = path.join(import.meta.dirname, "..", "..", ".swcrc.project-default");
20
- if (await exists(sonamuSwcrcPath)) {
21
- process.env.SWCRC_PATH = sonamuSwcrcPath;
22
- return;
23
- }
24
- } catch {
25
- // 환경 변수 설정 실패는 무시 (loader가 기본 설정 사용)
26
- }
27
- }
28
- // swc 설정 파일 경로를 환경 변수로 설정
29
- await setupSwcConfig();
30
- register("@sonamu-kit/loader/loader", {
31
- parentURL: import.meta.url
32
- });
33
-
34
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9iaW4vbG9hZGVyLXJlZ2lzdGVyLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHJlZ2lzdGVyIH0gZnJvbSBcIm5vZGU6bW9kdWxlXCI7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gXCJub2RlOnBhdGhcIjtcbmltcG9ydCB7IGV4aXN0cyB9IGZyb20gXCIuLi91dGlscy9mcy11dGlscy5qc1wiO1xuaW1wb3J0IHsgZmluZEFwaVJvb3RQYXRoIH0gZnJvbSBcIi4uL3V0aWxzL3V0aWxzLmpzXCI7XG5cbi8qKlxuICogQHNvbmFtdS1raXQvbG9hZGVyL2xvYWRlcuulvCDrk7HroZ3tlZjripQg7Iqk7YGs66a97Yq47J6F64uI64ukLlxuICog7J20IOyKpO2BrOumve2KuOuKlCBzb25hbXUgY2xp66GcIGRldiDsi6TtlontlaAg65WMIC0taW1wb3J066GcIOyLpO2WieuQqeuLiOuLpC5cbiAqL1xuYXN5bmMgZnVuY3Rpb24gc2V0dXBTd2NDb25maWcoKSB7XG4gIHRyeSB7XG4gICAgY29uc3QgYXBpUm9vdCA9IGZpbmRBcGlSb290UGF0aCgpO1xuXG4gICAgLy8g7ZSE66Gc7KCd7Yq4IOujqO2KuOyXkOyEnCAuc3djcmMg7LC+6riwXG4gICAgY29uc3QgcHJvamVjdFN3Y3JjUGF0aCA9IHBhdGguam9pbihhcGlSb290LCBcIi5zd2NyY1wiKTtcbiAgICBpZiAoYXdhaXQgZXhpc3RzKHByb2plY3RTd2NyY1BhdGgpKSB7XG4gICAgICAvLyDsgqzsmqnsnpAg7ZSE66Gc7KCd7Yq47JeQIC5zd2NyY+qwgCDsnojsnLzrqbQg7Jqw7ISg7Jy866GcIOyCrOyaqe2VqeuLiOuLpC5cbiAgICAgIHByb2Nlc3MuZW52LlNXQ1JDX1BBVEggPSBwcm9qZWN0U3djcmNQYXRoO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIC8vIOyVhOuLiOudvOuptCBzb25hbXXqsIAg6rSA66as7ZWY64qUIC5zd2NyYy5wcm9qZWN0LWRlZmF1bHTrpbwg6rCA7KC464ukIOyUgeuLiOuLpC5cbiAgICBjb25zdCBzb25hbXVTd2NyY1BhdGggPSBwYXRoLmpvaW4oaW1wb3J0Lm1ldGEuZGlybmFtZSwgXCIuLlwiLCBcIi4uXCIsIFwiLnN3Y3JjLnByb2plY3QtZGVmYXVsdFwiKTtcbiAgICBpZiAoYXdhaXQgZXhpc3RzKHNvbmFtdVN3Y3JjUGF0aCkpIHtcbiAgICAgIHByb2Nlc3MuZW52LlNXQ1JDX1BBVEggPSBzb25hbXVTd2NyY1BhdGg7XG4gICAgICByZXR1cm47XG4gICAgfVxuICB9IGNhdGNoIHtcbiAgICAvLyDtmZjqsr0g67OA7IiYIOyEpOyglSDsi6TtjKjripQg66y07IucIChsb2FkZXLqsIAg6riw67O4IOyEpOyglSDsgqzsmqkpXG4gIH1cbn1cblxuLy8gc3djIOyEpOyglSDtjIzsnbwg6rK966Gc66W8IO2ZmOqyvSDrs4DsiJjroZwg7ISk7KCVXG5hd2FpdCBzZXR1cFN3Y0NvbmZpZygpO1xuXG5yZWdpc3RlcihcIkBzb25hbXUta2l0L2xvYWRlci9sb2FkZXJcIiwge1xuICBwYXJlbnRVUkw6IGltcG9ydC5tZXRhLnVybCxcbn0pO1xuIl0sIm5hbWVzIjpbInJlZ2lzdGVyIiwicGF0aCIsImV4aXN0cyIsImZpbmRBcGlSb290UGF0aCIsInNldHVwU3djQ29uZmlnIiwiYXBpUm9vdCIsInByb2plY3RTd2NyY1BhdGgiLCJqb2luIiwicHJvY2VzcyIsImVudiIsIlNXQ1JDX1BBVEgiLCJzb25hbXVTd2NyY1BhdGgiLCJkaXJuYW1lIiwicGFyZW50VVJMIiwidXJsIl0sIm1hcHBpbmdzIjoiQUFBQSxTQUFTQSxRQUFRLFFBQVEsY0FBYztBQUN2QyxZQUFZQyxVQUFVLFlBQVk7QUFDbEMsU0FBU0MsTUFBTSxRQUFRLHVCQUF1QjtBQUM5QyxTQUFTQyxlQUFlLFFBQVEsb0JBQW9CO0FBRXBEOzs7Q0FHQyxHQUNELGVBQWVDO0lBQ2IsSUFBSTtRQUNGLE1BQU1DLFVBQVVGO1FBRWhCLHNCQUFzQjtRQUN0QixNQUFNRyxtQkFBbUJMLEtBQUtNLElBQUksQ0FBQ0YsU0FBUztRQUM1QyxJQUFJLE1BQU1ILE9BQU9JLG1CQUFtQjtZQUNsQyxvQ0FBb0M7WUFDcENFLFFBQVFDLEdBQUcsQ0FBQ0MsVUFBVSxHQUFHSjtZQUN6QjtRQUNGO1FBRUEscURBQXFEO1FBQ3JELE1BQU1LLGtCQUFrQlYsS0FBS00sSUFBSSxDQUFDLFlBQVlLLE9BQU8sRUFBRSxNQUFNLE1BQU07UUFDbkUsSUFBSSxNQUFNVixPQUFPUyxrQkFBa0I7WUFDakNILFFBQVFDLEdBQUcsQ0FBQ0MsVUFBVSxHQUFHQztZQUN6QjtRQUNGO0lBQ0YsRUFBRSxPQUFNO0lBQ04scUNBQXFDO0lBQ3ZDO0FBQ0Y7QUFFQSwwQkFBMEI7QUFDMUIsTUFBTVA7QUFFTkosU0FBUyw2QkFBNkI7SUFDcENhLFdBQVcsWUFBWUMsR0FBRztBQUM1QiJ9