keycloakify 10.0.0-rc.62 → 10.0.0-rc.64

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.
@@ -103,7 +103,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
103
103
  `${chalk.green("✓")} ${chalk.bold(
104
104
  pathJoin(".", pathRelative(process.cwd(), targetFilePath))
105
105
  )} copy pasted from the Keycloakify source code into your project`,
106
- `You can start storybook with ${chalk.bold("yarn storybook")}`
106
+ `You can start storybook with ${chalk.bold("npm run storybook")}`
107
107
  ].join("\n")
108
108
  );
109
109
  }
@@ -16,7 +16,7 @@ import { readFileSync } from "fs";
16
16
  import { isInside } from "../../tools/isInside";
17
17
  import child_process from "child_process";
18
18
  import { rmSync } from "../../tools/fs.rmSync";
19
- import { getMetaInfKeycloakThemesJsonFilePath } from "../../shared/metaInfKeycloakThemes";
19
+ import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
20
20
 
21
21
  export type BuildContextLike = BuildContextLike_generatePom & {
22
22
  keycloakifyBuildDirPath: string;
@@ -50,9 +50,16 @@ export async function buildJar(params: {
50
50
 
51
51
  rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true });
52
52
 
53
+ const tmpResourcesDirPath = pathJoin(
54
+ keycloakifyBuildTmpDirPath,
55
+ "src",
56
+ "main",
57
+ "resources"
58
+ );
59
+
53
60
  transformCodebase({
54
61
  srcDirPath: resourcesDirPath,
55
- destDirPath: pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources"),
62
+ destDirPath: tmpResourcesDirPath,
56
63
  transformSourceCode:
57
64
  keycloakAccountV1Version !== null
58
65
  ? undefined
@@ -71,31 +78,6 @@ export async function buildJar(params: {
71
78
  return undefined;
72
79
  }
73
80
 
74
- if (
75
- fileRelativePath ===
76
- getMetaInfKeycloakThemesJsonFilePath({
77
- resourcesDirPath: "."
78
- })
79
- ) {
80
- const keycloakThemesJsonParsed = JSON.parse(
81
- sourceCode.toString("utf8")
82
- ) as {
83
- themes: { name: string; types: string[] }[];
84
- };
85
-
86
- keycloakThemesJsonParsed.themes =
87
- keycloakThemesJsonParsed.themes.filter(
88
- ({ name }) => name !== accountV1ThemeName
89
- );
90
-
91
- return {
92
- modifiedSourceCode: Buffer.from(
93
- JSON.stringify(keycloakThemesJsonParsed, null, 2),
94
- "utf8"
95
- )
96
- };
97
- }
98
-
99
81
  for (const themeName of buildContext.themeNames) {
100
82
  if (
101
83
  fileRelativePath ===
@@ -123,6 +105,21 @@ export async function buildJar(params: {
123
105
  }
124
106
  });
125
107
 
108
+ if (keycloakAccountV1Version === null) {
109
+ writeMetaInfKeycloakThemes({
110
+ resourcesDirPath: tmpResourcesDirPath,
111
+ getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
112
+ assert(metaInfKeycloakTheme !== undefined);
113
+
114
+ metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
115
+ ({ name }) => name !== accountV1ThemeName
116
+ );
117
+
118
+ return metaInfKeycloakTheme;
119
+ }
120
+ });
121
+ }
122
+
126
123
  route_legacy_pages: {
127
124
  // NOTE: If there's no account theme there is no special target for keycloak 24 and up so we create
128
125
  // the pages anyway. If there is an account pages, since we know that account-v1 is only support keycloak
@@ -8,10 +8,10 @@ import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
8
8
  import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar";
9
9
  import type { BuildContext } from "../../shared/buildContext";
10
10
  import { getJarFileBasename } from "../../shared/getJarFileBasename";
11
- import { readMetaInfKeycloakThemes_fromResourcesDirPath } from "../../shared/metaInfKeycloakThemes";
12
- import { accountV1ThemeName } from "../../shared/constants";
11
+ import { getImplementedThemeTypes } from "../../shared/getImplementedThemeTypes";
13
12
 
14
13
  export type BuildContextLike = BuildContextLike_buildJar & {
14
+ projectDirPath: string;
15
15
  keycloakifyBuildDirPath: string;
16
16
  };
17
17
 
@@ -24,9 +24,9 @@ export async function buildJars(params: {
24
24
  }): Promise<void> {
25
25
  const { onlyBuildJarFileBasename, resourcesDirPath, buildContext } = params;
26
26
 
27
- const doesImplementAccountTheme = readMetaInfKeycloakThemes_fromResourcesDirPath({
28
- resourcesDirPath
29
- }).themes.some(({ name }) => name === accountV1ThemeName);
27
+ const doesImplementAccountTheme = getImplementedThemeTypes({
28
+ projectDirPath: buildContext.projectDirPath
29
+ }).implementedThemeTypes.account;
30
30
 
31
31
  await Promise.all(
32
32
  keycloakAccountV1Versions
@@ -1,6 +1,6 @@
1
1
  import { transformCodebase } from "../../tools/transformCodebase";
2
2
  import * as fs from "fs";
3
- import { join as pathJoin, resolve as pathResolve } from "path";
3
+ import { join as pathJoin, resolve as pathResolve, relative as pathRelative } from "path";
4
4
  import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
5
5
  import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
6
6
  import {
@@ -16,7 +16,6 @@ import {
16
16
  loginThemePageIds,
17
17
  accountThemePageIds
18
18
  } from "../../shared/constants";
19
- import { isInside } from "../../tools/isInside";
20
19
  import type { BuildContext } from "../../shared/buildContext";
21
20
  import { assert, type Equals } from "tsafe/assert";
22
21
  import {
@@ -39,6 +38,7 @@ import {
39
38
  } from "../../shared/metaInfKeycloakThemes";
40
39
  import { objectEntries } from "tsafe/objectEntries";
41
40
  import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
41
+ import { getImplementedThemeTypes } from "../../shared/getImplementedThemeTypes";
42
42
 
43
43
  export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
44
44
  BuildContextLike_downloadKeycloakStaticResources &
@@ -63,6 +63,10 @@ export async function generateResourcesForMainTheme(params: {
63
63
  projectDirPath: buildContext.projectDirPath
64
64
  });
65
65
 
66
+ const { implementedThemeTypes } = getImplementedThemeTypes({
67
+ projectDirPath: buildContext.projectDirPath
68
+ });
69
+
66
70
  const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
67
71
  const { themeType } = params;
68
72
  return pathJoin(resourcesDirPath, "theme", themeName, themeType);
@@ -70,19 +74,11 @@ export async function generateResourcesForMainTheme(params: {
70
74
 
71
75
  const cssGlobalsToDefine: Record<string, string> = {};
72
76
 
73
- const implementedThemeTypes: Record<ThemeType | "email", boolean> = {
74
- login: false,
75
- account: false,
76
- email: false
77
- };
78
-
79
77
  for (const themeType of ["login", "account"] as const) {
80
- if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
78
+ if (!implementedThemeTypes[themeType]) {
81
79
  continue;
82
80
  }
83
81
 
84
- implementedThemeTypes[themeType] = true;
85
-
86
82
  const themeTypeDirPath = getThemeTypeDirPath({ themeType });
87
83
 
88
84
  apply_replacers_and_move_to_theme_resources: {
@@ -112,25 +108,32 @@ export async function generateResourcesForMainTheme(params: {
112
108
  break apply_replacers_and_move_to_theme_resources;
113
109
  }
114
110
 
111
+ {
112
+ const dirPath = pathJoin(
113
+ buildContext.projectBuildDirPath,
114
+ keycloak_resources
115
+ );
116
+
117
+ if (fs.existsSync(dirPath)) {
118
+ assert(buildContext.bundler === "webpack");
119
+
120
+ throw new Error(
121
+ [
122
+ `Keycloakify build error: The ${keycloak_resources} directory shouldn't exist in your build directory.`,
123
+ `(${pathRelative(process.cwd(), dirPath)}).\n`,
124
+ `Theses assets are only required for local development with Storybook.",
125
+ "Please remove this directory as an additional step of your command.\n`,
126
+ `For example: \`"build": "... && rimraf ${pathRelative(buildContext.projectDirPath, dirPath)}"\``
127
+ ].join(" ")
128
+ );
129
+ }
130
+ }
131
+
115
132
  transformCodebase({
116
133
  srcDirPath: buildContext.projectBuildDirPath,
117
134
  destDirPath,
118
135
  transformSourceCode: ({ filePath, sourceCode }) => {
119
- //NOTE: Prevent cycles, excludes the folder we generated for debug in public/
120
- // This should not happen if users follow the new instruction setup but we keep it for retrocompatibility.
121
- if (
122
- isInside({
123
- dirPath: pathJoin(
124
- buildContext.projectBuildDirPath,
125
- keycloak_resources
126
- ),
127
- filePath
128
- })
129
- ) {
130
- return undefined;
131
- }
132
-
133
- if (/\.css?$/i.test(filePath)) {
136
+ if (filePath.endsWith(".css")) {
134
137
  const {
135
138
  cssGlobalsToDefine: cssGlobalsToDefineForThisFile,
136
139
  fixedCssCode
@@ -149,7 +152,7 @@ export async function generateResourcesForMainTheme(params: {
149
152
  };
150
153
  }
151
154
 
152
- if (/\.js?$/i.test(filePath)) {
155
+ if (filePath.endsWith(".js")) {
153
156
  const { fixedJsCode } = replaceImportsInJsCode({
154
157
  jsCode: sourceCode.toString("utf8"),
155
158
  buildContext
@@ -262,13 +265,11 @@ export async function generateResourcesForMainTheme(params: {
262
265
  }
263
266
 
264
267
  email: {
265
- const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
266
-
267
- if (!fs.existsSync(emailThemeSrcDirPath)) {
268
+ if (!implementedThemeTypes.email) {
268
269
  break email;
269
270
  }
270
271
 
271
- implementedThemeTypes.email = true;
272
+ const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
272
273
 
273
274
  transformCodebase({
274
275
  srcDirPath: emailThemeSrcDirPath,
@@ -302,7 +303,7 @@ export async function generateResourcesForMainTheme(params: {
302
303
 
303
304
  writeMetaInfKeycloakThemes({
304
305
  resourcesDirPath,
305
- metaInfKeycloakThemes
306
+ getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
306
307
  });
307
308
  }
308
309
  }
@@ -1,10 +1,7 @@
1
1
  import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
2
2
  import { transformCodebase } from "../../tools/transformCodebase";
3
3
  import type { BuildContext } from "../../shared/buildContext";
4
- import {
5
- readMetaInfKeycloakThemes_fromResourcesDirPath,
6
- writeMetaInfKeycloakThemes
7
- } from "../../shared/metaInfKeycloakThemes";
4
+ import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
8
5
  import { assert } from "tsafe/assert";
9
6
 
10
7
  export type BuildContextLike = {
@@ -34,8 +31,8 @@ export function generateResourcesForThemeVariant(params: {
34
31
  Buffer.from(sourceCode)
35
32
  .toString("utf-8")
36
33
  .replace(
37
- `out["themeName"] = "${themeName}";`,
38
- `out["themeName"] = "${themeVariantName}";`
34
+ `kcContext.themeName = "${themeName}";`,
35
+ `kcContext.themeName = "${themeVariantName}";`
39
36
  ),
40
37
  "utf8"
41
38
  );
@@ -49,26 +46,25 @@ export function generateResourcesForThemeVariant(params: {
49
46
  }
50
47
  });
51
48
 
52
- {
53
- const updatedMetaInfKeycloakThemes =
54
- readMetaInfKeycloakThemes_fromResourcesDirPath({
55
- resourcesDirPath
56
- });
49
+ writeMetaInfKeycloakThemes({
50
+ resourcesDirPath,
51
+ getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
52
+ assert(metaInfKeycloakTheme !== undefined);
57
53
 
58
- updatedMetaInfKeycloakThemes.themes.push({
59
- name: themeVariantName,
60
- types: (() => {
61
- const theme = updatedMetaInfKeycloakThemes.themes.find(
62
- ({ name }) => name === themeName
63
- );
64
- assert(theme !== undefined);
65
- return theme.types;
66
- })()
67
- });
54
+ const newMetaInfKeycloakTheme = metaInfKeycloakTheme;
55
+
56
+ newMetaInfKeycloakTheme.themes.push({
57
+ name: themeVariantName,
58
+ types: (() => {
59
+ const theme = newMetaInfKeycloakTheme.themes.find(
60
+ ({ name }) => name === themeName
61
+ );
62
+ assert(theme !== undefined);
63
+ return theme.types;
64
+ })()
65
+ });
68
66
 
69
- writeMetaInfKeycloakThemes({
70
- resourcesDirPath,
71
- metaInfKeycloakThemes: updatedMetaInfKeycloakThemes
72
- });
73
- }
67
+ return newMetaInfKeycloakTheme;
68
+ }
69
+ });
74
70
  }
@@ -0,0 +1,23 @@
1
+ import { join as pathJoin } from "path";
2
+ import { objectFromEntries } from "tsafe/objectFromEntries";
3
+ import * as fs from "fs";
4
+ import { type ThemeType } from "./constants";
5
+ import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
6
+
7
+ export function getImplementedThemeTypes(params: { projectDirPath: string }) {
8
+ const { projectDirPath } = params;
9
+
10
+ const { themeSrcDirPath } = getThemeSrcDirPath({
11
+ projectDirPath
12
+ });
13
+
14
+ const implementedThemeTypes: Readonly<Record<ThemeType | "email", boolean>> =
15
+ objectFromEntries(
16
+ (["login", "account", "email"] as const).map(themeType => [
17
+ themeType,
18
+ fs.existsSync(pathJoin(themeSrcDirPath, themeType))
19
+ ])
20
+ );
21
+
22
+ return { implementedThemeTypes };
23
+ }
@@ -3,48 +3,60 @@ import { exclude } from "tsafe";
3
3
  import { crawl } from "../tools/crawl";
4
4
  import { join as pathJoin } from "path";
5
5
  import { themeTypes } from "./constants";
6
+ import chalk from "chalk";
6
7
 
7
- const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
8
+ let cache: { projectDirPath: string; themeSrcDirPath: string } | undefined = undefined;
8
9
 
9
10
  /** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
10
11
  export function getThemeSrcDirPath(params: { projectDirPath: string }) {
11
12
  const { projectDirPath } = params;
12
13
 
13
- const srcDirPath = pathJoin(projectDirPath, "src");
14
-
15
- const themeSrcDirPath: string | undefined = crawl({
16
- dirPath: srcDirPath,
17
- returnedPathsType: "relative to dirPath"
18
- })
19
- .map(fileRelativePath => {
20
- for (const themeSrcDirBasename of themeSrcDirBasenames) {
21
- const split = fileRelativePath.split(themeSrcDirBasename);
22
- if (split.length === 2) {
23
- return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
24
- }
25
- }
26
- return undefined;
27
- })
28
- .filter(exclude(undefined))[0];
29
-
30
- if (themeSrcDirPath !== undefined) {
14
+ if (cache !== undefined && cache.projectDirPath === projectDirPath) {
15
+ const { themeSrcDirPath } = cache;
31
16
  return { themeSrcDirPath };
32
17
  }
33
18
 
34
- for (const themeType of [...themeTypes, "email"]) {
35
- if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
36
- continue;
19
+ cache = undefined;
20
+
21
+ const { themeSrcDirPath } = (() => {
22
+ const srcDirPath = pathJoin(projectDirPath, "src");
23
+
24
+ const themeSrcDirPath: string | undefined = crawl({
25
+ dirPath: srcDirPath,
26
+ returnedPathsType: "relative to dirPath"
27
+ })
28
+ .map(fileRelativePath => {
29
+ for (const themeSrcDirBasename of themeSrcDirBasenames) {
30
+ const split = fileRelativePath.split(themeSrcDirBasename);
31
+ if (split.length === 2) {
32
+ return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
33
+ }
34
+ }
35
+ return undefined;
36
+ })
37
+ .filter(exclude(undefined))[0];
38
+
39
+ if (themeSrcDirPath !== undefined) {
40
+ return { themeSrcDirPath };
41
+ }
42
+
43
+ for (const themeType of [...themeTypes, "email"]) {
44
+ if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
45
+ continue;
46
+ }
47
+ return { themeSrcDirPath: srcDirPath };
37
48
  }
38
- return { themeSrcDirPath: srcDirPath };
39
- }
40
49
 
41
- console.error(
42
- [
43
- "Can't locate your theme source directory. It should be either: ",
44
- "src/ or src/keycloak-theme or src/keycloak_theme.",
45
- "Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
46
- ].join("\n")
47
- );
50
+ console.log(
51
+ chalk.red("Can't locate your theme source directory. It should be either: ")
52
+ );
48
53
 
49
- process.exit(-1);
54
+ process.exit(-1);
55
+ })();
56
+
57
+ cache = { projectDirPath, themeSrcDirPath };
58
+
59
+ return { themeSrcDirPath };
50
60
  }
61
+
62
+ const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
@@ -1,84 +1,40 @@
1
1
  import { join as pathJoin, dirname as pathDirname } from "path";
2
2
  import type { ThemeType } from "./constants";
3
3
  import * as fs from "fs";
4
- import { assert } from "tsafe/assert";
5
- import { extractArchive } from "../tools/extractArchive";
6
4
 
7
5
  export type MetaInfKeycloakTheme = {
8
6
  themes: { name: string; types: (ThemeType | "email")[] }[];
9
7
  };
10
8
 
11
- export function getMetaInfKeycloakThemesJsonFilePath(params: {
12
- resourcesDirPath: string;
13
- }) {
14
- const { resourcesDirPath } = params;
15
-
16
- return pathJoin(
17
- resourcesDirPath === "." ? "" : resourcesDirPath,
18
- "META-INF",
19
- "keycloak-themes.json"
20
- );
21
- }
22
-
23
- export function readMetaInfKeycloakThemes_fromResourcesDirPath(params: {
9
+ export function writeMetaInfKeycloakThemes(params: {
24
10
  resourcesDirPath: string;
11
+ getNewMetaInfKeycloakTheme: (params: {
12
+ metaInfKeycloakTheme: MetaInfKeycloakTheme | undefined;
13
+ }) => MetaInfKeycloakTheme;
25
14
  }) {
26
- const { resourcesDirPath } = params;
27
-
28
- return JSON.parse(
29
- fs
30
- .readFileSync(
31
- getMetaInfKeycloakThemesJsonFilePath({
32
- resourcesDirPath
33
- })
34
- )
35
- .toString("utf8")
36
- ) as MetaInfKeycloakTheme;
37
- }
15
+ const { resourcesDirPath, getNewMetaInfKeycloakTheme } = params;
38
16
 
39
- export async function readMetaInfKeycloakThemes_fromJar(params: {
40
- jarFilePath: string;
41
- }): Promise<MetaInfKeycloakTheme> {
42
- const { jarFilePath } = params;
43
- let metaInfKeycloakThemes: MetaInfKeycloakTheme | undefined = undefined;
17
+ const filePath = pathJoin(resourcesDirPath, "META-INF", "keycloak-themes.json");
44
18
 
45
- await extractArchive({
46
- archiveFilePath: jarFilePath,
47
- onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => {
48
- if (
49
- relativeFilePathInArchive ===
50
- getMetaInfKeycloakThemesJsonFilePath({ resourcesDirPath: "." })
51
- ) {
52
- metaInfKeycloakThemes = JSON.parse((await readFile()).toString("utf8"));
53
- earlyExit();
54
- }
55
- }
56
- });
57
-
58
- assert(metaInfKeycloakThemes !== undefined);
59
-
60
- return metaInfKeycloakThemes;
61
- }
62
-
63
- export function writeMetaInfKeycloakThemes(params: {
64
- resourcesDirPath: string;
65
- metaInfKeycloakThemes: MetaInfKeycloakTheme;
66
- }) {
67
- const { resourcesDirPath, metaInfKeycloakThemes } = params;
19
+ const currentMetaInfKeycloakTheme = !fs.existsSync(filePath)
20
+ ? undefined
21
+ : (JSON.parse(
22
+ fs.readFileSync(filePath).toString("utf8")
23
+ ) as MetaInfKeycloakTheme);
68
24
 
69
- const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({
70
- resourcesDirPath
25
+ const newMetaInfKeycloakThemes = getNewMetaInfKeycloakTheme({
26
+ metaInfKeycloakTheme: currentMetaInfKeycloakTheme
71
27
  });
72
28
 
73
29
  {
74
- const dirPath = pathDirname(metaInfKeycloakThemesJsonPath);
30
+ const dirPath = pathDirname(filePath);
75
31
  if (!fs.existsSync(dirPath)) {
76
32
  fs.mkdirSync(dirPath, { recursive: true });
77
33
  }
78
34
  }
79
35
 
80
36
  fs.writeFileSync(
81
- metaInfKeycloakThemesJsonPath,
82
- Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8")
37
+ filePath,
38
+ Buffer.from(JSON.stringify(newMetaInfKeycloakThemes, null, 2), "utf8")
83
39
  );
84
40
  }
@@ -109,7 +109,7 @@ export async function appBuild(params: {
109
109
 
110
110
  const dResult = new Deferred<{ isSuccess: boolean }>();
111
111
 
112
- const child = child_process.spawn(command, args, { cwd });
112
+ const child = child_process.spawn(command, args, { cwd, shell: true });
113
113
 
114
114
  child.stdout.on("data", data => {
115
115
  if (data.toString("utf8").includes("gzip:")) {
@@ -14,7 +14,7 @@ export type BuildContextLike = {
14
14
  assert<BuildContext extends BuildContextLike ? true : false>();
15
15
 
16
16
  export async function keycloakifyBuild(params: {
17
- onlyBuildJarFileBasename: string | undefined;
17
+ onlyBuildJarFileBasename: string;
18
18
  buildContext: BuildContextLike;
19
19
  }): Promise<{ isKeycloakifyBuildSuccess: boolean }> {
20
20
  const { buildContext, onlyBuildJarFileBasename } = params;
@@ -26,7 +26,8 @@ export async function keycloakifyBuild(params: {
26
26
  env: {
27
27
  ...process.env,
28
28
  [onlyBuildJarFileBasenameEnvName]: onlyBuildJarFileBasename
29
- }
29
+ },
30
+ shell: true
30
31
  });
31
32
 
32
33
  child.stdout.on("data", data => process.stdout.write(data));