@modern-js/utils 1.1.6 → 1.2.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 (61) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/js/modern/constants.js +2 -1
  3. package/dist/js/modern/getBrowserslist.js +1 -1
  4. package/dist/js/modern/prettyInstructions.js +1 -1
  5. package/dist/js/node/constants.js +2 -1
  6. package/dist/js/node/getBrowserslist.js +2 -2
  7. package/dist/js/node/prettyInstructions.js +1 -1
  8. package/dist/js/treeshaking/constants.js +2 -1
  9. package/dist/js/treeshaking/getBrowserslist.js +1 -1
  10. package/dist/js/treeshaking/prettyInstructions.js +1 -1
  11. package/dist/types/getBrowserslist.d.ts +1 -1
  12. package/jest.config.js +8 -0
  13. package/package.json +18 -12
  14. package/tests/applyOptionsChain.test.ts +1 -1
  15. package/tests/compatRequire.test.ts +1 -1
  16. package/tests/debug.test.ts +4 -2
  17. package/tests/ensureAbsolutePath.test.ts +1 -1
  18. package/tests/findExists.test.ts +1 -1
  19. package/tests/getBrowserslist.test.ts +1 -1
  20. package/tests/getCacheIdentifier.test.ts +1 -1
  21. package/tests/getEntryOptions.test.ts +1 -1
  22. package/tests/index.test.ts +1 -1
  23. package/tests/logger.test.ts +1 -1
  24. package/tests/removeSlash.test.ts +1 -1
  25. package/tests/tsconfig.json +1 -3
  26. package/tsconfig.json +1 -3
  27. package/src/FileSizeReporter.ts +0 -181
  28. package/src/alias.ts +0 -90
  29. package/src/applyOptionsChain.ts +0 -44
  30. package/src/chalk.ts +0 -3
  31. package/src/clearConsole.ts +0 -5
  32. package/src/compatRequire.ts +0 -25
  33. package/src/constants.ts +0 -262
  34. package/src/debug.ts +0 -8
  35. package/src/ensureAbsolutePath.ts +0 -10
  36. package/src/findExists.ts +0 -15
  37. package/src/formatWebpackMessages.ts +0 -131
  38. package/src/generateMetaTags.ts +0 -75
  39. package/src/getBrowserslist.ts +0 -6
  40. package/src/getCacheIdentifier.ts +0 -30
  41. package/src/getEntryOptions.ts +0 -37
  42. package/src/getPackageManager.ts +0 -30
  43. package/src/getPort.ts +0 -62
  44. package/src/import.ts +0 -10
  45. package/src/index.ts +0 -31
  46. package/src/is/index.ts +0 -78
  47. package/src/is/node-env.ts +0 -9
  48. package/src/is/platform.ts +0 -7
  49. package/src/is/type.ts +0 -43
  50. package/src/logger.ts +0 -184
  51. package/src/monorepo.ts +0 -106
  52. package/src/nodeEnv.ts +0 -28
  53. package/src/path.ts +0 -9
  54. package/src/pkgUp.ts +0 -3
  55. package/src/prettyInstructions.ts +0 -88
  56. package/src/printBuildError.ts +0 -47
  57. package/src/readTsConfig.ts +0 -21
  58. package/src/removeSlash.ts +0 -6
  59. package/src/runtimeExports.ts +0 -68
  60. package/src/types.d.ts +0 -1
  61. package/src/watch.ts +0 -56
