@simplysm/sd-cli 11.1.45 → 11.1.51

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 (53) hide show
  1. package/dist/build-cluster.js +16 -3
  2. package/dist/build-cluster.js.map +1 -1
  3. package/dist/build-tools/SdLibBundler.d.ts +13 -0
  4. package/dist/build-tools/SdLibBundler.js +51 -0
  5. package/dist/build-tools/SdLibBundler.js.map +1 -0
  6. package/dist/build-tools/SdLinter.js +4 -4
  7. package/dist/build-tools/SdLinter.js.map +1 -1
  8. package/dist/build-tools/SdNgBundler.d.ts +11 -20
  9. package/dist/build-tools/SdNgBundler.js +103 -96
  10. package/dist/build-tools/SdNgBundler.js.map +1 -1
  11. package/dist/build-tools/SdServerBundler.js.map +1 -1
  12. package/dist/build-tools/SdTsCompiler.d.ts +24 -27
  13. package/dist/build-tools/SdTsCompiler.js +237 -184
  14. package/dist/build-tools/SdTsCompiler.js.map +1 -1
  15. package/dist/builders/SdCliTsLibBuilder.d.ts +2 -6
  16. package/dist/builders/SdCliTsLibBuilder.js +30 -30
  17. package/dist/builders/SdCliTsLibBuilder.js.map +1 -1
  18. package/dist/bundle-plugins/sdNgPlugin.d.ts +3 -3
  19. package/dist/bundle-plugins/sdNgPlugin.js +22 -212
  20. package/dist/bundle-plugins/sdNgPlugin.js.map +1 -1
  21. package/dist/bundle-plugins/sdServerPlugin.d.ts +2 -2
  22. package/dist/bundle-plugins/sdServerPlugin.js +17 -110
  23. package/dist/bundle-plugins/sdServerPlugin.js.map +1 -1
  24. package/dist/commons.d.ts +1 -0
  25. package/dist/entry/SdCliProject.d.ts +1 -0
  26. package/dist/entry/SdCliProject.js +14 -30
  27. package/dist/entry/SdCliProject.js.map +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/sd-cli.d.ts +1 -1
  32. package/dist/sd-cli.js +8 -2
  33. package/dist/sd-cli.js.map +1 -1
  34. package/dist/server-worker.js.map +1 -1
  35. package/dist/utils/SdCliBuildResultUtil.d.ts +10 -0
  36. package/dist/utils/SdCliBuildResultUtil.js +17 -1
  37. package/dist/utils/SdCliBuildResultUtil.js.map +1 -1
  38. package/package.json +15 -15
  39. package/src/build-cluster.ts +18 -4
  40. package/src/build-tools/SdLibBundler.ts +70 -0
  41. package/src/build-tools/SdLinter.ts +4 -4
  42. package/src/build-tools/SdNgBundler.ts +120 -112
  43. package/src/build-tools/SdServerBundler.ts +2 -2
  44. package/src/build-tools/SdTsCompiler.ts +352 -241
  45. package/src/builders/SdCliTsLibBuilder.ts +33 -31
  46. package/src/bundle-plugins/sdNgPlugin.ts +26 -320
  47. package/src/bundle-plugins/sdServerPlugin.ts +20 -168
  48. package/src/commons.ts +1 -0
  49. package/src/entry/SdCliProject.ts +15 -31
  50. package/src/index.ts +1 -0
  51. package/src/sd-cli.ts +8 -2
  52. package/src/server-worker.ts +3 -3
  53. package/src/utils/SdCliBuildResultUtil.ts +22 -3
@@ -1,203 +1,60 @@
1
1
  import esbuild from "esbuild";
2
- import {FsUtil} from "@simplysm/sd-core-node";
3
2
  import ts from "typescript";
4
3
  import path from "path";
5
4
  import {convertTypeScriptDiagnostic} from "@angular-devkit/build-angular/src/tools/esbuild/angular/diagnostics";
5
+ import {ISdTsCompiler2Result, SdTsCompiler} from "../build-tools/SdTsCompiler";
6
6
 
