@shoper/cli 0.5.2 → 0.6.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.
- package/README.md +4 -2
- package/build/cli/auth/cli_auth_errors_factory.js +1 -1
- package/build/cli/auth/cli_auth_initializer.js +6 -1
- package/build/cli/auth/service/cli_auth_service.js +11 -2
- package/build/cli/auth/tokens/cli_auth_tokens_errors_factory.js +1 -1
- package/build/cli/auth/tokens/cli_auth_tokens_initalizer.js +6 -1
- package/build/cli/auth/tokens/service/cli_auth_tokens_service.js +8 -1
- package/build/cli/class/base_command.js +15 -1
- package/build/cli/class/cli_help.js +24 -0
- package/build/cli/class/errors/file_system_errors_factory.js +3 -3
- package/build/cli/class/errors/http/http_errors_factory.js +1 -1
- package/build/cli/commands/auth/cli_auth_add_token_command.js +5 -0
- package/build/cli/commands/auth/cli_auth_list_tokens_command.js +20 -11
- package/build/cli/commands/auth/cli_auth_logout_command.js +5 -0
- package/build/cli/commands/auth/cli_auth_remove_token_command.js +5 -0
- package/build/cli/commands/auth/cli_auth_switch_token_command.js +8 -0
- package/build/cli/commands/cli_update_command.js +5 -0
- package/build/cli/core/cli_setup.js +5 -13
- package/build/cli/features/execution_context/execution_context_service.js +2 -0
- package/build/cli/features/version/service/cli_version_service.js +2 -2
- package/build/cli/hooks/authorization/ensure_authorization_hook.js +9 -2
- package/build/cli/hooks/ensure_cli_initialized_hook.js +1 -6
- package/build/cli/hooks/ensure_logs_flushed_hook.js +7 -0
- package/build/cli/{features → utilities/features}/http_requester/http_client.js +2 -2
- package/build/cli/{features → utilities/features}/http_requester/http_requester_initializer.js +1 -1
- package/build/cli/utilities/features/logger/api/logger_api.js +28 -0
- package/build/cli/utilities/features/logger/logger_constants.js +7 -0
- package/build/cli/utilities/features/logger/logger_initializer.js +44 -0
- package/build/cli/utilities/features/logger/logs/app_error.js +14 -0
- package/build/cli/utilities/features/logger/logs/app_log.js +40 -0
- package/build/cli/{class/errors/app/app_error_constants.js → utilities/features/logger/logs/app_logs_constants.js} +1 -1
- package/build/cli/utilities/features/logger/service/logger_service.js +108 -0
- package/build/cli/utilities/features/logger/transports/log_object_map_transport.js +15 -0
- package/build/index.js +14 -2
- package/build/theme/class/archive/theme_archive.js +2 -1
- package/build/theme/class/archive/theme_archive_errors_factory.js +2 -2
- package/build/theme/class/checksums/theme_checksums.js +25 -5
- package/build/theme/class/checksums/theme_checksums_error_factory.js +3 -3
- package/build/theme/class/fetch_resources/fetch_resources.js +34 -4
- package/build/theme/class/fetch_resources/fetch_resources_errors_factory.js +1 -1
- package/build/theme/commands/delete/theme_delete_command.js +3 -2
- package/build/theme/commands/info/theme_info_command.js +3 -0
- package/build/theme/commands/init/theme_init_command.js +10 -1
- package/build/theme/commands/list/theme_list_command.js +22 -8
- package/build/theme/commands/publish/theme_publish_command.js +5 -1
- package/build/theme/commands/pull/theme_pull_command.js +17 -5
- package/build/theme/commands/push/theme_push_command.js +7 -1
- package/build/theme/commands/theme_verify_command.js +6 -1
- package/build/theme/commands/ui/theme_error.js +1 -1
- package/build/theme/features/theme/actions/service/theme_actions_service.js +42 -2
- package/build/theme/features/theme/actions/theme_actions_initializer.js +3 -1
- package/build/theme/features/theme/delete/service/theme_delete_service.js +12 -1
- package/build/theme/features/theme/delete/theme_delete_initalizer.js +6 -1
- package/build/theme/features/theme/fetch/service/theme_fetch_service.js +35 -6
- package/build/theme/features/theme/fetch/theme_fetch_initializer.js +3 -0
- package/build/theme/features/theme/init/service/theme_init_service.js +32 -6
- package/build/theme/features/theme/init/theme_init_initializer.js +4 -1
- package/build/theme/features/theme/merge/service/theme_merge_service.js +38 -4
- package/build/theme/features/theme/merge/theme_merge_initializer.js +3 -1
- package/build/theme/features/theme/push/service/theme_push_service.js +38 -10
- package/build/theme/features/theme/push/theme_push_errors_factory.js +2 -2
- package/build/theme/features/theme/push/theme_push_initializer.js +3 -1
- package/build/theme/features/theme/push/theme_push_utils.js +1 -1
- package/build/theme/features/theme/utils/files_structure/theme_file_structure_errors_factory.js +2 -3
- package/build/theme/features/theme/utils/hidden_directory/hidden_directory_utils.js +5 -2
- package/build/theme/features/theme/utils/meta_data/theme_meta_data_error_factory.js +1 -1
- package/build/theme/features/theme/utils/resources/theme_resources_with_id_directory_utils.js +5 -2
- package/build/theme/features/theme/verify/theme_verify_initializer.js +4 -3
- package/build/theme/features/theme/verify/verify/theme_verify_service.js +20 -2
- package/build/theme/features/theme/watch/api/theme_watch_api.js +11 -0
- package/build/theme/features/theme/watch/service/theme_watch_service.js +48 -0
- package/build/theme/features/theme/watch/theme_watch_constants.js +2 -0
- package/build/theme/features/theme/watch/theme_watch_initializer.js +20 -0
- package/build/theme/features/themes/list/services/themes_list_service.js +36 -5
- package/build/theme/features/themes/list/themes_list_initializer.js +3 -1
- package/build/theme/hooks/ensure_theme_meta_data_untouched_hook.js +4 -2
- package/build/theme/hooks/theme_checksums/ensure_theme_current_checksums_up_to_date_hook.js +7 -2
- package/build/theme/hooks/themes_actions/ensure_themes_actions_hook.js +2 -3
- package/build/ui/gradient.js +2 -0
- package/build/ui/hooks/stream_hook.js +26 -0
- package/build/utils/download_file/download_file_errors_factory.js +1 -1
- package/build/utils/download_file/download_file_utils.js +19 -1
- package/build/utils/fs/errors/stream_read_error.js +7 -5
- package/build/utils/fs/errors/stream_write_error.js +7 -5
- package/build/utils/get_api.js +9 -0
- package/build/utils/use_api.js +5 -0
- package/build/utils/zip/create_zip_utils.js +21 -9
- package/build/utils/zip/errors/create_zip_error.js +7 -5
- package/build/utils/zip/errors/open_zip_error.js +7 -5
- package/build/utils/zip/extract_zip_utils.js +90 -15
- package/oclif.config.js +3 -1
- package/package.json +13 -4
- package/build/cli/class/errors/app/app_error.js +0 -17
|
@@ -6,7 +6,18 @@ import { ThemeFilesStructureUtils } from '../../utils/files_structure/theme_file
|
|
|
6
6
|
import { SHOPER_THEME_METADATA_DIR } from '../../../../constants/directory_contstants.js';
|
|
7
7
|
import { join, platformSeparator } from '../../../../../utils/path_utils.js';
|
|
8
8
|
export class ThemeMergeService {
|
|
9
|
+
#loggerApi;
|
|
10
|
+
constructor({ loggerApi }) {
|
|
11
|
+
this.#loggerApi = loggerApi;
|
|
12
|
+
}
|
|
9
13
|
async applyChanges(fromTheme, toTheme, changes) {
|
|
14
|
+
this.#loggerApi.info('Applying changes between themes.', {
|
|
15
|
+
details: {
|
|
16
|
+
from: fromTheme,
|
|
17
|
+
to: toTheme,
|
|
18
|
+
changesCount: changes.length
|
|
19
|
+
}
|
|
20
|
+
});
|
|
10
21
|
FSTree.applyPatch(fromTheme, toTheme, changes, {
|
|
11
22
|
create(inputPath, outputPath) {
|
|
12
23
|
copyFileSync(inputPath, outputPath);
|
|
@@ -15,19 +26,36 @@ export class ThemeMergeService {
|
|
|
15
26
|
copyFileSync(inputPath, outputPath);
|
|
16
27
|
}
|
|
17
28
|
});
|
|
18
|
-
|
|
29
|
+
this.#loggerApi.debug('Updating checksums after applying changes.');
|
|
30
|
+
await new ThemeChecksums({
|
|
31
|
+
themeDir: toTheme,
|
|
32
|
+
loggerApi: this.#loggerApi
|
|
33
|
+
}).updateAllChecksums();
|
|
19
34
|
}
|
|
20
35
|
async getChangesBetweenThemes(theme1, theme2) {
|
|
36
|
+
this.#loggerApi.info('Calculating changes between themes.', {
|
|
37
|
+
details: {
|
|
38
|
+
theme1,
|
|
39
|
+
theme2
|
|
40
|
+
}
|
|
41
|
+
});
|
|
21
42
|
const theme1Tree = new FSTree({
|
|
22
43
|
entries: walkSync.entries(theme1)
|
|
23
44
|
});
|
|
24
45
|
const theme2Tree = new FSTree({
|
|
25
46
|
entries: walkSync.entries(theme2)
|
|
26
47
|
});
|
|
27
|
-
const theme1Checksums = new ThemeChecksums(
|
|
28
|
-
|
|
48
|
+
const theme1Checksums = new ThemeChecksums({
|
|
49
|
+
themeDir: theme1,
|
|
50
|
+
loggerApi: this.#loggerApi
|
|
51
|
+
});
|
|
52
|
+
const theme2Checksums = new ThemeChecksums({
|
|
53
|
+
themeDir: theme2,
|
|
54
|
+
loggerApi: this.#loggerApi
|
|
55
|
+
});
|
|
56
|
+
this.#loggerApi.debug('Getting user-defined root directories to exclude from changes.');
|
|
29
57
|
const userDirectories = await this._getUserRootDirectories(theme2);
|
|
30
|
-
|
|
58
|
+
const patch = theme2Tree
|
|
31
59
|
.calculatePatch(theme1Tree, (entryA, entryB) => {
|
|
32
60
|
if (entryA.isDirectory() && entryB.isDirectory())
|
|
33
61
|
return true;
|
|
@@ -37,6 +65,12 @@ export class ThemeMergeService {
|
|
|
37
65
|
.filter(([_, name]) => {
|
|
38
66
|
return !name.startsWith(SHOPER_THEME_METADATA_DIR) && !userDirectories.some((userDir) => name.startsWith(userDir));
|
|
39
67
|
});
|
|
68
|
+
this.#loggerApi.debug('Calculated patch between themes.', {
|
|
69
|
+
details: {
|
|
70
|
+
patchSize: patch.length
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return patch;
|
|
40
74
|
}
|
|
41
75
|
async _getUserRootDirectories(themeDir) {
|
|
42
76
|
const filesStructure = await ThemeFilesStructureUtils.getThemeRootDirectories(themeDir);
|
|
@@ -2,10 +2,12 @@ import { FEATURE_CORES_TYPES, SyncFeatureInitializer } from '@dreamcommerce/star
|
|
|
2
2
|
import { ThemeMergeApi } from './api/theme_merge_api.js';
|
|
3
3
|
import { ThemeMergeService } from './service/theme_merge_service.js';
|
|
4
4
|
import { THEME_MERGE_FEATURE_NAME } from './theme_merge_constants.js';
|
|
5
|
+
import { LOGGER_API_NAME } from '../../../../cli/utilities/features/logger/logger_constants.js';
|
|
5
6
|
export class ThemeMergeInitializer extends SyncFeatureInitializer {
|
|
6
7
|
static featureName = THEME_MERGE_FEATURE_NAME;
|
|
7
8
|
init() {
|
|
8
|
-
const
|
|
9
|
+
const loggerApi = this.getApiSync(LOGGER_API_NAME);
|
|
10
|
+
const themeMergeService = new ThemeMergeService({ loggerApi });
|
|
9
11
|
return {
|
|
10
12
|
cores: [
|
|
11
13
|
{
|
|
@@ -7,7 +7,7 @@ import { THEME_MODULE_SETTINGS_FILE_NAME } from '../theme_push_constants.js';
|
|
|
7
7
|
import { ThemePushErrorsFactory } from '../theme_push_errors_factory.js';
|
|
8
8
|
import { ThemeImagesUtils } from '../../utils/theme_images_utils.js';
|
|
9
9
|
import { ThemePublishUtils } from '../../skinstore/theme_publish_utils.js';
|
|
10
|
-
import {
|
|
10
|
+
import { writeJSONFile } from '../../../../../utils/fs/fs_utils.js';
|
|
11
11
|
import { MODULES_DIRECTORY_NAME } from '../../theme_constants.js';
|
|
12
12
|
import { removeOldResources } from '../../../../class/fetch_resources/fetch_resources_utils.js';
|
|
13
13
|
import { ThemeArchive } from '../../../../class/archive/theme_archive.js';
|
|
@@ -16,63 +16,92 @@ import { ThemeActionsUtils } from '../../actions/theme_actions_utils.js';
|
|
|
16
16
|
import { ArrayUtils } from '@dreamcommerce/utilities';
|
|
17
17
|
export class ThemePushService {
|
|
18
18
|
#themeFetchApi;
|
|
19
|
-
|
|
19
|
+
#loggerApi;
|
|
20
|
+
constructor({ themeFetchApi, loggerApi }) {
|
|
20
21
|
this.#themeFetchApi = themeFetchApi;
|
|
22
|
+
this.#loggerApi = loggerApi;
|
|
21
23
|
}
|
|
22
24
|
async push({ pushAction, credentials, filesStructure, executionContext, themeChecksums, themeFilesUploadApi }) {
|
|
25
|
+
this.#loggerApi.info('Pushing theme changes.');
|
|
23
26
|
const { path: tmpDir } = await tmp.dir({ unsafeCleanup: true });
|
|
27
|
+
this.#loggerApi.debug('Temporary directory created.', { details: { path: tmpDir } });
|
|
24
28
|
const themeRootDir = executionContext.themeRootDir;
|
|
25
29
|
if (await themeChecksums.hasThemeFileBeenCreated(ThemePublishUtils.getSkinStoreSettingsFilePath(themeRootDir)))
|
|
26
30
|
throw ThemePushErrorsFactory.createErrorWhilePushingUnpublishedThemeWithSkinstoreData(credentials.shopUrl);
|
|
27
31
|
try {
|
|
28
|
-
|
|
32
|
+
this.#loggerApi.debug('Getting file records from action data.');
|
|
29
33
|
const filesRecords = await ThemeActionsUtils.getFilesRecordsFromActionData({
|
|
30
34
|
themeRootDir,
|
|
31
35
|
themeAction: pushAction,
|
|
32
36
|
filesStructure
|
|
33
37
|
});
|
|
38
|
+
this.#loggerApi.debug('Filtering for created or modified files to upload.');
|
|
34
39
|
const filesToUpload = await ArrayUtils.asyncFilter(filesRecords, async ({ path }) => (await themeChecksums.hasThemeFileBeenCreated(path)) || (await themeChecksums.hasThemeFileBeenModified(path)));
|
|
40
|
+
this.#loggerApi.debug('Files to upload determined.', { details: { count: filesToUpload.length } });
|
|
35
41
|
if (filesToUpload.length) {
|
|
42
|
+
this.#loggerApi.info(`Uploading ${filesToUpload.length} theme files.`);
|
|
36
43
|
const { localFileNameToUploaded } = await this._uploadThemeFiles({
|
|
37
44
|
filesToUpload,
|
|
38
45
|
credentials,
|
|
39
46
|
themeRootDir,
|
|
40
47
|
themeFilesUploadApi
|
|
41
48
|
});
|
|
49
|
+
this.#loggerApi.info('Theme files uploaded.');
|
|
42
50
|
await this._createFilesList(themeRootDir, filesRecords, localFileNameToUploaded);
|
|
43
51
|
}
|
|
44
52
|
else {
|
|
53
|
+
this.#loggerApi.info('No new or modified files to upload. Creating files list for archive.');
|
|
45
54
|
await this._createFilesList(themeRootDir, filesRecords);
|
|
46
55
|
}
|
|
47
56
|
const themeArchivePath = join(tmpDir, `${uuid()}.zip`);
|
|
57
|
+
this.#loggerApi.info('Creating theme archive.');
|
|
48
58
|
await new ThemeArchive(themeRootDir).createFullArchive({
|
|
49
59
|
dist: themeArchivePath,
|
|
50
60
|
actionValue: THEME_WILDCARD_ACTION_NAME,
|
|
51
|
-
actionType: THEME_ACTIONS_TYPES.push
|
|
61
|
+
actionType: THEME_ACTIONS_TYPES.push,
|
|
62
|
+
logger: this.#loggerApi
|
|
52
63
|
});
|
|
64
|
+
this.#loggerApi.info('Theme archive created.');
|
|
53
65
|
const { resources, modules } = await themeFilesUploadApi.uploadArchive({
|
|
54
66
|
action: pushAction,
|
|
55
67
|
themeArchivePath,
|
|
56
68
|
credentials
|
|
57
69
|
});
|
|
58
|
-
|
|
70
|
+
this.#loggerApi.info('Theme archive uploaded.');
|
|
71
|
+
if (modules) {
|
|
72
|
+
this.#loggerApi.debug('Updating data for new modules.');
|
|
59
73
|
await this._updateDataForNewCreatedModules({ modules, themeRootDir });
|
|
74
|
+
}
|
|
60
75
|
if (resources) {
|
|
76
|
+
this.#loggerApi.debug('Removing old resources.');
|
|
61
77
|
await removeOldResources(themeRootDir, resources);
|
|
78
|
+
this.#loggerApi.info('Fetching new resources.');
|
|
62
79
|
await this.#themeFetchApi.fetchResources(credentials.shopUrl, themeRootDir, resources);
|
|
80
|
+
this.#loggerApi.info('New resources fetched.');
|
|
63
81
|
}
|
|
82
|
+
this.#loggerApi.debug('Updating theme checksums.');
|
|
64
83
|
await themeChecksums.updateAllChecksums();
|
|
84
|
+
this.#loggerApi.info('Push completed successfully.');
|
|
65
85
|
}
|
|
66
86
|
finally {
|
|
87
|
+
this.#loggerApi.debug('Cleaning up files list.');
|
|
67
88
|
await ThemeFilesStructureUtils.removeAFilesListFile(themeRootDir);
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
91
|
async _uploadThemeFiles({ filesToUpload, themeRootDir, credentials, themeFilesUploadApi }) {
|
|
71
92
|
try {
|
|
93
|
+
this.#loggerApi.debug('Uploading individual files.');
|
|
72
94
|
const { uploadedImageData, rejectedImageData } = await themeFilesUploadApi.uploadFiles(filesToUpload);
|
|
73
|
-
|
|
95
|
+
this.#loggerApi.debug('Individual files upload finished.', {
|
|
96
|
+
details: {
|
|
97
|
+
uploadedCount: uploadedImageData.length,
|
|
98
|
+
rejectedCount: rejectedImageData.length
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
if (uploadedImageData.length) {
|
|
102
|
+
this.#loggerApi.debug('Removing original files for uploaded images.');
|
|
74
103
|
await ThemeImagesUtils.removeUploadedOriginalFiles(themeRootDir, uploadedImageData);
|
|
75
|
-
|
|
104
|
+
}
|
|
76
105
|
return {
|
|
77
106
|
localFileNameToUploaded: uploadedImageData.reduce((acc, { originalFilename, uploadedFilename }) => {
|
|
78
107
|
return {
|
|
@@ -86,15 +115,14 @@ export class ThemePushService {
|
|
|
86
115
|
throw ThemePushErrorsFactory.createErrorWhileUploadingThemeFiles(credentials.shopUrl, err);
|
|
87
116
|
}
|
|
88
117
|
}
|
|
89
|
-
async _removeUploadedThemeFiles(uploadedImageData, themeRootDir) {
|
|
90
|
-
await Promise.all(uploadedImageData.map(({ location, originalFilename }) => removeFile(join(themeRootDir, location, originalFilename))));
|
|
91
|
-
}
|
|
92
118
|
async _updateDataForNewCreatedModules({ modules, themeRootDir }) {
|
|
93
119
|
for (const [moduleDirectoryName, metaData] of Object.entries(modules)) {
|
|
120
|
+
this.#loggerApi.debug('Updating settings for new module.', { details: { module: moduleDirectoryName } });
|
|
94
121
|
await writeJSONFile(join(themeRootDir, MODULES_DIRECTORY_NAME, moduleDirectoryName, THEME_MODULE_SETTINGS_FILE_NAME), JSON.parse(metaData[THEME_MODULE_SETTINGS_FILE_NAME]));
|
|
95
122
|
}
|
|
96
123
|
}
|
|
97
124
|
async _createFilesList(themeRootDir, filesRecords, localFileNameToUploaded = {}) {
|
|
125
|
+
this.#loggerApi.debug('Creating filesList.json for the archive.');
|
|
98
126
|
await ThemeFilesStructureUtils.createAFilesListFile(themeRootDir, ThemeFilesStructureUtils.mapFilesRecordsToFilesList(filesRecords, localFileNameToUploaded));
|
|
99
127
|
}
|
|
100
128
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppError } from '../../../../cli/
|
|
1
|
+
import { AppError } from '../../../../cli/utilities/features/logger/logs/app_error.js';
|
|
2
2
|
import { THEME_ARCHIVE_UPLOAD_ERROR, THEME_FILES_UPLOAD_ERROR } from './theme_push_constants.js';
|
|
3
3
|
export class ThemePushErrorsFactory {
|
|
4
4
|
static createErrorWhileUploadingThemeFiles(shopUrl, messages) {
|
|
@@ -16,7 +16,7 @@ export class ThemePushErrorsFactory {
|
|
|
16
16
|
code: 'theme.push.error_creating_theme_archive',
|
|
17
17
|
message: `Error while creating theme archive for shop "${shopUrl}"`,
|
|
18
18
|
level: 'error',
|
|
19
|
-
|
|
19
|
+
error
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
static createErrorWhilePushingUnpublishedThemeWithSkinstoreData(shopUrl) {
|
|
@@ -3,11 +3,13 @@ import { ThemePushService } from './service/theme_push_service.js';
|
|
|
3
3
|
import { THEME_PUSH_FEATURE_NAME } from './theme_push_constants.js';
|
|
4
4
|
import { ThemePushApi } from './api/theme_push_api.js';
|
|
5
5
|
import { THEME_FETCH_API_NAME } from '../fetch/theme_fetch_constants.js';
|
|
6
|
+
import { LOGGER_API_NAME } from '../../../../cli/utilities/features/logger/logger_constants.js';
|
|
6
7
|
export class ThemePushInitializer extends SyncFeatureInitializer {
|
|
7
8
|
static featureName = THEME_PUSH_FEATURE_NAME;
|
|
8
9
|
init() {
|
|
9
10
|
const service = new ThemePushService({
|
|
10
|
-
themeFetchApi: this.getApiSync(THEME_FETCH_API_NAME)
|
|
11
|
+
themeFetchApi: this.getApiSync(THEME_FETCH_API_NAME),
|
|
12
|
+
loggerApi: this.getApiSync(LOGGER_API_NAME)
|
|
11
13
|
});
|
|
12
14
|
return {
|
|
13
15
|
cores: [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import globs from 'fast-glob';
|
|
2
|
-
import { AppError } from '../../../../cli/
|
|
2
|
+
import { AppError } from '../../../../cli/utilities/features/logger/logs/app_error.js';
|
|
3
3
|
import { toUnixPath } from '../../../../utils/path_utils.js';
|
|
4
4
|
import { ThemeFilesStructureUtils } from '../utils/files_structure/theme_files_structure_utils.js';
|
|
5
5
|
export class ThemePushUtils {
|
package/build/theme/features/theme/utils/files_structure/theme_file_structure_errors_factory.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { AppError } from '../../../../../cli/
|
|
1
|
+
import { AppError } from '../../../../../cli/utilities/features/logger/logs/app_error.js';
|
|
2
2
|
export class ThemeFileStructureErrorsFactory {
|
|
3
3
|
static createNoFilesStructureError() {
|
|
4
4
|
return new AppError({
|
|
5
5
|
code: 'theme.file_structure.no_files_structure',
|
|
6
|
-
message: 'No files structure found for the theme.'
|
|
7
|
-
level: 'error'
|
|
6
|
+
message: 'No files structure found for the theme.'
|
|
8
7
|
});
|
|
9
8
|
}
|
|
10
9
|
}
|
|
@@ -5,9 +5,12 @@ import { SHOPER_THEME_METADATA_DIR } from '../../../../constants/directory_conts
|
|
|
5
5
|
import { computeFileChecksum } from '../../../../../utils/checksums/checksums_utils.js';
|
|
6
6
|
import { ThemeChecksums } from '../../../../class/checksums/theme_checksums.js';
|
|
7
7
|
export class HiddenDirectoryUtils {
|
|
8
|
-
static async ensureFilesInsideThemeMetaDataDirectoryUntouched(themeDirectory) {
|
|
8
|
+
static async ensureFilesInsideThemeMetaDataDirectoryUntouched(themeDirectory, logger) {
|
|
9
9
|
const themeMetadataPath = this.getThemeHiddenDirectoryPath(themeDirectory);
|
|
10
|
-
const themeChecksums = new ThemeChecksums(
|
|
10
|
+
const themeChecksums = new ThemeChecksums({
|
|
11
|
+
themeDir: themeDirectory,
|
|
12
|
+
loggerApi: logger
|
|
13
|
+
});
|
|
11
14
|
if (!(await themeChecksums.verify()))
|
|
12
15
|
throw new Error('Theme checksum file is not valid');
|
|
13
16
|
const filesNames = (await getAllFilesNamesInside(themeMetadataPath)).filter((fileName) => fileName !== THEME_CURRENT_CHECKSUMS_FILE_NAME &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppError } from '../../../../../cli/
|
|
1
|
+
import { AppError } from '../../../../../cli/utilities/features/logger/logs/app_error.js';
|
|
2
2
|
import { THEME_WORK_URL_MISMATCH_ERROR } from './theme_meta_data_constants.js';
|
|
3
3
|
export class ThemeMetaDataErrorFactory {
|
|
4
4
|
static createThemeWorkUrlMismatchError(executionUrl, themeWorkUrl) {
|
package/build/theme/features/theme/utils/resources/theme_resources_with_id_directory_utils.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileExists, readJSONFile, renameFile } from '../../../../../utils/fs/fs
|
|
|
5
5
|
import { join } from '../../../../../utils/path_utils.js';
|
|
6
6
|
import { ThemeChecksums } from '../../../../class/checksums/theme_checksums.js';
|
|
7
7
|
export class ThemeResourcesWithIdDirectoryUtils {
|
|
8
|
-
static async updateDirectoryNamesOfResourcesWithIdAccordingToLocalThemeNames(localResourcesDir, remoteResourcesDir, remoteThemeDir) {
|
|
8
|
+
static async updateDirectoryNamesOfResourcesWithIdAccordingToLocalThemeNames(localResourcesDir, remoteResourcesDir, remoteThemeDir, loggerApi) {
|
|
9
9
|
const localThemeTree = new FSTree({
|
|
10
10
|
entries: walkSync.entries(localResourcesDir, {
|
|
11
11
|
globs: ['*/'],
|
|
@@ -37,7 +37,10 @@ export class ThemeResourcesWithIdDirectoryUtils {
|
|
|
37
37
|
/**
|
|
38
38
|
* If these become performance bottlenecks, we can consider computing checksums only for the changed directories.
|
|
39
39
|
*/
|
|
40
|
-
await new ThemeChecksums(
|
|
40
|
+
await new ThemeChecksums({
|
|
41
|
+
themeDir: remoteThemeDir,
|
|
42
|
+
loggerApi
|
|
43
|
+
}).updateAllChecksums();
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
static async _getResourceIdToFilePathMap(entries) {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { FEATURE_CORES_TYPES,
|
|
1
|
+
import { FEATURE_CORES_TYPES, SyncFeatureInitializer } from '@dreamcommerce/star_core';
|
|
2
2
|
import { ThemeVerifyApi } from './api/theme_verify_api.js';
|
|
3
3
|
import { THEME_VERIFY_FEATURE_NAME } from './theme_verify_constants.js';
|
|
4
4
|
import { ThemeVerifyService } from './verify/theme_verify_service.js';
|
|
5
|
+
import { LOGGER_API_NAME } from '../../../../cli/utilities/features/logger/logger_constants.js';
|
|
5
6
|
export class ThemeVerifyInitializer extends SyncFeatureInitializer {
|
|
6
7
|
static featureName = THEME_VERIFY_FEATURE_NAME;
|
|
7
8
|
init() {
|
|
8
|
-
const
|
|
9
|
-
const service = new ThemeVerifyService();
|
|
9
|
+
const loggerApi = this.getApiSync(LOGGER_API_NAME);
|
|
10
|
+
const service = new ThemeVerifyService({ loggerApi });
|
|
10
11
|
return {
|
|
11
12
|
cores: [
|
|
12
13
|
{
|
|
@@ -10,21 +10,33 @@ import { ThemeFilesStructureUtils } from '../../utils/files_structure/theme_file
|
|
|
10
10
|
import { ThemeActionsUtils } from '../../actions/theme_actions_utils.js';
|
|
11
11
|
import { ArrayUtils } from '@dreamcommerce/utilities';
|
|
12
12
|
export class ThemeVerifyService {
|
|
13
|
+
#loggerApi;
|
|
14
|
+
constructor({ loggerApi }) {
|
|
15
|
+
this.#loggerApi = loggerApi;
|
|
16
|
+
}
|
|
13
17
|
async verifyTheme({ verifyAction, executionContext, credentials, themeChecksums, filesStructure, themeFilesUploadApi }) {
|
|
18
|
+
this.#loggerApi.info('Verifying theme.');
|
|
14
19
|
const { path: tmpDir } = await tmp.dir({ unsafeCleanup: true });
|
|
20
|
+
this.#loggerApi.debug('Temporary directory created.', { details: { path: tmpDir } });
|
|
15
21
|
const themeRootDir = executionContext.themeRootDir;
|
|
16
22
|
if (await themeChecksums.hasThemeFileBeenCreated(ThemePublishUtils.getSkinStoreSettingsFilePath(themeRootDir)))
|
|
17
23
|
throw ThemePushErrorsFactory.createErrorWhilePushingUnpublishedThemeWithSkinstoreData(credentials.shopUrl);
|
|
18
24
|
try {
|
|
19
|
-
|
|
25
|
+
this.#loggerApi.debug('Getting file records from action data.');
|
|
20
26
|
const filesRecords = await ThemeActionsUtils.getFilesRecordsFromActionData({
|
|
21
27
|
themeRootDir,
|
|
22
28
|
themeAction: verifyAction,
|
|
23
29
|
filesStructure
|
|
24
30
|
});
|
|
31
|
+
this.#loggerApi.debug('Filtering for modified or not created files to verify.');
|
|
25
32
|
const filesToVerify = await ArrayUtils.asyncFilter(filesRecords, async ({ path }) => (await themeChecksums.hasThemeFileBeenModified(path)) || !(await themeChecksums.hasThemeFileBeenCreated(path)));
|
|
33
|
+
this.#loggerApi.debug('Files to verify determined.', { details: { count: filesToVerify.length } });
|
|
26
34
|
if (filesToVerify.length) {
|
|
35
|
+
this.#loggerApi.info(`Verifying ${filesToVerify.length} theme files.`);
|
|
27
36
|
const { rejectedImageData } = await themeFilesUploadApi.uploadFiles(filesToVerify);
|
|
37
|
+
this.#loggerApi.debug('Individual files verification finished.', {
|
|
38
|
+
details: { rejectedCount: rejectedImageData.length }
|
|
39
|
+
});
|
|
28
40
|
if (rejectedImageData.length)
|
|
29
41
|
return {
|
|
30
42
|
isSuccess: false,
|
|
@@ -33,23 +45,29 @@ export class ThemeVerifyService {
|
|
|
33
45
|
}
|
|
34
46
|
await this._createFilesList(themeRootDir, filesToVerify);
|
|
35
47
|
const themeArchivePath = join(tmpDir, `${uuid()}.zip`);
|
|
48
|
+
this.#loggerApi.info('Creating theme archive for verification.');
|
|
36
49
|
await new ThemeArchive(themeRootDir).createFullArchive({
|
|
37
50
|
dist: themeArchivePath,
|
|
38
51
|
actionValue: THEME_WILDCARD_ACTION_NAME,
|
|
39
|
-
actionType: THEME_ACTIONS_TYPES.push
|
|
52
|
+
actionType: THEME_ACTIONS_TYPES.push,
|
|
53
|
+
logger: this.#loggerApi
|
|
40
54
|
});
|
|
55
|
+
this.#loggerApi.info('Theme archive created.');
|
|
41
56
|
const { isSuccess, messages } = await themeFilesUploadApi.uploadArchive({
|
|
42
57
|
action: verifyAction,
|
|
43
58
|
themeArchivePath,
|
|
44
59
|
credentials
|
|
45
60
|
});
|
|
61
|
+
this.#loggerApi.info('Theme archive verification finished.');
|
|
46
62
|
return { isSuccess, messages };
|
|
47
63
|
}
|
|
48
64
|
finally {
|
|
65
|
+
this.#loggerApi.debug('Cleaning up files list.');
|
|
49
66
|
await ThemeFilesStructureUtils.removeAFilesListFile(themeRootDir);
|
|
50
67
|
}
|
|
51
68
|
}
|
|
52
69
|
async _createFilesList(themeRootDir, filesRecords) {
|
|
70
|
+
this.#loggerApi.debug('Creating filesList.json for the archive.');
|
|
53
71
|
await ThemeFilesStructureUtils.createAFilesListFile(themeRootDir, ThemeFilesStructureUtils.mapFilesRecordsToFilesList(filesRecords));
|
|
54
72
|
}
|
|
55
73
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FeatureApi } from '@dreamcommerce/star_core';
|
|
2
|
+
export class ThemeWatchApi extends FeatureApi {
|
|
3
|
+
#service;
|
|
4
|
+
constructor(service) {
|
|
5
|
+
super();
|
|
6
|
+
this.#service = service;
|
|
7
|
+
}
|
|
8
|
+
async watchTheme(props) {
|
|
9
|
+
return this.#service.watchTheme(props);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// import chokidar from 'chokidar';
|
|
2
|
+
import { relative } from '../../../../../utils/path_utils.js';
|
|
3
|
+
export class ThemeWatchService {
|
|
4
|
+
#themePushApi;
|
|
5
|
+
constructor({ themePushApi }) {
|
|
6
|
+
this.#themePushApi = themePushApi;
|
|
7
|
+
}
|
|
8
|
+
async watchTheme({ themeChecksums, executionContext, onChange, credentials, themeFilesUploadApi, filesStructure, action }) {
|
|
9
|
+
const themePath = executionContext.themeRootDir;
|
|
10
|
+
console.log(`[watch] Watching for changes in: ${themePath}`);
|
|
11
|
+
// const watcher = chokidar.watch(themePath, {
|
|
12
|
+
// ignored: /(^|[\/\\])\../, // ignore dotfiles
|
|
13
|
+
// persistent: true,
|
|
14
|
+
// ignoreInitial: true // Don't trigger on initial scan
|
|
15
|
+
// });
|
|
16
|
+
const handleFileChange = async (filePath) => {
|
|
17
|
+
const relativePath = relative(themePath, filePath);
|
|
18
|
+
console.log(`[watch] Detected change in: ${relativePath}. Pushing theme...`);
|
|
19
|
+
try {
|
|
20
|
+
// Assuming pushTheme handles the logic of pushing the entire theme
|
|
21
|
+
return;
|
|
22
|
+
//@ts-ignore
|
|
23
|
+
await this.#themePushApi.partialPush({
|
|
24
|
+
credentials,
|
|
25
|
+
filesStructure,
|
|
26
|
+
action,
|
|
27
|
+
executionContext,
|
|
28
|
+
themeChecksums,
|
|
29
|
+
themeFilesUploadApi
|
|
30
|
+
});
|
|
31
|
+
console.log(`[watch] Theme pushed successfully.`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error(`[watch] Error pushing theme:`, error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
//
|
|
38
|
+
// watcher
|
|
39
|
+
// .on('add', handleFileChange)
|
|
40
|
+
// .on('change', handleFileChange)
|
|
41
|
+
// .on('unlink', handleFileChange)
|
|
42
|
+
// //@ts-ignore
|
|
43
|
+
// .on('error', (error) => console.error(`[watch] Watcher error: ${error}`))
|
|
44
|
+
// .on('ready', () => console.log('[watch] Initial scan complete. Ready for changes.'));
|
|
45
|
+
// Keep the process alive while watching
|
|
46
|
+
return new Promise(() => { });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FEATURE_CORES_TYPES, SyncFeatureInitializer } from '@dreamcommerce/star_core';
|
|
2
|
+
import { THEME_WATCH_FEATURE_NAME } from './theme_watch_constants.js';
|
|
3
|
+
import { ThemeWatchService } from './service/theme_watch_service.js';
|
|
4
|
+
import { THEME_PUSH_API_NAME } from '../push/theme_push_constants.js';
|
|
5
|
+
import { ThemeWatchApi } from './api/theme_watch_api.js';
|
|
6
|
+
export class ThemeWatchInitializer extends SyncFeatureInitializer {
|
|
7
|
+
static featureName = THEME_WATCH_FEATURE_NAME;
|
|
8
|
+
init() {
|
|
9
|
+
const themePushApi = this.getApiSync(THEME_PUSH_API_NAME);
|
|
10
|
+
const service = new ThemeWatchService({ themePushApi });
|
|
11
|
+
return {
|
|
12
|
+
cores: [
|
|
13
|
+
{
|
|
14
|
+
type: FEATURE_CORES_TYPES.api,
|
|
15
|
+
instance: new ThemeWatchApi(service)
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -4,20 +4,38 @@ import { HttpErrorsFactory } from '../../../../../cli/class/errors/http/http_err
|
|
|
4
4
|
import { DownloadFileErrorsFactory } from '../../../../../utils/download_file/download_file_errors_factory.js';
|
|
5
5
|
export class ThemesListService {
|
|
6
6
|
#httpApi;
|
|
7
|
-
|
|
7
|
+
#loggerApi;
|
|
8
|
+
constructor(httpApi, loggerApi) {
|
|
8
9
|
this.#httpApi = httpApi;
|
|
10
|
+
this.#loggerApi = loggerApi;
|
|
9
11
|
}
|
|
10
12
|
async getThemes({ shopUrl }) {
|
|
13
|
+
this.#loggerApi.info('Fetching themes list.', {
|
|
14
|
+
details: {
|
|
15
|
+
shopUrl
|
|
16
|
+
}
|
|
17
|
+
});
|
|
11
18
|
try {
|
|
12
19
|
const { response: request } = this.#httpApi.getThemes(shopUrl);
|
|
13
20
|
const response = await request;
|
|
14
|
-
if (response?.status !== STATUS_CODES.ok)
|
|
21
|
+
if (response?.status !== STATUS_CODES.ok) {
|
|
22
|
+
this.#loggerApi.debug('Failed to fetch themes list.', {
|
|
23
|
+
details: {
|
|
24
|
+
status: response?.status
|
|
25
|
+
}
|
|
26
|
+
});
|
|
15
27
|
return;
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
}
|
|
29
|
+
const themes = response?.data.map(({ _links, ...rest }) => new ThemeMetaData({
|
|
18
30
|
...rest,
|
|
19
31
|
links: _links
|
|
20
32
|
}));
|
|
33
|
+
this.#loggerApi.info('Successfully fetched themes list.', {
|
|
34
|
+
details: {
|
|
35
|
+
count: themes.length
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return themes;
|
|
21
39
|
}
|
|
22
40
|
catch (err) {
|
|
23
41
|
switch (err.response?.status) {
|
|
@@ -35,9 +53,22 @@ export class ThemesListService {
|
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
55
|
async getTheme({ themeId, shopUrl }) {
|
|
56
|
+
this.#loggerApi.debug('Getting a specific theme from list.', {
|
|
57
|
+
details: {
|
|
58
|
+
themeId,
|
|
59
|
+
shopUrl
|
|
60
|
+
}
|
|
61
|
+
});
|
|
38
62
|
const themes = await this.getThemes({ shopUrl });
|
|
39
63
|
if (!themes)
|
|
40
64
|
return;
|
|
41
|
-
|
|
65
|
+
const theme = themes.find((theme) => String(theme.skinId) === themeId);
|
|
66
|
+
this.#loggerApi.debug(theme ? 'Theme found.' : 'Theme not found.', {
|
|
67
|
+
details: {
|
|
68
|
+
themeId,
|
|
69
|
+
found: !!theme
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return theme;
|
|
42
73
|
}
|
|
43
74
|
}
|
|
@@ -3,11 +3,13 @@ import { ThemesListApi } from './api/themes_list_api.js';
|
|
|
3
3
|
import { FEATURE_CORES_TYPES, HTTP_REQUESTER_API_NAME, SyncFeatureInitializer } from '@dreamcommerce/star_core';
|
|
4
4
|
import { THEMES_LIST_FEATURE_NAME } from './themes_list_constants.js';
|
|
5
5
|
import { ThemesListHttpApi } from './http/themes_list_http_api.js';
|
|
6
|
+
import { LOGGER_API_NAME } from '../../../../cli/utilities/features/logger/logger_constants.js';
|
|
6
7
|
export class ThemesListInitializer extends SyncFeatureInitializer {
|
|
7
8
|
static featureName = THEMES_LIST_FEATURE_NAME;
|
|
8
9
|
init() {
|
|
9
10
|
const httpApi = this.getApiSync(HTTP_REQUESTER_API_NAME);
|
|
10
|
-
const
|
|
11
|
+
const loggerApi = this.getApiSync(LOGGER_API_NAME);
|
|
12
|
+
const service = new ThemesListService(new ThemesListHttpApi(httpApi), loggerApi);
|
|
11
13
|
return {
|
|
12
14
|
cores: [
|
|
13
15
|
{
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { useApi } from '../../cli/hooks/ensure_cli_initialized_hook.js';
|
|
2
1
|
import { EXECUTION_CONTEXT_API_NAME, EXECUTION_CONTEXTS } from '../../cli/features/execution_context/execution_context_constants.js';
|
|
3
2
|
import { HiddenDirectoryUtils } from '../features/theme/utils/hidden_directory/hidden_directory_utils.js';
|
|
3
|
+
import { LOGGER_API_NAME } from '../../cli/utilities/features/logger/logger_constants.js';
|
|
4
|
+
import { useApi } from '../../utils/use_api.js';
|
|
4
5
|
const ensureThemeMetaDataUntouched = async () => {
|
|
5
6
|
const executionContextApi = useApi(EXECUTION_CONTEXT_API_NAME);
|
|
6
7
|
const executionContext = await executionContextApi.getExecutionContext();
|
|
7
8
|
if (executionContext.type !== EXECUTION_CONTEXTS.theme)
|
|
8
9
|
return;
|
|
10
|
+
const loggerApi = useApi(LOGGER_API_NAME);
|
|
9
11
|
try {
|
|
10
|
-
await HiddenDirectoryUtils.ensureFilesInsideThemeMetaDataDirectoryUntouched(executionContext.themeRootDir);
|
|
12
|
+
await HiddenDirectoryUtils.ensureFilesInsideThemeMetaDataDirectoryUntouched(executionContext.themeRootDir, loggerApi);
|
|
11
13
|
}
|
|
12
14
|
catch (err) {
|
|
13
15
|
console.error(err);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { EXECUTION_CONTEXT_API_NAME, EXECUTION_CONTEXTS } from '../../../cli/features/execution_context/execution_context_constants.js';
|
|
2
|
-
import { useApi } from '../../../cli/hooks/ensure_cli_initialized_hook.js';
|
|
3
2
|
import { THEME_COMMANDS_THAT_REQUIRES_UP_TO_DATE_CHECKSUMS } from './ensure_theme_current_checksums_up_to_date_constants.js';
|
|
4
3
|
import { ThemeChecksums } from '../../class/checksums/theme_checksums.js';
|
|
4
|
+
import { LOGGER_API_NAME } from '../../../cli/utilities/features/logger/logger_constants.js';
|
|
5
|
+
import { useApi } from '../../../utils/use_api.js';
|
|
5
6
|
export const ensureThemeChecksumsUpToDate = async ({ Command }) => {
|
|
6
7
|
if (!THEME_COMMANDS_THAT_REQUIRES_UP_TO_DATE_CHECKSUMS.includes(Command.id))
|
|
7
8
|
return;
|
|
@@ -9,10 +10,14 @@ export const ensureThemeChecksumsUpToDate = async ({ Command }) => {
|
|
|
9
10
|
const executionContext = await executionContextApi.getExecutionContext();
|
|
10
11
|
if (executionContext.type !== EXECUTION_CONTEXTS.theme)
|
|
11
12
|
return;
|
|
13
|
+
const loggerApi = useApi(LOGGER_API_NAME);
|
|
12
14
|
/**
|
|
13
15
|
* Naive solution, recalculate checksums every time a command based on checksums calculation is executed;
|
|
14
16
|
* If performance becomes an issue, we can implement a more sophisticated solution, eg. recalculate checksums only when files in the theme directory have changed.
|
|
15
17
|
*/
|
|
16
|
-
await new ThemeChecksums(
|
|
18
|
+
await new ThemeChecksums({
|
|
19
|
+
themeDir: executionContext.themeRootDir,
|
|
20
|
+
loggerApi
|
|
21
|
+
}).updateCurrentChecksums();
|
|
17
22
|
};
|
|
18
23
|
export default ensureThemeChecksumsUpToDate;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { useApi } from '../../../cli/hooks/ensure_cli_initialized_hook.js';
|
|
2
1
|
import { THEME_ACTIONS_API_NAME } from '../../features/theme/actions/theme_actions_constants.js';
|
|
3
2
|
import { CLI_AUTH_API_NAME } from '../../../cli/auth/cli_auth_constants.js';
|
|
4
3
|
import { THEME_COMMANDS_THAT_REQUIRED_ACTIONS_LIST } from './ensure_themes_actions_hook_constants.js';
|
|
5
4
|
import { CLI_AUTH_TOKENS_API_NAME } from '../../../cli/auth/tokens/cli_auth_tokens_constants.js';
|
|
6
5
|
import { promptForToken } from '../../../cli/commands/utils/prompt_for_token_utils.js';
|
|
7
6
|
import { EXECUTION_CONTEXT_API_NAME, EXECUTION_CONTEXTS } from '../../../cli/features/execution_context/execution_context_constants.js';
|
|
7
|
+
import { useApi } from '../../../utils/use_api.js';
|
|
8
8
|
const ensureThemesActionsHook = async ({ Command, argv }) => {
|
|
9
9
|
if (!THEME_COMMANDS_THAT_REQUIRED_ACTIONS_LIST.includes(Command.id))
|
|
10
10
|
return;
|
|
@@ -38,8 +38,7 @@ const ensureThemesActionsHook = async ({ Command, argv }) => {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
|
-
|
|
42
|
-
process.exit(1);
|
|
41
|
+
throw err;
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
44
|
};
|