package/src/getPort.ts DELETED
@@ -1,62 +0,0 @@
1
- import net from 'net';
2
- import { chalk } from './chalk';
3
- import { logger } from './logger';
4
-
5
- /**
6
- * Get available free port.
7
- * @param port - Current port want to use.
8
- * @param tryLimits - Maximum number of retries.
9
- * @returns Available port number.
10
- */
11
- /* eslint-disable no-param-reassign, @typescript-eslint/no-loop-func */
12
- export const getPort = async (
13
- port: string | number,
14
- tryLimits = 20,
15
- ): Promise<number> => {
16
- if (typeof port === 'string') {
17
- port = parseInt(port, 10);
18
- }
19
-
20
- const original = port;
21
-
22
- let found = false;
23
- let attempts = 0;
24
- while (!found && attempts <= tryLimits) {
25
- try {
26
- await new Promise((resolve, reject) => {
27
- const server = net.createServer();
28
- server.unref();
29
- server.on('error', reject);
30
- server.listen(
31
- {
32
- port,
33
- host: '0.0.0.0',
34
- },
35
- () => {
36
- found = true;
37
- server.close(resolve);
38
- },
39
- );
40
- });
41
- } catch (e: any) {
42
- if (e.code !== 'EADDRINUSE') {
43
- throw e;
44
- }
45
- port++;
46
- attempts++;
47
- }
48
- }
49
-
50
- if (port !== original) {
51
- logger.info(
52
- chalk.red(
53
- `Something is already running on port ${original}. ${chalk.yellow(
54
- `Use port ${port} instead.`,
55
- )}`,
56
- ),
57
- );
58
- }
59
-
60
- return port;
61
- };
62
- /* eslint-enable no-param-reassign, @typescript-eslint/no-loop-func */
package/src/import.ts DELETED
@@ -1,10 +0,0 @@
1
- // cover: https://rushstack.io/pages/api/node-core-library.import.lazy/
2
- const lazy = (moduleName: string, requireFn: (id: string) => unknown): any => {
3
- const importLazyLocal: (moduleName: string) => unknown =
4
- require('import-lazy')(requireFn);
5
- return importLazyLocal(moduleName);
6
- };
7
-
8
- export { lazy as lazyImport };
9
-
10
- export const Import = { lazy };
package/src/index.ts DELETED
@@ -1,31 +0,0 @@
1
- export * as fs from 'fs-extra';
2
- export * from './chalk';
3
- export * from './formatWebpackMessages';
4
- export * from './FileSizeReporter';
5
- export * from './printBuildError';
6
- export * from './debug';
7
- export * from './findExists';
8
- export * from './is';
9
- export * from './compatRequire';
10
- export * from './logger';
11
- export * from './constants';
12
- export * from './ensureAbsolutePath';
13
- export * from './getCacheIdentifier';
14
- export * from './clearConsole';
15
- export * from './pkgUp';
16
- export * from './applyOptionsChain';
17
- export * from './getBrowserslist';
18
- export * from './removeSlash';
19
- export * from './getEntryOptions';
20
- export * from './getPort';
21
- export * from './monorepo';
22
- export * from './getPackageManager';
23
- export * from './runtimeExports';
24
- export * from './readTsConfig';
25
- export * from './path';
26
- export * from './generateMetaTags';
27
- export * from './prettyInstructions';
28
- export * from './alias';
29
- export * from './import';
30
- export * from './watch';
31
- export * from './nodeEnv';
package/src/is/index.ts DELETED
@@ -1,78 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { isDev } from './node-env';
4
-
5
- /**
6
- * Check if the package name is in dependencies or devDependencies.
7
- *
8
- * @param appDirectory - Project root directory.
9
- * @param name - Package name.
10
- * @returns True if the name is in dependencies or devDependencies, false otherwise.
11
- */
12
- export const isDepExists = (appDirectory: string, name: string): boolean => {
13
- const json = require(path.resolve(appDirectory, './package.json'));
14
-
15
- const { dependencies = {}, devDependencies = {} } = json;
16
-
17
- return (
18
- dependencies.hasOwnProperty(name) || devDependencies.hasOwnProperty(name)
19
- );
20
- };
21
-
22
- /**
23
- * Is typescript project.
24
- *
25
- * @param root - App directory.
26
- * @returns Whether to use typescript.
27
- */
28
- export const isTypescript = (root: string): boolean =>
29
- fs.existsSync(path.resolve(root, './tsconfig.json'));
30
-
31
- /**
32
- * Is Empty object
33
- *
34
- * @param o - Any object.
35
- * @returns Whether it is empty object.
36
- */
37
- export const isEmpty = (o: Record<string, unknown>) =>
38
- Object.entries(o).length === 0 && o.constructor === Object;
39
-
40
- /**
41
- * Is SSR project
42
- *
43
- * @param config - User config.
44
- * @returns Whether to use server side render.
45
- */
46
- export const isSSR = (config: any): boolean => {
47
- const { server } = config;
48
-
49
- if (server?.ssr) {
50
- return true;
51
- }
52
-
53
- if (server?.ssrByEntries && !isEmpty(server.ssrByEntries)) {
54
- for (const name of Object.keys(server.ssrByEntries)) {
55
- if (server.ssrByEntries[name]) {
56
- return true;
57
- }
58
- }
59
- }
60
-
61
- return false;
62
- };
63
-
64
- export const isUseSSRBundle = (config: any): boolean => {
65
- const { output } = config;
66
- if (output?.ssg) {
67
- return true;
68
- }
69
-
70
- return isSSR(config);
71
- };
72
-
73
- export const isFastRefresh = () =>
74
- isDev() && process.env.FAST_REFRESH !== 'false';
75
-
76
- export * from './node-env';
77
- export * from './platform';
78
- export * from './type';
@@ -1,9 +0,0 @@
1
- export const isDev = (): boolean => process.env.NODE_ENV === 'development';
2
-
3
- export const isProd = (): boolean => process.env.NODE_ENV === 'production';
4
-
5
- export const isTest = () => process.env.NODE_ENV === 'test';
6
-
7
- // Variable used for enabling profiling in Production.
8
- export const isProdProfile = () =>
9
- isProd() && process.argv.includes('--profile');
@@ -1,7 +0,0 @@
1
- export const isNodeJS = (): boolean =>
2
- typeof process !== 'undefined' &&
3
- process.versions != null &&
4
- process.versions.node != null &&
5
- (process.versions as any).electron == null;
6
-
7
- export const isBrowser = (): boolean => typeof window !== 'undefined';
package/src/is/type.ts DELETED
@@ -1,43 +0,0 @@
1
- export function isString(str: any): str is string {
2
- return typeof str === 'string';
3
- }
4
-
5
- export function isUndefined(obj: any): obj is undefined {
6
- return typeof obj === 'undefined';
7
- }
8
-
9
- export function isArray(obj: any): obj is any[] {
10
- return Object.prototype.toString.call(obj) === '[object Array]';
11
- }
12
-
13
- // eslint-disable-next-line @typescript-eslint/ban-types
14
- export function isFunction(func: any): func is Function {
15
- return typeof func === 'function';
16
- }
17
-
18
- // eslint-disable-next-line @typescript-eslint/ban-types
19
- export function isObject(obj: any): obj is object {
20
- return typeof obj === 'object';
21
- }
22
-
23
- export function isPlainObject(obj: any): obj is Record<string, any> {
24
- return (
25
- obj &&
26
- typeof obj === 'object' &&
27
- Object.prototype.toString.call(obj) === '[object Object]'
28
- );
29
- }
30
-
31
- export function isPromise(obj: any): obj is Promise<any> {
32
- /* eslint-disable promise/prefer-await-to-then */
33
- return (
34
- Boolean(obj) &&
35
- (typeof obj === 'object' || typeof obj === 'function') &&
36
- typeof obj.then === 'function'
37
- );
38
- /* eslint-enable promise/prefer-await-to-then */
39
- }
40
-
41
- export function isRegExp(obj: any): obj is RegExp {
42
- return Object.prototype.toString.call(obj) === '[object RegExp]';
43
- }
package/src/logger.ts DELETED
@@ -1,184 +0,0 @@
1
- import chalk, { Color } from 'chalk';
2
-
3
- type LogLevel = 'debug' | 'info' | 'warn' | 'error';
4
-
5
- type LogMsg = number | string | Error;
6
-
7
- interface LoggerConfiguration {
8
- color?: typeof Color;
9
- label?: string;
10
- level?: LogLevel;
11
- }
12
-
13
- interface InstanceConfiguration {
14
- displayLabel?: boolean;
15
- underlineLabel?: boolean;
16
- uppercaseLabel?: boolean;
17
- }
18
-
19
- interface ConstructorOptions {
20
- config?: InstanceConfiguration;
21
- types?: Record<string, LoggerConfiguration>;
22
- }
23
-
24
- type LoggerFunction = (
25
- message?: number | string | Error,
26
- ...args: any[]
27
- ) => void;
28
-
29
- const { grey, underline } = chalk;
30
-
31
- const LOG_TYPES = {
32
- error: {
33
- color: 'red',
34
- label: 'error',
35
- level: 'error',
36
- },
37
- info: {
38
- color: 'blue',
39
- label: 'info',
40
- level: 'info',
41
- },
42
- warn: {
43
- color: 'yellow',
44
- label: 'warning',
45
- level: 'warn',
46
- },
47
- debug: {
48
- color: 'red',
49
- label: 'debug',
50
- level: 'debug',
51
- },
52
- log: { level: 'info' },
53
- };
54
-
55
- const DEFAULT_CONFIG = {
56
- displayLabel: true,
57
- underlineLabel: true,
58
- uppercaseLabel: false,
59
- };
60
-
61
- class Logger {
62
- private readonly logCount: number = 200;
63
-
64
- private history: Partial<Record<string, Array<string>>> = {};
65
-
66
- private readonly config: InstanceConfiguration;
67
-
68
- private readonly types: Record<string, LoggerConfiguration>;
69
-
70
- private readonly longestLabel: string;
71
-
72
- [key: string]: any;
73
-
74
- constructor(options: ConstructorOptions = {}) {
75
- this.config = { ...DEFAULT_CONFIG, ...(options.config || {}) };
76
- this.types = {
77
- ...(LOG_TYPES as Record<string, LoggerConfiguration>),
78
- ...(options.types || {}),
79
- };
80
- this.longestLabel = this.getLongestLabel();
81
-
82
- Object.keys(this.types).forEach(type => {
83
- this[type] = this._log.bind(this, type);
84
- });
85
- }
86
-
87
- private retainLog(type: string, message: string) {
88
- if (!this.history[type]) {
89
- this.history[type] = [];
90
- }
91
- this.history[type]!.push(message);
92
- while (this.history[type]!.length > this.logCount) {
93
- this.history[type]!.shift();
94
- }
95
- }
96
-
97
- // eslint-disable-next-line max-statements
98
- private _log(type: string, message?: LogMsg) {
99
- if (message === undefined) {
100
- // eslint-disable-next-line no-console
101
- console.log();
102
- return;
103
- }
104
-
105
- let label = '';
106
- let text = '';
107
- const logType = this.types[type];
108
-
109
- if (this.config.displayLabel && logType.label) {
110
- label = this.config.uppercaseLabel
111
- ? logType.label.toUpperCase()
112
- : logType.label;
113
-
114
- if (this.config.underlineLabel) {
115
- label = underline(label).padEnd(this.longestUnderlinedLabel.length + 1);
116
- } else {
117
- label = label.padEnd(this.longestLabel.length + 1);
118
- }
119
-
120
- label = logType.color ? chalk[logType.color](label) : label;
121
- }
122
-
123
- if (message instanceof Error) {
124
- if (message.stack) {
125
- const [name, ...rest] = message.stack.split('\n');
126
- text = `${name}\n${grey(rest.join('\n'))}`;
127
- } else {
128
- text = message.message;
129
- }
130
- } else {
131
- text = `${message}`;
132
- }
133
-
134
- // only retain logs of warn/error level
135
- if (logType.level === 'warn' || logType.level === 'error') {
136
- // retain log text without label
137
- this.retainLog(type, text);
138
- }
139
-
140
- const log = label.length > 0 ? `${label} ${text}` : text;
141
- // eslint-disable-next-line no-console
142
- console.log(log);
143
- }
144
-
145
- private getLongestLabel() {
146
- let longestLabel = '';
147
- Object.keys(this.types).forEach(type => {
148
- const { label = '' } = this.types[type];
149
- if (label.length > longestLabel.length) {
150
- longestLabel = label;
151
- }
152
- });
153
- return longestLabel;
154
- }
155
-
156
- private get longestUnderlinedLabel() {
157
- return underline(this.longestLabel);
158
- }
159
-
160
- getRetainedLogs(type: string) {
161
- return this.history[type] || [];
162
- }
163
-
164
- clearRetainedLogs(type: string) {
165
- if (type) {
166
- if (this.history[type]) {
167
- this.history[type] = [];
168
- }
169
- } else {
170
- this.history = {};
171
- }
172
- }
173
- }
174
-
175
- type LoggerInterface = {
176
- [key in keyof typeof LOG_TYPES]: LoggerFunction;
177
- };
178
-
179
- const logger = new Logger() as Logger & LoggerInterface;
180
-
181
- logger.Logger = Logger;
182
-
183
- export { logger };
184
- export type { LoggerInterface };
package/src/monorepo.ts DELETED
@@ -1,106 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import glob from 'glob';
4
- import yaml from 'yaml';
5
-
6
- const PACKAGE_MAX_DEPTH = 5;
7
-
8
- const WOKRSPACES_FILES = {
9
- YARN: 'package.json',
10
- PNPM: 'pnpm-workspaces.yaml',
11
- LERNA: 'lerna.json',
12
- };
13
-
14
- export const isLerna = (root: string) =>
15
- fs.existsSync(path.join(root, WOKRSPACES_FILES.LERNA));
16
-
17
- export const isYarnWorkspaces = (root: string) => {
18
- const pkg = path.join(root, WOKRSPACES_FILES.YARN);
19
-
20
- if (!fs.existsSync(pkg)) {
21
- return false;
22
- }
23
-
24
- const json = JSON.parse(fs.readFileSync(pkg, 'utf8'));
25
-
26
- return Boolean(json.workspaces?.packages);
27
- };
28
-
29
- export const isPnpmWorkspaces = (root: string) =>
30
- fs.existsSync(path.join(root, WOKRSPACES_FILES.PNPM));
31
-
32
- export const isMonorepo = (root: string) =>
33
- isLerna(root) || isYarnWorkspaces(root) || isPnpmWorkspaces(root);
34
-
35
- export const isModernjsMonorepo = (root: string) => {
36
- const json = JSON.parse(
37
- fs.readFileSync(path.join(root, 'package.json'), 'utf8'),
38
- );
39
-
40
- const deps = {
41
- ...(json.dependencies || {}),
42
- ...(json.devDependencies || {}),
43
- };
44
-
45
- return Boolean(deps['@modern-js/monorepo-tools']);
46
- };
47
-
48
- export const findMonorepoRoot = (
49
- appDirectory: string,
50
- maxDepth: number = PACKAGE_MAX_DEPTH,
51
- ) => {
52
- let inMonorepo = false;
53
-
54
- for (let depth = 0; depth < maxDepth; depth++) {
55
- if (isMonorepo(appDirectory)) {
56
- inMonorepo = true;
57
- break;
58
- }
59
- // eslint-disable-next-line no-param-reassign
60
- appDirectory = path.dirname(appDirectory);
61
- }
62
-
63
- return inMonorepo ? appDirectory : undefined;
64
- };
65
-
66
- export const getMonorepoPackages = (
67
- root: string,
68
- ): { name: string; path: string }[] => {
69
- let packages: string[] = [];
70
-
71
- if (isYarnWorkspaces(root)) {
72
- const json = JSON.parse(
73
- fs.readFileSync(path.join(root, 'package.json'), 'utf8'),
74
- );
75
- ({ packages } = json.workspaces);
76
- } else if (isLerna(root)) {
77
- const json = JSON.parse(
78
- fs.readFileSync(path.resolve(root, 'lerna.json'), 'utf8'),
79
- );
80
- ({ packages } = json);
81
- } else {
82
- ({ packages } = yaml.parse(
83
- fs.readFileSync(path.join(root, WOKRSPACES_FILES.PNPM), 'utf8'),
84
- ));
85
- }
86
-
87
- if (packages) {
88
- return packages
89
- .map(name =>
90
- // The trailing / ensures only dirs are picked up
91
- glob.sync(path.join(root, `${name}/`), {
92
- ignore: ['**/node_modules/**'],
93
- }),
94
- )
95
- .flat()
96
- .filter(filepath => fs.existsSync(path.resolve(filepath, 'package.json')))
97
- .map(filepath => ({
98
- path: filepath,
99
- name: JSON.parse(
100
- fs.readFileSync(path.resolve(filepath, 'package.json'), 'utf8'),
101
- ).name,
102
- }));
103
- }
104
-
105
- return [];
106
- };
package/src/nodeEnv.ts DELETED
@@ -1,28 +0,0 @@
1
- import execa from 'execa';
2
-
3
- export async function canUseNpm() {
4
- try {
5
- await execa('npm', ['--version'], { env: process.env });
6
- return true;
7
- } catch (e) {
8
- return false;
9
- }
10
- }
11
-
12
- export async function canUseYarn() {
13
- try {
14
- await execa('yarn', ['--version'], { env: process.env });
15
- return true;
16
- } catch (e) {
17
- return false;
18
- }
19
- }
20
-
21
- export async function canUsePnpm() {
22
- try {
23
- await execa('pnpm', ['--version'], { env: process.env });
24
- return true;
25
- } catch (e) {
26
- return false;
27
- }
28
- }
package/src/path.ts DELETED
@@ -1,9 +0,0 @@
1
- import path from 'path';
2
- import upath from 'upath';
3
-
4
- export const isRelativePath = (test: string): boolean =>
5
- /^\.\.?($|[\\/])/.test(test);
6
-
7
- export const normalizeOutputPath = (s: string) => s.replace(/\\/g, '\\\\');
8
- export const normalizeToPosixPath = (p: string) =>
9
- upath.normalizeSafe(path.normalize(p));
package/src/pkgUp.ts DELETED
@@ -1,3 +0,0 @@
1
- import pkgUp from 'pkg-up';
2
-
3
- export { pkgUp };
@@ -1,88 +0,0 @@
1
- import os from 'os';
2
- import chalk from 'chalk';
3
- import { isDev } from './is';
4
-
5
- // TODO: type
6
- interface EntryPoint {
7
- entryName: string;
8
- }
9
-
10
- interface ServerRoute {
11
- entryName: string;
12
- isSSR: boolean;
13
- urlPath: string;
14
- }
15
-
16
- // TODO: remove hard code 'main'
17
- export const isSingleEntry = (entrypoints: EntryPoint[]) =>
18
- entrypoints.length === 1 && entrypoints[0].entryName === 'main';
19
-
20
- const normalizeUrl = (url: string) => url.replace(/([^:]\/)\/+/g, '$1');
21
-
22
- const getAddressUrls = (protocol = 'http', port: number) => {
23
- const interfaces = os.networkInterfaces();
24
- const ipv4Interfaces: os.NetworkInterfaceInfo[] = [];
25
- Object.keys(interfaces).forEach(key => {
26
- interfaces[key]!.forEach(detail => {
27
- if (detail.family === 'IPv4') {
28
- ipv4Interfaces.push(detail);
29
- }
30
- });
31
- });
32
-
33
- return ipv4Interfaces.reduce(
34
- (memo: { type: string; url: string }[], detail) => {
35
- let type = 'Network: ';
36
- let url = `${protocol}://${detail.address}:${port}`;
37
- if (detail.address.includes(`127.0.0.1`)) {
38
- type = 'Local: ';
39
- url = `${protocol}://localhost:${port}`;
40
- }
41
-
42
- memo.push({ type, url });
43
- return memo;
44
- },
45
- [],
46
- );
47
- };
48
-
49
- export const prettyInstructions = (appContext: any, config: any) => {
50
- const { entrypoints, serverRoutes, port } = appContext as {
51
- entrypoints: EntryPoint[];
52
- serverRoutes: ServerRoute[];
53
- port: number;
54
- };
55
-
56
- const urls = getAddressUrls(
57
- config.dev.https && isDev() ? 'https' : 'http',
58
- port,
59
- );
60
-
61
- const routes = serverRoutes.filter(route => route.entryName);
62
-
63
- let message = 'App running at:\n\n';
64
-
65
- if (isSingleEntry(entrypoints)) {
66
- message += urls
67
- .map(
68
- ({ type, url }) =>
69
- ` ${chalk.bold(`> ${type.padEnd(10)}`)}${chalk.cyanBright(
70
- normalizeUrl(`${url}/${routes[0].urlPath}`),
71
- )}\n`,
72
- )
73
- .join('');
74
- } else {
75
- const maxNameLength = Math.max(...routes.map(r => r.entryName.length));
76
-
77
- urls.forEach(({ type, url }) => {
78
- message += ` ${chalk.bold(`> ${type}`)}\n`;
79
- routes.forEach(({ entryName, urlPath }) => {
80
- message += ` ${chalk.yellowBright(
81
- entryName.padEnd(maxNameLength + 8),
82
- )}${chalk.cyanBright(normalizeUrl(`${url}/${urlPath}`))}\n`;
83
- });
84
- });
85
- }
86
-
87
- return message;
88
- };