7
7
  export function sdServerPlugin(conf: {
8
8
  pkgPath: string;
9
9
  dev: boolean;
10
10
  modifiedFileSet: Set<string>;
11
- result: IServerBundlerResultCache;
11
+ result: IServerPluginResultCache;
12
12
  }): esbuild.Plugin {
13
- const tsConfigPath = path.resolve(conf.pkgPath, "tsconfig.json");
14
- const tsConfig = FsUtil.readJson(tsConfigPath);
15
- const parsedTsConfig = ts.parseJsonConfigFileContent(tsConfig, ts.sys, conf.pkgPath, {
16
- declaration: false
17
- });
18
-
19
- const sourceFileCache = new Map<string, ts.SourceFile>();
20
- const referencingMap = new Map<string, Set<string>>();
21
-
22
- let program: ts.Program | undefined;
23
- let builder: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined;
24
-
25
- let resultCache: IResultCache = {
26
- watchFileSet: new Set<string>(),
27
- affectedFileSet: new Set<string>()
28
- };
29
- const tscPrepareMap = new Map<string, string>();
30
-
31
- function createCompilerHost() {
32
- const compilerHost = ts.createIncrementalCompilerHost(parsedTsConfig.options);
33
- const baseGetSourceFile = compilerHost.getSourceFile;
34
- compilerHost.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile, ...args) => {
35
- if (!shouldCreateNewSourceFile && sourceFileCache.has(path.normalize(fileName))) {
36
- return sourceFileCache.get(path.normalize(fileName));
37
- }
38
-
39
- const file = baseGetSourceFile.call(
40
- compilerHost,
41
- fileName,
42
- languageVersionOrOptions,
43
- onError,
44
- true,
45
- ...args,
46
- );
47
-
48
- if (file) {
49
- sourceFileCache.set(path.normalize(fileName), file);
50
- }
51
-
52
- return file;
53
- };
54
-
55
- return compilerHost;
56
- }
57
-
58
- function findAffectedFileSet() {
59
- const affectedFileSet = new Set<string>();
60
-
61
- while (true) {
62
- const result = builder!.getSemanticDiagnosticsOfNextAffectedFile();
63
- if (!result) {
64
- break;
65
- }
66
- affectedFileSet.add(path.normalize((result.affected as ts.SourceFile).fileName));
67
- }
68
-
69
- return affectedFileSet;
70
- }
71
-
72
13
  return {
73
14
  name: "sd-server-compiler",
74
15
  setup: (build: esbuild.PluginBuild) => {
75
- //-- compilerHost
76
- const compilerHost = createCompilerHost();
77
-
78
- build.onStart(() => {
79
- //-- modified
80
- for (const modifiedFile of conf.modifiedFileSet) {
81
- sourceFileCache.delete(modifiedFile);
82
-
83
- if (referencingMap.has(modifiedFile)) {
84
- for (const referencingFile of referencingMap.get(modifiedFile)!) {
85
- sourceFileCache.delete(referencingFile);
86
- }
87
- }
88
- }
89
- referencingMap.clear();
90
-
91
- //-- init resultCache
92
-
93
- resultCache = {
94
- watchFileSet: new Set<string>(),
95
- affectedFileSet: new Set<string>(),
96
- };
97
-
98
- program = ts.createProgram(
99
- parsedTsConfig.fileNames,
100
- parsedTsConfig.options,
101
- compilerHost,
102
- program
103
- );
104
- builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(
105
- program,
106
- compilerHost,
107
- builder
108
- );
109
-
110
- //-- affectedFilePathSet
111
- resultCache.affectedFileSet.adds(...findAffectedFileSet());
112
-
113
- // Deps -> refMap
114
- builder.getSourceFiles().forEach(sf => {
115
- resultCache.watchFileSet.add(path.normalize(sf.fileName));
116
-
117
- const deps = builder!.getAllDependencies(sf);
118
- for (const dep of deps) {
119
- const ref = referencingMap.getOrCreate(dep, new Set<string>());
120
- ref.add(dep);
121
-
122
- resultCache.watchFileSet.add(path.normalize(dep));
123
- }
124
- });
125
-
126
- // refMap, modFile -> affectedFileSet
127
- for (const modifiedFile of conf.modifiedFileSet) {
128
- resultCache.affectedFileSet.adds(...referencingMap.get(modifiedFile) ?? []);
129
- }
130
-
131
- //-- diagnostics / build
132
-
133
- const diagnostics: ts.Diagnostic[] = [];
16
+ const compiler = new SdTsCompiler(conf.pkgPath, {declaration: false}, conf.dev);
134
17
 
135
- diagnostics.push(
136
- ...builder.getConfigFileParsingDiagnostics(),
137
- ...builder.getOptionsDiagnostics(),
138
- ...builder.getGlobalDiagnostics()
139
- );
18
+ let buildResult: ISdTsCompiler2Result;
140
19
 
141
- for (const affectedFile of resultCache.affectedFileSet) {
142
- const affectedSourceFile = sourceFileCache.get(path.normalize(affectedFile));
143
- if (!affectedSourceFile) {
144
- continue;
145
- }
20
+ build.onStart(async () => {
21
+ compiler.invalidate(conf.modifiedFileSet);
22
+ buildResult = await compiler.buildAsync();
146
23
 
147
- diagnostics.push(
148
- ...builder.getSyntacticDiagnostics(affectedSourceFile),
149
- ...builder.getSemanticDiagnostics(affectedSourceFile)
150
- );
151
- }
152
-
153
- //-- prepare emit cache
154
- while (true) {
155
- const affectedFileResult = builder.emitNextAffectedFile((fileName, text, writeByteOrderMark, onError, sourceFiles, data) => {
156
- if (!sourceFiles || sourceFiles.length === 0) {
157
- compilerHost.writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data);
158
- return;
159
- }
160
-
161
- const sourceFile = ts.getOriginalNode(sourceFiles[0], ts.isSourceFile);
162
- tscPrepareMap.set(path.normalize(sourceFile.fileName), text);
163
- });
164
-
165
- if (!affectedFileResult) {
166
- break;
167
- }
168
-
169
- diagnostics.push(...affectedFileResult.result.diagnostics);
170
- }
24
+ conf.result.watchFileSet = buildResult.watchFileSet;
25
+ conf.result.affectedFileSet = buildResult.affectedFileSet;
26
+ conf.result.program = buildResult.program;
171
27
 
172
28
  //-- return err/warn
173
29
 
174
30
  return {
175
- errors: diagnostics.filter(item => item.category === ts.DiagnosticCategory.Error).map(item => convertTypeScriptDiagnostic(ts, item)),
176
- warnings: diagnostics.filter(item => item.category !== ts.DiagnosticCategory.Error).map(item => convertTypeScriptDiagnostic(ts, item)),
31
+ errors: buildResult.typescriptDiagnostics.filter(item => item.category === ts.DiagnosticCategory.Error).map(item => convertTypeScriptDiagnostic(ts, item)),
32
+ warnings: buildResult.typescriptDiagnostics.filter(item => item.category !== ts.DiagnosticCategory.Error).map(item => convertTypeScriptDiagnostic(ts, item)),
177
33
  };
178
34
  });
179
35
 
180
36
 
181
37
  build.onLoad({filter: /\.ts$/}, (args) => {
182
- resultCache.watchFileSet.add(path.normalize(args.path));
183
- const contents = tscPrepareMap.get(path.normalize(args.path));
38
+ const contents = buildResult.emittedFilesCacheMap.get(path.normalize(args.path))!.last()!.text;
184
39
  return {contents, loader: "js"};
185
40
  });
186
41
 
42
+ build.onLoad({filter: /\.[cm]?js$/}, (args) => {
43
+ conf.result.watchFileSet!.add(path.normalize(args.path));
44
+ return null;
45
+ });
46
+
187
47
  build.onLoad(
188
48
  {filter: new RegExp("(" + Object.keys(build.initialOptions.loader!).map(item => "\\" + item).join("|") + ")$")},
189
49
  (args) => {
190
- resultCache.watchFileSet!.add(path.normalize(args.path));
50
+ conf.result.watchFileSet!.add(path.normalize(args.path));
191
51
  return null;
192
52
  }
193
53
  );
194
54
 
195
55
  build.onEnd((result) => {
196
- conf.result.watchFileSet = resultCache.watchFileSet;
197
- conf.result.affectedFileSet = resultCache.affectedFileSet;
198
56
  conf.result.outputFiles = result.outputFiles;
199
57
  conf.result.metafile = result.metafile;
200
- conf.result.program = program;
201
58
 
202
59
  conf.modifiedFileSet.clear();
203
60
  });
@@ -205,15 +62,10 @@ export function sdServerPlugin(conf: {
205
62
  };
206
63
  }
207
64
 
208
- export interface IServerBundlerResultCache {
65
+ export interface IServerPluginResultCache {
209
66
  watchFileSet?: Set<string>;
210
67
  affectedFileSet?: Set<string>;
211
68
  outputFiles?: esbuild.OutputFile[];
212
69
  metafile?: esbuild.Metafile;
213
70
  program?: ts.Program;
214
- }
215
-
216
- interface IResultCache {
217
- watchFileSet: Set<string>;
218
- affectedFileSet: Set<string>;
219
71
  }
package/src/commons.ts CHANGED
@@ -28,6 +28,7 @@ export interface ISdCliBuildClusterReqMessage {
28
28
  projConf: ISdCliConfig;
29
29
  pkgPath: string;
30
30
  // builderKey?: "web" | "electron";
31
+ execArgs?: string[];
31
32
  }
32
33
 
33
34
  export interface ISdCliBuildClusterResMessage {
@@ -2,7 +2,6 @@ import path from "path";
2
2
  import {FsUtil, Logger, PathUtil, SdProcess} from "@simplysm/sd-core-node";
3
3
  import {
4
4
  INpmConfig,
5
- ISdCliBuildClusterReqMessage,
6
5
  ISdCliBuildClusterResMessage,
7
6
  ISdCliConfig,
8
7
  ISdCliPackageBuildResult,
@@ -23,6 +22,7 @@ export class SdCliProject {
23
22
  confFileRelPath: string;
24
23
  optNames: string[];
25
24
  pkgNames: string[];
25
+ inspectNames: string[];
26
26
  }): Promise<void> {
27
27
  const logger = Logger.get(["simplysm", "sd-cli", "SdCliProject", "watchAsync"]);
28
28
 
@@ -87,7 +87,9 @@ export class SdCliProject {
87
87
  else if (message.type === "complete") {
88
88
  resultCache.delete("none");
89
89
  for (const affectedFilePath of message.result!.affectedFilePaths) {
90
- resultCache.delete(affectedFilePath);
90
+ if (PathUtil.isChildPath(affectedFilePath, message.req.pkgPath)) {
91
+ resultCache.delete(affectedFilePath);
92
+ }
91
93
  }
92
94
 
93
95
  for (const buildResult of message.result!.buildResults) {
@@ -220,16 +222,7 @@ export class SdCliProject {
220
222
  logger.log("빌드를 시작합니다...");
221
223
 
222
224
  await pkgPaths.parallelAsync(async (pkgPath) => {
223
- const pkgConf = projConf.packages[path.basename(pkgPath)]!;
224
- if (pkgConf.type === "client") {
225
- const builderKeys = Object.keys(pkgConf.builder ?? {web: {}});
226
- await builderKeys.parallelAsync(async (builderKey) => {
227
- await this._runCommandAsync(cluster, "watch", projConf, pkgPath, builderKey);
228
- });
229
- }
230
- else {
231
- await this._runCommandAsync(cluster, "watch", projConf, pkgPath);
232
- }
225
+ await this._runCommandAsync(cluster, "watch", projConf, pkgPath, opt.inspectNames.includes(path.basename(pkgPath)) ? ["--inspect"] : []);
233
226
  });
234
227
 
235
228
  busyReqCntMap.set(
@@ -274,7 +267,7 @@ export class SdCliProject {
274
267
  logger.debug("빌드 프로세스 명령 전달...");
275
268
  const results = (
276
269
  await pkgPaths.parallelAsync(async (pkgPath) => {
277
- const pkgConf = projConf.packages[path.basename(pkgPath)]!;
270
+ /*const pkgConf = projConf.packages[path.basename(pkgPath)]!;
278
271
  if (pkgConf.type === "client") {
279
272
  const builderKeys = Object.keys(pkgConf.builder ?? {web: {}});
280
273
  return (await builderKeys.parallelAsync(async (builderKey) => {
@@ -282,8 +275,8 @@ export class SdCliProject {
282
275
  })).mapMany();
283
276
  }
284
277
  else {
285
- return await this._runCommandAsync(cluster, "build", projConf, pkgPath);
286
- }
278
+ }*/
279
+ return await this._runCommandAsync(cluster, "build", projConf, pkgPath);
287
280
  })
288
281
  ).mapMany();
289
282
 
@@ -342,16 +335,7 @@ export class SdCliProject {
342
335
  logger.debug("빌드 프로세스 명령 전달...");
343
336
  const results = (
344
337
  await pkgPaths.parallelAsync(async (pkgPath) => {
345
- const pkgConf = projConf.packages[path.basename(pkgPath)]!;
346
- if (pkgConf.type === "client") {
347
- const builderKeys = Object.keys(pkgConf.builder ?? {web: {}});
348
- return (await builderKeys.parallelAsync(async (builderKey) => {
349
- return await this._runCommandAsync(cluster, "build", projConf, pkgPath, builderKey);
350
- })).mapMany();
351
- }
352
- else {
353
- return await this._runCommandAsync(cluster, "build", projConf, pkgPath);
354
- }
338
+ return await this._runCommandAsync(cluster, "build", projConf, pkgPath);
355
339
  })
356
340
  ).mapMany();
357
341
 
@@ -577,9 +561,9 @@ export class SdCliProject {
577
561
  });
578
562
  }
579
563
 
580
- private static async _runCommandAsync(cluster: cp.ChildProcess, cmd: "watch", projConf: ISdCliConfig, pkgPath: string, builderKey?: string): Promise<void>;
581
- private static async _runCommandAsync(cluster: cp.ChildProcess, cmd: "build", projConf: ISdCliConfig, pkgPath: string, builderKey?: string): Promise<ISdCliPackageBuildResult[]>;
582
- private static async _runCommandAsync(cluster: cp.ChildProcess, cmd: "watch" | "build", projConf: ISdCliConfig, pkgPath: string, builderKey?: string): Promise<ISdCliPackageBuildResult[] | void> {
564
+ private static async _runCommandAsync(cluster: cp.ChildProcess, cmd: "watch", projConf: ISdCliConfig, pkgPath: string, execArgs: string[]): Promise<void>;
565
+ private static async _runCommandAsync(cluster: cp.ChildProcess, cmd: "build", projConf: ISdCliConfig, pkgPath: string): Promise<ISdCliPackageBuildResult[]>;
566
+ private static async _runCommandAsync(cluster: cp.ChildProcess, cmd: "watch" | "build", projConf: ISdCliConfig, pkgPath: string, execArgs?: string[]): Promise<ISdCliPackageBuildResult[] | void> {
583
567
  return await new Promise<ISdCliPackageBuildResult[] | void>((resolve) => {
584
568
  const cb = (message: ISdCliBuildClusterResMessage): void => {
585
569
  if (cmd === "watch" && message.type === "ready" && message.req.cmd === cmd && message.req.pkgPath === pkgPath) {
@@ -597,8 +581,8 @@ export class SdCliProject {
597
581
  cmd,
598
582
  projConf,
599
583
  pkgPath,
600
- builderKey
601
- } as ISdCliBuildClusterReqMessage);
584
+ execArgs
585
+ });
602
586
  });
603
587
  }
604
588
 
@@ -667,4 +651,4 @@ export class SdCliProject {
667
651
  });
668
652
  });
669
653
  }
670
- }
654
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./build-tools/SdCliCordova";
2
2
  export * from "./build-tools/SdCliIndexFileGenerator";
3
3
  export * from "./build-tools/SdCliNgRoutesFileGenerator";
4
+ export * from "./build-tools/SdLibBundler";
4
5
  export * from "./build-tools/SdLinter";
5
6
  export * from "./build-tools/SdNgBundler";
6
7
  export * from "./build-tools/SdNgBundlerContext";
package/src/sd-cli.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node --es-module-specifier-resolution=node --no-warnings --experimental-import-meta-resolve --max-old-space-size=8000
1
+ #!/usr/bin/env node --es-module-specifier-resolution=node --no-warnings --experimental-import-meta-resolve
2
2
 
3
3
  import yargs from "yargs";
4
4
  import {hideBin} from "yargs/helpers";
@@ -57,6 +57,11 @@ const argv = (
57
57
  string: true,
58
58
  array: true,
59
59
  describe: "수행할 패키지 설정"
60
+ },
61
+ inspects: {
62
+ string: true,
63
+ array: true,
64
+ describe: "크롬 inspect를 수행할 패키지 설정"
60
65
  }
61
66
  })
62
67
  )
@@ -197,7 +202,8 @@ else if (argv._[0] === "watch") {
197
202
  .watchAsync({
198
203
  confFileRelPath: argv.config ?? "simplysm.cjs",
199
204
  optNames: argv.options ?? [],
200
- pkgNames: argv.packages ?? []
205
+ pkgNames: argv.packages ?? [],
206
+ inspectNames: argv.inspects ?? []
201
207
  });
202
208
  }
203
209
  else if (argv._[0] === "build") {
@@ -52,14 +52,14 @@ else {
52
52
 
53
53
 
54
54
  server.on("ready", () => {
55
- process.send!({port: server!.options.port});
55
+ process.send!({port: server.options.port});
56
56
  });
57
57
 
58
58
  process.on("message", (message: any) => {
59
59
  if (message.type === "setPathProxy") {
60
- server!.pathProxy = message.pathProxy;
60
+ server.pathProxy = message.pathProxy;
61
61
  }
62
62
  if (message.type === "broadcastReload") {
63
- server!.broadcastReload();
63
+ server.broadcastReload();
64
64
  }
65
65
  });
@@ -2,9 +2,10 @@ import ts from "typescript";
2
2
  import os from "os";
3
3
  import path from "path";
4
4
  import {ISdCliPackageBuildResult} from "../commons";
5
+ import {PartialMessage} from "esbuild";
5
6
 
6
7
  export class SdCliBuildResultUtil {
7
- public static convertFromTsDiag(diag: ts.Diagnostic, type: "build" | "check"): ISdCliPackageBuildResult {
8
+ static convertFromTsDiag(diag: ts.Diagnostic, type: "build" | "check"): ISdCliPackageBuildResult {
8
9
  const severity = diag.category === ts.DiagnosticCategory.Error ? "error" as const
9
10
  : diag.category === ts.DiagnosticCategory.Warning ? "warning" as const
10
11
  : diag.category === ts.DiagnosticCategory.Suggestion ? "suggestion" as const
@@ -19,7 +20,7 @@ export class SdCliBuildResultUtil {
19
20
  const char = position ? position.character + 1 : undefined;
20
21
 
21
22
  return {
22
- filePath: filePath !== undefined ? path.resolve(filePath) : undefined,
23
+ filePath,
23
24
  line,
24
25
  char,
25
26
  code,
@@ -29,7 +30,25 @@ export class SdCliBuildResultUtil {
29
30
  };
30
31
  }
31
32
 
32
- public static getMessage(result: ISdCliPackageBuildResult): string {
33
+ static convertFromEsbuildResult(msg: PartialMessage, type: "build" | "check", severity: "warning" | "error") {
34
+ const filePath = msg.location?.file != null ? path.resolve(msg.location.file) : undefined;
35
+ const line = msg.location?.line;
36
+ const char = msg.location?.column;
37
+ const code = msg.text?.slice(0, msg.text.indexOf(":"));
38
+ const message = `${msg.pluginName != null ? `(${msg.pluginName}) ` : ""} ${msg.text?.slice(msg.text.indexOf(":") + 1)}`;
39
+
40
+ return {
41
+ filePath,
42
+ line,
43
+ char,
44
+ code,
45
+ severity,
46
+ message,
47
+ type
48
+ };
49
+ }
50
+
51
+ static getMessage(result: ISdCliPackageBuildResult): string {
33
52
  let str = "";
34
53
  if (result.filePath !== undefined) {
35
54
  str += `${result.filePath}(${result.line ?? 0}, ${result.char ?? 0}): `;