@ahmedrowaihi/pdf-forge-cli 1.0.0-canary.0

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.
@@ -0,0 +1,49 @@
1
+ import { parse } from '@babel/parser';
2
+
3
+ import traverseModule from '@babel/traverse';
4
+
5
+ const traverse =
6
+ // we keep this check here so that this still works with the dev:preview
7
+ // script's use of tsx
8
+ typeof traverseModule === 'function'
9
+ ? traverseModule
10
+ : traverseModule.default;
11
+
12
+ export const getImportedModules = (contents: string) => {
13
+ const importedPaths: string[] = [];
14
+ const parsedContents = parse(contents, {
15
+ sourceType: 'unambiguous',
16
+ strictMode: false,
17
+ errorRecovery: true,
18
+ plugins: ['jsx', 'typescript', 'decorators'],
19
+ });
20
+
21
+ traverse(parsedContents, {
22
+ ImportDeclaration({ node }) {
23
+ importedPaths.push(node.source.value);
24
+ },
25
+ ExportAllDeclaration({ node }) {
26
+ importedPaths.push(node.source.value);
27
+ },
28
+ ExportNamedDeclaration({ node }) {
29
+ if (node.source) {
30
+ importedPaths.push(node.source.value);
31
+ }
32
+ },
33
+ TSExternalModuleReference({ node }) {
34
+ importedPaths.push(node.expression.value);
35
+ },
36
+ CallExpression({ node }) {
37
+ if ('name' in node.callee && node.callee.name === 'require') {
38
+ if (node.arguments.length === 1) {
39
+ const importPathNode = node.arguments[0]!;
40
+ if (importPathNode.type === 'StringLiteral') {
41
+ importedPaths.push(importPathNode.value);
42
+ }
43
+ }
44
+ }
45
+ },
46
+ });
47
+
48
+ return importedPaths;
49
+ };
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import { createMatchPath, loadConfig } from 'tsconfig-paths';
3
+
4
+ export const resolvePathAliases = (
5
+ importPaths: string[],
6
+ projectPath: string,
7
+ ) => {
8
+ const configLoadResult = loadConfig(projectPath);
9
+
10
+ if (configLoadResult.resultType === 'success') {
11
+ const matchPath = createMatchPath(
12
+ configLoadResult.absoluteBaseUrl,
13
+ configLoadResult.paths,
14
+ );
15
+ return importPaths.map((importedPath) => {
16
+ const unaliasedPath = matchPath(importedPath, undefined, undefined, [
17
+ '.tsx',
18
+ '.ts',
19
+ '.js',
20
+ '.jsx',
21
+ '.cjs',
22
+ '.mjs',
23
+ ]);
24
+ if (unaliasedPath) {
25
+ return `./${path.relative(projectPath, unaliasedPath)}`;
26
+ }
27
+ return importedPath;
28
+ });
29
+ }
30
+
31
+ return importPaths;
32
+ };
@@ -0,0 +1,125 @@
1
+ import type http from 'node:http';
2
+ import path from 'node:path';
3
+ import { watch } from 'chokidar';
4
+ import debounce from 'debounce';
5
+ import { type Socket, Server as SocketServer } from 'socket.io';
6
+ import type { HotReloadChange } from '../../types/hot-reload-change.js';
7
+ import { createDependencyGraph } from './create-dependency-graph.js';
8
+
9
+ export const setupHotreloading = async (
10
+ devServer: http.Server,
11
+ templatesDirRelativePath: string,
12
+ ) => {
13
+ let clients: Socket[] = [];
14
+ const io = new SocketServer(devServer);
15
+
16
+ io.on('connection', (client) => {
17
+ clients.push(client);
18
+
19
+ client.on('disconnect', () => {
20
+ clients = clients.filter((item) => item !== client);
21
+ });
22
+ });
23
+
24
+ // used to keep track of all changes
25
+ // and send them at once to the preview app through the web socket
26
+ let changes = [] as HotReloadChange[];
27
+
28
+ const reload = debounce(() => {
29
+ // we detect these using the useHotreload hook on the Next app
30
+ clients.forEach((client) => {
31
+ client.emit(
32
+ 'reload',
33
+ changes.filter((change) =>
34
+ // Ensures only changes inside the templates directory are emitted
35
+ path
36
+ .resolve(absolutePathToTemplatesDirectory, change.filename)
37
+ .startsWith(absolutePathToTemplatesDirectory),
38
+ ),
39
+ );
40
+ });
41
+
42
+ changes = [];
43
+ }, 150);
44
+
45
+ const absolutePathToTemplatesDirectory = path.resolve(
46
+ process.cwd(),
47
+ templatesDirRelativePath,
48
+ );
49
+
50
+ const [dependencyGraph, updateDependencyGraph, { resolveDependentsOf }] =
51
+ await createDependencyGraph(absolutePathToTemplatesDirectory);
52
+
53
+ const watcher = watch('', {
54
+ ignoreInitial: true,
55
+ cwd: absolutePathToTemplatesDirectory,
56
+ });
57
+
58
+ const getFilesOutsideTemplatesDirectory = () =>
59
+ Object.keys(dependencyGraph).filter((p) =>
60
+ path.relative(absolutePathToTemplatesDirectory, p).startsWith('..'),
61
+ );
62
+ let filesOutsideTemplatesDirectory = getFilesOutsideTemplatesDirectory();
63
+ // adds in to be watched separately all of the files that are outside of
64
+ // the user's templates directory
65
+ for (const p of filesOutsideTemplatesDirectory) {
66
+ watcher.add(p);
67
+ }
68
+
69
+ const exit = async () => {
70
+ await watcher.close();
71
+ };
72
+ process.on('SIGINT', exit);
73
+ process.on('uncaughtException', exit);
74
+
75
+ watcher.on('all', async (event, relativePathToChangeTarget) => {
76
+ const file = relativePathToChangeTarget.split(path.sep);
77
+ if (file.length === 0) {
78
+ return;
79
+ }
80
+ const pathToChangeTarget = path.resolve(
81
+ absolutePathToTemplatesDirectory,
82
+ relativePathToChangeTarget,
83
+ );
84
+
85
+ await updateDependencyGraph(event, pathToChangeTarget);
86
+
87
+ const newFilesOutsideTemplatesDirectory =
88
+ getFilesOutsideTemplatesDirectory();
89
+ // updates the files outside of the user's templates directory by unwatching
90
+ // the inexistent ones and watching the new ones
91
+ //
92
+ // Update watched files outside templates directory to handle dependency changes
93
+ for (const p of filesOutsideTemplatesDirectory) {
94
+ if (!newFilesOutsideTemplatesDirectory.includes(p)) {
95
+ watcher.unwatch(p);
96
+ }
97
+ }
98
+ for (const p of newFilesOutsideTemplatesDirectory) {
99
+ if (!filesOutsideTemplatesDirectory.includes(p)) {
100
+ watcher.add(p);
101
+ }
102
+ }
103
+ filesOutsideTemplatesDirectory = newFilesOutsideTemplatesDirectory;
104
+
105
+ changes.push({
106
+ event,
107
+ filename: relativePathToChangeTarget,
108
+ });
109
+
110
+ // These dependents are dependents resolved recursively, so even dependents of dependents
111
+ // will be notified of this change so that we ensure that things are updated in the preview.
112
+ for (const dependentPath of resolveDependentsOf(pathToChangeTarget)) {
113
+ changes.push({
114
+ event: 'change' as const,
115
+ filename: path.relative(
116
+ absolutePathToTemplatesDirectory,
117
+ dependentPath,
118
+ ),
119
+ });
120
+ }
121
+ reload();
122
+ });
123
+
124
+ return watcher;
125
+ };
@@ -0,0 +1,134 @@
1
+ import { existsSync, promises as fs } from 'node:fs';
2
+ import type http from 'node:http';
3
+ import path from 'node:path';
4
+ import type url from 'node:url';
5
+ import { lookup } from 'mime-types';
6
+
7
+ /**
8
+ * Extracts the template directory path from the referer URL.
9
+ * @param referer - The referer header value
10
+ * @returns The template directory path, or null if not found
11
+ */
12
+ const extractTemplatePathFromReferer = (referer: string): string | null => {
13
+ try {
14
+ const refererUrl = new URL(referer);
15
+ const previewMatch = refererUrl.pathname.match(/\/preview\/(.+)$/);
16
+ if (!previewMatch?.[1]) {
17
+ return null;
18
+ }
19
+
20
+ const templateSlug = previewMatch[1];
21
+ return templateSlug.replace(/\.(tsx|jsx|ts|js)$/, '');
22
+ } catch {
23
+ return null;
24
+ }
25
+ };
26
+
27
+ /**
28
+ * Recursively searches for a static file starting from the template directory
29
+ * and traversing up to the templates root directory.
30
+ * @param templateFullPath - Full path to the template directory
31
+ * @param templatesDirResolved - Resolved path to the templates root directory
32
+ * @param relativeFilePath - Relative path to the file within the static folder
33
+ * @returns Absolute path to the found file, or null if not found
34
+ */
35
+ const findStaticFileRecursively = (
36
+ templateFullPath: string,
37
+ templatesDirResolved: string,
38
+ relativeFilePath: string,
39
+ ): string | null => {
40
+ let currentDir = templateFullPath;
41
+
42
+ while (currentDir.startsWith(templatesDirResolved)) {
43
+ const staticPath = path.join(currentDir, 'static', relativeFilePath);
44
+ if (existsSync(staticPath)) {
45
+ return staticPath;
46
+ }
47
+
48
+ // Move up one directory level
49
+ const parentDir = path.dirname(currentDir);
50
+ if (parentDir === currentDir) {
51
+ // Reached filesystem root
52
+ break;
53
+ }
54
+ currentDir = parentDir;
55
+ }
56
+
57
+ return null;
58
+ };
59
+
60
+ export const serveStaticFile = async (
61
+ res: http.ServerResponse,
62
+ parsedUrl: url.UrlWithParsedQuery,
63
+ staticDirRelativePath: string,
64
+ templatesDirRelativePath?: string,
65
+ req?: http.IncomingMessage,
66
+ ) => {
67
+ const originalPath = parsedUrl.pathname!;
68
+ const pathname = originalPath.startsWith('/static')
69
+ ? originalPath.replace('/static', './static')
70
+ : `./static${originalPath}`;
71
+ const ext = path.parse(pathname).ext;
72
+
73
+ const staticBaseDir = path.resolve(process.cwd(), staticDirRelativePath);
74
+ let fileAbsolutePath: string | null = null;
75
+
76
+ if (templatesDirRelativePath && req?.headers.referer) {
77
+ const templateDirPath = extractTemplatePathFromReferer(req.headers.referer);
78
+
79
+ if (templateDirPath) {
80
+ const templatesDir = path.resolve(
81
+ process.cwd(),
82
+ templatesDirRelativePath,
83
+ );
84
+ const templateFullPath = path.join(templatesDir, templateDirPath);
85
+ const relativeFilePath = pathname.replace('./static/', '');
86
+ const templatesDirResolved = path.resolve(templatesDir);
87
+
88
+ fileAbsolutePath = findStaticFileRecursively(
89
+ templateFullPath,
90
+ templatesDirResolved,
91
+ relativeFilePath,
92
+ );
93
+ }
94
+ }
95
+
96
+ // Fallback to root static directory
97
+ if (!fileAbsolutePath) {
98
+ fileAbsolutePath = path.resolve(staticBaseDir, pathname);
99
+ if (!fileAbsolutePath.startsWith(staticBaseDir)) {
100
+ res.statusCode = 403;
101
+ res.end();
102
+ return;
103
+ }
104
+ }
105
+
106
+ try {
107
+ const fileHandle = await fs.open(fileAbsolutePath, 'r');
108
+
109
+ const fileData = await fs.readFile(fileHandle);
110
+
111
+ // if the file is found, set Content-type and send data
112
+ res.setHeader('Content-type', lookup(ext) || 'text/plain');
113
+ res.end(fileData);
114
+
115
+ void fileHandle.close();
116
+ } catch (exception) {
117
+ if (!existsSync(fileAbsolutePath)) {
118
+ res.statusCode = 404;
119
+ res.end();
120
+ } else {
121
+ const sanitizedFilePath = fileAbsolutePath.replace(/\n|\r/g, '');
122
+ console.error(
123
+ `Could not read file at %s to be served, here's the exception:`,
124
+ sanitizedFilePath,
125
+ exception,
126
+ );
127
+
128
+ res.statusCode = 500;
129
+ res.end(
130
+ 'Could not read file to be served! Check your terminal for more information.',
131
+ );
132
+ }
133
+ }
134
+ };
@@ -0,0 +1,242 @@
1
+ import http from 'node:http';
2
+ import path from 'node:path';
3
+ import url from 'node:url';
4
+ import { createJiti } from 'jiti';
5
+ import logSymbols from 'log-symbols';
6
+ import ora from 'ora';
7
+ import { getPreviewServerLocation } from '../get-preview-server-location.js';
8
+ import { packageJson } from '../packageJson.js';
9
+ import { registerSpinnerAutostopping } from '../register-spinner-autostopping.js';
10
+ import { styleText } from '../style-text.js';
11
+ import { getEnvVariablesForPreviewApp } from './get-env-variables-for-preview-app.js';
12
+ import { serveStaticFile } from './serve-static-file.js';
13
+
14
+ let devServer: http.Server | undefined;
15
+
16
+ const safeAsyncServerListen = (server: http.Server, port: number) => {
17
+ return new Promise<{ portAlreadyInUse: boolean }>((resolve) => {
18
+ server.listen(port, () => {
19
+ resolve({ portAlreadyInUse: false });
20
+ });
21
+
22
+ server.on('error', (e: NodeJS.ErrnoException) => {
23
+ if (e.code === 'EADDRINUSE') {
24
+ resolve({ portAlreadyInUse: true });
25
+ }
26
+ });
27
+ });
28
+ };
29
+
30
+ export const startDevServer = async (
31
+ templatesDirRelativePath: string,
32
+ staticBaseDirRelativePath: string,
33
+ port: number,
34
+ ): Promise<http.Server> => {
35
+ const [majorNodeVersion] = process.versions.node.split('.');
36
+ if (majorNodeVersion && Number.parseInt(majorNodeVersion, 10) < 20) {
37
+ console.error(
38
+ ` ${logSymbols.error} Node ${majorNodeVersion} is not supported. Please upgrade to Node 20 or higher.`,
39
+ );
40
+ process.exit(1);
41
+ }
42
+
43
+ const previewServerLocation = await getPreviewServerLocation();
44
+ const previewServer = createJiti(previewServerLocation);
45
+
46
+ devServer = http.createServer((req, res) => {
47
+ if (!req.url) {
48
+ res.end(404);
49
+ return;
50
+ }
51
+
52
+ const parsedUrl = url.parse(req.url, true);
53
+
54
+ // Never cache anything to avoid
55
+ res.setHeader(
56
+ 'Cache-Control',
57
+ 'no-cache, max-age=0, must-revalidate, no-store',
58
+ );
59
+ res.setHeader('Pragma', 'no-cache');
60
+ res.setHeader('Expires', '-1');
61
+
62
+ try {
63
+ if (
64
+ parsedUrl.path &&
65
+ !parsedUrl.path.startsWith('/preview/') &&
66
+ !parsedUrl.path.startsWith('/api/') &&
67
+ !parsedUrl.path.includes('_next/')
68
+ ) {
69
+ void serveStaticFile(
70
+ res,
71
+ parsedUrl,
72
+ staticBaseDirRelativePath,
73
+ templatesDirRelativePath,
74
+ req,
75
+ );
76
+ } else if (!isNextReady) {
77
+ void nextReadyPromise.then(() =>
78
+ nextHandleRequest?.(req, res, parsedUrl),
79
+ );
80
+ } else {
81
+ void nextHandleRequest?.(req, res, parsedUrl);
82
+ }
83
+ } catch (e) {
84
+ console.error('caught error', e);
85
+
86
+ res.writeHead(500);
87
+ res.end();
88
+ }
89
+ });
90
+
91
+ const { portAlreadyInUse } = await safeAsyncServerListen(devServer, port);
92
+
93
+ if (!portAlreadyInUse) {
94
+ console.log(
95
+ styleText('greenBright', ` React PDF ${packageJson.version}`),
96
+ );
97
+ console.log(` Running preview at: http://localhost:${port}\n`);
98
+ } else {
99
+ const nextPortToTry = port + 1;
100
+ console.warn(
101
+ ` ${logSymbols.warning} Port ${port} is already in use, trying ${nextPortToTry}`,
102
+ );
103
+ return startDevServer(
104
+ templatesDirRelativePath,
105
+ staticBaseDirRelativePath,
106
+ nextPortToTry,
107
+ );
108
+ }
109
+
110
+ devServer.on('close', () => {
111
+ void app.close();
112
+ });
113
+
114
+ devServer.on('error', (e: NodeJS.ErrnoException) => {
115
+ spinner.stopAndPersist({
116
+ symbol: logSymbols.error,
117
+ text: `Preview Server had an error: ${e.message}`,
118
+ });
119
+ process.exit(1);
120
+ });
121
+
122
+ const spinner = ora({
123
+ text: 'Getting react-pdf preview server ready...\n',
124
+ prefixText: ' ',
125
+ }).start();
126
+
127
+ registerSpinnerAutostopping(spinner);
128
+ const timeBeforeNextReady = performance.now();
129
+
130
+ // these environment variables are used on the next app
131
+ // this is the most reliable way of communicating these paths through
132
+ process.env = {
133
+ NODE_ENV: 'development',
134
+ ...(process.env as Omit<NodeJS.ProcessEnv, 'NODE_ENV'> & {
135
+ NODE_ENV?: NodeJS.ProcessEnv['NODE_ENV'];
136
+ }),
137
+ ...getEnvVariablesForPreviewApp(
138
+ // Normalize paths to avoid issues with path resolution
139
+ path.normalize(templatesDirRelativePath),
140
+ previewServerLocation,
141
+ process.cwd(),
142
+ ),
143
+ };
144
+
145
+ const next = await previewServer.import<typeof import('next')['default']>(
146
+ 'next',
147
+ {
148
+ default: true,
149
+ },
150
+ );
151
+
152
+ const app = next({
153
+ // passing in env here does not get the environment variables there
154
+ dev: false,
155
+ conf: {
156
+ images: {
157
+ // This is to avoid the warning with sharp
158
+ unoptimized: true,
159
+ },
160
+ },
161
+ hostname: 'localhost',
162
+ port,
163
+ dir: previewServerLocation,
164
+ });
165
+
166
+ let isNextReady = false;
167
+ const nextReadyPromise = app.prepare();
168
+ try {
169
+ await nextReadyPromise;
170
+ } catch (exception) {
171
+ spinner.stopAndPersist({
172
+ symbol: logSymbols.error,
173
+ text: ` Preview Server had an error: ${exception}`,
174
+ });
175
+ process.exit(1);
176
+ }
177
+ isNextReady = true;
178
+
179
+ const nextHandleRequest:
180
+ | ReturnType<typeof app.getRequestHandler>
181
+ | undefined = app.getRequestHandler();
182
+
183
+ const secondsToNextReady = (
184
+ (performance.now() - timeBeforeNextReady) /
185
+ 1000
186
+ ).toFixed(1);
187
+
188
+ spinner.stopAndPersist({
189
+ text: `Ready in ${secondsToNextReady}s\n`,
190
+ symbol: logSymbols.success,
191
+ });
192
+
193
+ return devServer;
194
+ };
195
+
196
+ // based on https://stackoverflow.com/a/14032965
197
+ const makeExitHandler =
198
+ (
199
+ options?:
200
+ | { shouldKillProcess: false }
201
+ | { shouldKillProcess: true; killWithErrorCode: boolean },
202
+ ) =>
203
+ (codeSignalOrError: number | NodeJS.Signals | Error) => {
204
+ if (typeof devServer !== 'undefined') {
205
+ console.log('\nshutting down dev server');
206
+ devServer.close();
207
+ devServer = undefined;
208
+ }
209
+
210
+ if (codeSignalOrError instanceof Error) {
211
+ console.error(codeSignalOrError);
212
+ }
213
+
214
+ if (options?.shouldKillProcess) {
215
+ process.exit(options.killWithErrorCode ? 1 : 0);
216
+ }
217
+ };
218
+
219
+ // do something when app is closing
220
+ process.on('exit', makeExitHandler());
221
+
222
+ // catches ctrl+c event
223
+ process.on(
224
+ 'SIGINT',
225
+ makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false }),
226
+ );
227
+
228
+ // catches "kill pid" (for example: nodemon restart)
229
+ process.on(
230
+ 'SIGUSR1',
231
+ makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false }),
232
+ );
233
+ process.on(
234
+ 'SIGUSR2',
235
+ makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false }),
236
+ );
237
+
238
+ // catches uncaught exceptions
239
+ process.on(
240
+ 'uncaughtException',
241
+ makeExitHandler({ shouldKillProcess: true, killWithErrorCode: true }),
242
+ );
@@ -0,0 +1,29 @@
1
+ import logSymbols from 'log-symbols';
2
+ import type { Ora } from 'ora';
3
+
4
+ const spinners = new Set<Ora>();
5
+
6
+ process.on('SIGINT', () => {
7
+ spinners.forEach((spinner) => {
8
+ if (spinner.isSpinning) {
9
+ spinner.stop();
10
+ }
11
+ });
12
+ });
13
+
14
+ process.on('exit', (code) => {
15
+ if (code !== 0) {
16
+ spinners.forEach((spinner) => {
17
+ if (spinner.isSpinning) {
18
+ spinner.stopAndPersist({
19
+ symbol: logSymbols.error,
20
+ text: 'Process exited with error',
21
+ });
22
+ }
23
+ });
24
+ }
25
+ });
26
+
27
+ export const registerSpinnerAutostopping = (spinner: Ora) => {
28
+ spinners.add(spinner);
29
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Centralized fallback for Node versions (<20.12.0) without util.styleText.
3
+ * Returns the original text when styleText is unavailable.
4
+ */
5
+ import * as nodeUtil from 'node:util';
6
+
7
+ type StyleTextFunction = typeof nodeUtil.styleText;
8
+
9
+ export const styleText: StyleTextFunction = (nodeUtil as any).styleText
10
+ ? (nodeUtil as any).styleText
11
+ : (_: string, text: string) => text;
@@ -0,0 +1,76 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ const SYMBOLS = {
6
+ BRANCH: '├── ',
7
+ EMPTY: '',
8
+ INDENT: ' ',
9
+ LAST_BRANCH: '└── ',
10
+ VERTICAL: '│ ',
11
+ };
12
+
13
+ const getTreeLines = async (
14
+ dirPath: string,
15
+ depth: number,
16
+ currentDepth = 0,
17
+ ) => {
18
+ const base = process.cwd();
19
+ const dirFullpath = path.resolve(base, dirPath);
20
+ const dirname = path.basename(dirFullpath);
21
+ let lines = [dirname];
22
+
23
+ const dirStat = await fs.stat(dirFullpath);
24
+ if (dirStat.isDirectory() && currentDepth < depth) {
25
+ const childDirents = await fs.readdir(dirFullpath, { withFileTypes: true });
26
+
27
+ childDirents.sort((a, b) => {
28
+ // orders directories before files
29
+ if (a.isDirectory() && b.isFile()) {
30
+ return -1;
31
+ }
32
+
33
+ if (a.isFile() && b.isDirectory()) {
34
+ return 1;
35
+ }
36
+
37
+ // orders by name because they are the same type
38
+ // either directory & directory
39
+ // or file & file
40
+ return b.name > a.name ? -1 : 1;
41
+ });
42
+
43
+ for (let i = 0; i < childDirents.length; i++) {
44
+ const dirent = childDirents[i]!;
45
+ const isLast = i === childDirents.length - 1;
46
+
47
+ const branchingSymbol = isLast ? SYMBOLS.LAST_BRANCH : SYMBOLS.BRANCH;
48
+ const verticalSymbol = isLast ? SYMBOLS.INDENT : SYMBOLS.VERTICAL;
49
+
50
+ if (dirent.isFile()) {
51
+ lines.push(`${branchingSymbol}${dirent.name}`);
52
+ } else {
53
+ const pathToDirectory = path.join(dirFullpath, dirent.name);
54
+ const treeLinesForSubDirectory = await getTreeLines(
55
+ pathToDirectory,
56
+ depth,
57
+ currentDepth + 1,
58
+ );
59
+ lines = lines.concat(
60
+ treeLinesForSubDirectory.map((line, index) =>
61
+ index === 0
62
+ ? `${branchingSymbol}${line}`
63
+ : `${verticalSymbol}${line}`,
64
+ ),
65
+ );
66
+ }
67
+ }
68
+ }
69
+
70
+ return lines;
71
+ };
72
+
73
+ export const tree = async (dirPath: string, depth: number) => {
74
+ const lines = await getTreeLines(dirPath, depth);
75
+ return lines.join(os.EOL);
76
+ };