@shoper/cli 0.9.4-7 → 0.9.6-1
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/build/cli/features/controls/ui/controls_mappers.js +38 -1
- package/build/cli/features/controls/ui/form.js +1 -1
- package/build/theme/class/checksums/theme_checksums.js +3 -8
- package/build/theme/commands/publish/theme_publish_command.js +205 -18
- package/build/theme/features/theme/skinstore/api/theme_skinstore_api.js +4 -4
- package/build/theme/features/theme/skinstore/http/theme_skinstore_http_api.js +26 -0
- package/build/theme/features/theme/skinstore/service/theme_skinstore_service.js +64 -12
- package/build/theme/features/theme/skinstore/theme_publish_constants.js +4 -0
- package/build/theme/features/theme/skinstore/theme_skinstore_initialzier.js +6 -1
- package/build/theme/features/theme/utils/files/theme_files_utils.js +11 -5
- package/build/theme/features/theme/utils/hidden_directory/hidden_directory_utils.js +2 -0
- package/build/theme/hooks/themes_actions/ensure_themes_actions_hook_constants.js +1 -0
- package/build/theme/index.js +2 -0
- package/build/utils/path_utils.js +6 -6
- package/package.json +1 -1
|
@@ -56,7 +56,44 @@ export const toInquirerControl = (control) => {
|
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
58
|
export const toInquirerValidate = (validators) => {
|
|
59
|
+
if (!validators || validators.length === 0)
|
|
60
|
+
return undefined;
|
|
59
61
|
return (value) => {
|
|
60
|
-
|
|
62
|
+
for (const v of validators) {
|
|
63
|
+
if (typeof v?.isValid === 'function') {
|
|
64
|
+
if (!v.isValid(value)) {
|
|
65
|
+
return typeof v.getErrorMessage === 'function' ? v.getErrorMessage() : 'Validation failed';
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
switch (v?.type) {
|
|
70
|
+
case 'length': {
|
|
71
|
+
const strValue = String(value ?? '');
|
|
72
|
+
const { min, max } = v.options ?? {};
|
|
73
|
+
if (typeof min === 'number' && strValue.length < min) {
|
|
74
|
+
return `Minimum length is ${min} characters`;
|
|
75
|
+
}
|
|
76
|
+
if (typeof max === 'number' && strValue.length > max) {
|
|
77
|
+
return `Maximum length is ${max} characters`;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'greaterEqThan': {
|
|
82
|
+
const numValue = Number(value);
|
|
83
|
+
const { min } = v.options ?? {};
|
|
84
|
+
if (typeof min === 'number' && (isNaN(numValue) || numValue < min)) {
|
|
85
|
+
return `Value must be greater than or equal to ${min}`;
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case 'required': {
|
|
90
|
+
if (value === null || value === undefined || String(value).trim() === '') {
|
|
91
|
+
return 'This field is required';
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
61
98
|
};
|
|
62
99
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { copyFile, fileExists, readJSONFile, readJSONFileSync, removeFile } from '../../../utils/fs/fs_utils.js';
|
|
2
2
|
import { ThemeChecksumsUtils } from './theme_checksums_utils.js';
|
|
3
|
-
import {
|
|
4
|
-
import { join, mapKeysPathsToWindowPlatform, toCurrentPlatformPath } from '../../../utils/path_utils.js';
|
|
3
|
+
import { join, normalizeObjectPathKeys, toCurrentPlatformPath } from '../../../utils/path_utils.js';
|
|
5
4
|
import { createWriteStream } from 'node:fs';
|
|
6
5
|
import { JSON_FILE_INDENT } from '../../../cli/cli_constants.js';
|
|
7
6
|
import { ThemeChecksumsErrorFactory } from './theme_checksums_error_factory.js';
|
|
@@ -204,15 +203,11 @@ export class ThemeChecksums {
|
|
|
204
203
|
}
|
|
205
204
|
async _getChecksums(path) {
|
|
206
205
|
const checksums = await readJSONFile(path);
|
|
207
|
-
|
|
208
|
-
return checksums;
|
|
209
|
-
return mapKeysPathsToWindowPlatform(checksums);
|
|
206
|
+
return normalizeObjectPathKeys(checksums);
|
|
210
207
|
}
|
|
211
208
|
_getChecksumsSync(path) {
|
|
212
209
|
const checksums = readJSONFileSync(path);
|
|
213
|
-
|
|
214
|
-
return checksums;
|
|
215
|
-
return mapKeysPathsToWindowPlatform(checksums);
|
|
210
|
+
return normalizeObjectPathKeys(checksums);
|
|
216
211
|
}
|
|
217
212
|
async _createThemeChecksumVerifyFile(checksumFilePath, checksumVerifyFullPath) {
|
|
218
213
|
this.#loggerApi?.debug('Creating checksum verification file.', { details: { path: checksumVerifyFullPath } });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Args } from '@oclif/core';
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseThemeCommand } from '../../class/base_theme_command.js';
|
|
3
3
|
import { CLI_AUTH_API_NAME } from '../../../cli/auth/cli_auth_constants.js';
|
|
4
4
|
import { THEME_ACTIONS_API_NAME, THEME_ACTIONS_TYPES } from '../../features/theme/actions/theme_actions_constants.js';
|
|
@@ -7,20 +7,41 @@ import { renderOnce } from '../../../ui/ui_utils.js';
|
|
|
7
7
|
import { MissingCredentialsError } from '../../../cli/commands/auth/ui/missing_credentials_error.js';
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { MissingThemeIdError } from '../ui/missing_theme_id_error.js';
|
|
10
|
-
import { THEME_SKINSTORE_API_NAME } from '../../features/theme/skinstore/theme_publish_constants.js';
|
|
10
|
+
import { THEME_SKINSTORE_API_NAME, RELEASE_TYPE_TEST, RELEASE_TYPE_STABLE, IMAGE_MAX_SIZE, IMAGE_ALLOWED_EXTENSIONS } from '../../features/theme/skinstore/theme_publish_constants.js';
|
|
11
11
|
import { Form } from '../../../cli/features/controls/ui/form.js';
|
|
12
|
+
import { toInquirerControls } from '../../../cli/features/controls/ui/controls_mappers.js';
|
|
12
13
|
import { ThemeError } from '../ui/theme_error.js';
|
|
13
14
|
import { LOGGER_API_NAME } from '../../../cli/utilities/features/logger/logger_constants.js';
|
|
15
|
+
import { Text } from '../../../ui/text.js';
|
|
16
|
+
import { Box } from '../../../ui/box.js';
|
|
17
|
+
import { Success } from '../../../ui/message_box/success.js';
|
|
18
|
+
import { Info } from '../../../ui/message_box/info.js';
|
|
19
|
+
import { Error as ErrorBox } from '../../../ui/message_box/error.js';
|
|
20
|
+
import { UnpermittedCommandError } from '../ui/unpermitted_command_error.js';
|
|
21
|
+
import { promptConfirmation } from '../../../ui/prompts/prompt_confirmation.js';
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
14
24
|
export class ThemePublishCommand extends BaseThemeCommand {
|
|
15
|
-
static summary = '
|
|
16
|
-
static description = '
|
|
25
|
+
static summary = 'Publish theme to SkinStore.';
|
|
26
|
+
static description = 'Publishes a theme to the Shoper SkinStore marketplace.\n\nFor first-time publication, you will be prompted to fill in theme details.\nFor already published themes, this performs an upgrade (new version).';
|
|
17
27
|
static examples = [
|
|
18
28
|
{
|
|
19
|
-
description: '
|
|
29
|
+
description: 'Publish current theme (from theme directory)',
|
|
30
|
+
command: '<%= config.bin %> <%= command.id %>'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
description: 'Publish specific theme by ID',
|
|
20
34
|
command: '<%= config.bin %> <%= command.id %> 123'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
description: 'Publish as test release',
|
|
38
|
+
command: '<%= config.bin %> <%= command.id %> --release test'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
description: 'Publish with screenshots from directory',
|
|
42
|
+
command: '<%= config.bin %> <%= command.id %> --images ./screenshots'
|
|
21
43
|
}
|
|
22
44
|
];
|
|
23
|
-
static hidden = true;
|
|
24
45
|
static args = {
|
|
25
46
|
id: Args.string({
|
|
26
47
|
description: 'Theme id',
|
|
@@ -29,6 +50,18 @@ export class ThemePublishCommand extends BaseThemeCommand {
|
|
|
29
50
|
type: 'string'
|
|
30
51
|
})
|
|
31
52
|
};
|
|
53
|
+
static flags = {
|
|
54
|
+
release: Flags.string({
|
|
55
|
+
char: 'r',
|
|
56
|
+
description: 'Release type',
|
|
57
|
+
options: ['test', 'stable'],
|
|
58
|
+
default: 'stable'
|
|
59
|
+
}),
|
|
60
|
+
images: Flags.string({
|
|
61
|
+
char: 'i',
|
|
62
|
+
description: 'Path to directory with screenshots'
|
|
63
|
+
})
|
|
64
|
+
};
|
|
32
65
|
async run() {
|
|
33
66
|
const themeId = this.args.id;
|
|
34
67
|
const cliAuthApi = this.getApi(CLI_AUTH_API_NAME);
|
|
@@ -43,38 +76,192 @@ export class ThemePublishCommand extends BaseThemeCommand {
|
|
|
43
76
|
try {
|
|
44
77
|
let _themeId = themeId;
|
|
45
78
|
if (executionContext.type !== EXECUTION_CONTEXTS.theme && !_themeId) {
|
|
79
|
+
renderOnce(React.createElement(MissingThemeIdError, null,
|
|
80
|
+
React.createElement(Box, { flexDirection: "column", gap: 1 },
|
|
81
|
+
React.createElement(Text, null, "Usage: shoper theme publish [ID]"),
|
|
82
|
+
React.createElement(Text, null, "Please run this command inside a theme directory or provide a theme ID."))));
|
|
46
83
|
return;
|
|
47
84
|
}
|
|
48
|
-
if (executionContext.type === EXECUTION_CONTEXTS.theme)
|
|
85
|
+
if (executionContext.type === EXECUTION_CONTEXTS.theme)
|
|
49
86
|
_themeId = _themeId ?? executionContext.themeId;
|
|
50
|
-
}
|
|
51
87
|
if (!_themeId) {
|
|
52
88
|
renderOnce(React.createElement(MissingThemeIdError, null));
|
|
53
89
|
return;
|
|
54
90
|
}
|
|
55
91
|
const themeActionsApi = this.getApi(THEME_ACTIONS_API_NAME);
|
|
56
92
|
const themeSkinstoreApi = this.getApi(THEME_SKINSTORE_API_NAME);
|
|
57
|
-
const
|
|
58
|
-
actionType: THEME_ACTIONS_TYPES.
|
|
93
|
+
const publishAction = themeActionsApi.getThemeAction({
|
|
94
|
+
actionType: THEME_ACTIONS_TYPES.publish,
|
|
59
95
|
themeId: _themeId,
|
|
60
96
|
credentials
|
|
61
97
|
});
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
if (!publishAction) {
|
|
99
|
+
renderOnce(React.createElement(UnpermittedCommandError, { themeId: _themeId, commandName: "publish" }));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const publishFormAction = themeActionsApi.getThemeAction({
|
|
103
|
+
actionType: THEME_ACTIONS_TYPES.publishForm,
|
|
104
|
+
themeId: _themeId,
|
|
64
105
|
credentials
|
|
65
106
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
const releaseType = this.flags.release === 'test' ? RELEASE_TYPE_TEST : RELEASE_TYPE_STABLE;
|
|
108
|
+
const isUpgrade = !publishFormAction;
|
|
109
|
+
if (isUpgrade) {
|
|
110
|
+
await this.#handleUpgrade({
|
|
111
|
+
themeSkinstoreApi,
|
|
112
|
+
themeActionsApi,
|
|
113
|
+
publishAction,
|
|
114
|
+
credentials,
|
|
115
|
+
releaseType,
|
|
116
|
+
themeId: _themeId,
|
|
117
|
+
loggerApi
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
await this.#handleFirstPublish({
|
|
122
|
+
themeSkinstoreApi,
|
|
123
|
+
themeActionsApi,
|
|
124
|
+
publishFormAction,
|
|
125
|
+
publishAction,
|
|
126
|
+
credentials,
|
|
127
|
+
releaseType,
|
|
128
|
+
themeId: _themeId,
|
|
129
|
+
loggerApi
|
|
130
|
+
});
|
|
131
|
+
}
|
|
72
132
|
}
|
|
73
133
|
catch (err) {
|
|
134
|
+
if (err?.name === 'ExitPromptError') {
|
|
135
|
+
renderOnce(React.createElement(Info, { header: "Theme publish was cancelled." }));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
74
138
|
loggerApi.error('Theme publish command error:', {
|
|
75
139
|
error: err
|
|
76
140
|
});
|
|
77
141
|
renderOnce(React.createElement(ThemeError, { err: err, executionContext: executionContext }));
|
|
78
142
|
}
|
|
79
143
|
}
|
|
144
|
+
async #handleUpgrade({ themeSkinstoreApi, themeActionsApi, publishAction, credentials, releaseType, themeId, loggerApi }) {
|
|
145
|
+
renderOnce(React.createElement(Info, { header: `Upgrading published theme (ID: ${themeId})` },
|
|
146
|
+
React.createElement(Text, null,
|
|
147
|
+
"Release type: ",
|
|
148
|
+
this.flags.release)));
|
|
149
|
+
const { proceed } = await promptConfirmation('Proceed with upgrade?');
|
|
150
|
+
if (!proceed) {
|
|
151
|
+
renderOnce(React.createElement(Info, { header: "Theme publish was cancelled." }));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const result = await themeSkinstoreApi.publishTheme({
|
|
155
|
+
actionData: publishAction.data,
|
|
156
|
+
credentials,
|
|
157
|
+
payload: { release: releaseType }
|
|
158
|
+
});
|
|
159
|
+
if (result?.isSuccess) {
|
|
160
|
+
themeActionsApi.removeThemeActions({ themeId, credentials });
|
|
161
|
+
renderOnce(React.createElement(Success, { header: "Theme upgraded successfully!" },
|
|
162
|
+
React.createElement(Text, null,
|
|
163
|
+
"Release type: ",
|
|
164
|
+
this.flags.release)));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.#renderPublishErrors(result?.messages);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async #handleFirstPublish({ themeSkinstoreApi, themeActionsApi, publishFormAction, publishAction, credentials, releaseType, themeId, loggerApi }) {
|
|
171
|
+
const controls = await themeSkinstoreApi.getPublishFormData({
|
|
172
|
+
actionData: publishFormAction.data,
|
|
173
|
+
credentials
|
|
174
|
+
});
|
|
175
|
+
if (!controls || controls.length === 0) {
|
|
176
|
+
renderOnce(React.createElement(ErrorBox, { header: "Could not fetch publish form." },
|
|
177
|
+
React.createElement(Text, null, "Check if SkinStore SDK feature is enabled for your store.")));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const inquirerControls = toInquirerControls(controls.filter((c) => c.name !== 'release'));
|
|
181
|
+
const formData = await new Promise((resolve, reject) => {
|
|
182
|
+
Form({
|
|
183
|
+
controls: inquirerControls,
|
|
184
|
+
onSubmit: resolve
|
|
185
|
+
}).catch(reject);
|
|
186
|
+
});
|
|
187
|
+
let uploadedImageIds = [];
|
|
188
|
+
if (this.flags.images) {
|
|
189
|
+
uploadedImageIds = await this.#uploadImages({
|
|
190
|
+
themeSkinstoreApi,
|
|
191
|
+
credentials,
|
|
192
|
+
imagesDir: this.flags.images,
|
|
193
|
+
skinId: themeId,
|
|
194
|
+
loggerApi
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const payload = {
|
|
198
|
+
name: formData.name,
|
|
199
|
+
desc: formData.desc ?? '',
|
|
200
|
+
description: formData.description,
|
|
201
|
+
categories: Array.isArray(formData.categories) ? formData.categories : [formData.categories],
|
|
202
|
+
colors: Array.isArray(formData.colors) ? formData.colors : [formData.colors],
|
|
203
|
+
price: Number(formData.price) || 0,
|
|
204
|
+
release: releaseType,
|
|
205
|
+
...(uploadedImageIds.length > 0 ? { filesList: uploadedImageIds } : {})
|
|
206
|
+
};
|
|
207
|
+
const result = await themeSkinstoreApi.publishTheme({
|
|
208
|
+
actionData: publishAction.data,
|
|
209
|
+
credentials,
|
|
210
|
+
payload
|
|
211
|
+
});
|
|
212
|
+
if (result?.isSuccess) {
|
|
213
|
+
themeActionsApi.removeThemeActions({ themeId, credentials });
|
|
214
|
+
renderOnce(React.createElement(Success, { header: "Theme published successfully!" },
|
|
215
|
+
React.createElement(Text, null, "Your theme is now available on SkinStore.")));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.#renderPublishErrors(result?.messages);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async #uploadImages({ themeSkinstoreApi, credentials, imagesDir, skinId, loggerApi }) {
|
|
222
|
+
const imageIds = [];
|
|
223
|
+
if (!fs.existsSync(imagesDir)) {
|
|
224
|
+
renderOnce(React.createElement(ErrorBox, { header: "Images directory not found." },
|
|
225
|
+
React.createElement(Text, null,
|
|
226
|
+
"Path: ",
|
|
227
|
+
imagesDir)));
|
|
228
|
+
return imageIds;
|
|
229
|
+
}
|
|
230
|
+
const files = fs.readdirSync(imagesDir).filter((file) => {
|
|
231
|
+
const ext = path.extname(file).toLowerCase().replace('.', '');
|
|
232
|
+
return IMAGE_ALLOWED_EXTENSIONS.includes(ext);
|
|
233
|
+
});
|
|
234
|
+
for (const file of files) {
|
|
235
|
+
const filePath = path.join(imagesDir, file);
|
|
236
|
+
const stat = fs.statSync(filePath);
|
|
237
|
+
if (stat.size > IMAGE_MAX_SIZE) {
|
|
238
|
+
loggerApi.warn(`Skipping ${file}: exceeds ${IMAGE_MAX_SIZE} bytes limit`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const uploadUrl = `/webapi/cli/themes/${skinId}/skinstore/files`;
|
|
242
|
+
const result = await themeSkinstoreApi.uploadImage({
|
|
243
|
+
credentials,
|
|
244
|
+
uploadUrl,
|
|
245
|
+
imagePath: filePath
|
|
246
|
+
});
|
|
247
|
+
if (result?.isSuccess && result?.imageId) {
|
|
248
|
+
imageIds.push(result.imageId);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
loggerApi.warn(`Failed to upload ${file}`, { details: result?.messages });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return imageIds;
|
|
255
|
+
}
|
|
256
|
+
#renderPublishErrors(messages) {
|
|
257
|
+
if (!messages) {
|
|
258
|
+
renderOnce(React.createElement(ErrorBox, { header: "Publishing failed." },
|
|
259
|
+
React.createElement(Text, null, "An unknown error occurred.")));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
renderOnce(React.createElement(ErrorBox, { header: "Publishing failed. Validation errors:" }, Object.entries(messages).map(([field, msg]) => (React.createElement(Text, { key: field },
|
|
263
|
+
field,
|
|
264
|
+
": ",
|
|
265
|
+
typeof msg === 'string' ? msg : Object.values(msg).join(', '))))));
|
|
266
|
+
}
|
|
80
267
|
}
|
|
@@ -10,10 +10,10 @@ export class ThemeSkinstoreApi extends FeatureApi {
|
|
|
10
10
|
async getPublishFormData(props) {
|
|
11
11
|
return this.#service.getPublishFormData(props);
|
|
12
12
|
}
|
|
13
|
-
async
|
|
14
|
-
|
|
13
|
+
async publishTheme(props) {
|
|
14
|
+
return this.#service.publishTheme(props);
|
|
15
15
|
}
|
|
16
|
-
async
|
|
17
|
-
|
|
16
|
+
async uploadImage(props) {
|
|
17
|
+
return this.#service.uploadImage(props);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -14,4 +14,30 @@ export class ThemeSkinstoreHttpApi {
|
|
|
14
14
|
isPrivate: true
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
+
publishTheme({ actionData, shopUrl, payload }) {
|
|
18
|
+
const { method, url } = actionData;
|
|
19
|
+
return this.#httpApi.fetch({
|
|
20
|
+
url: `${shopUrl}${url}`,
|
|
21
|
+
method,
|
|
22
|
+
data: payload,
|
|
23
|
+
sanitizeOptions: {
|
|
24
|
+
disable: true
|
|
25
|
+
},
|
|
26
|
+
isPrivate: true
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
uploadImage({ shopUrl, url, imageBuffer }) {
|
|
30
|
+
return this.#httpApi.fetch({
|
|
31
|
+
url: `${shopUrl}${url}`,
|
|
32
|
+
method: 'post',
|
|
33
|
+
data: imageBuffer,
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/octet-stream'
|
|
36
|
+
},
|
|
37
|
+
sanitizeOptions: {
|
|
38
|
+
disable: true
|
|
39
|
+
},
|
|
40
|
+
isPrivate: true
|
|
41
|
+
});
|
|
42
|
+
}
|
|
17
43
|
}
|
|
@@ -2,13 +2,18 @@ import { STATUS_CODES } from '@dreamcommerce/star_core';
|
|
|
2
2
|
import { HttpErrorsFactory } from '../../../../../cli/class/errors/http/http_errors_factory.js';
|
|
3
3
|
import { DownloadFileErrorsFactory } from '../../../../../utils/download_file/download_file_errors_factory.js';
|
|
4
4
|
import { toControls } from '../../../../../cli/features/controls/controls_dto_mappers.js';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
const PUBLISH_RESPONSE_STATUSES = [STATUS_CODES.ok, STATUS_CODES.badRequest];
|
|
5
7
|
export class ThemeSkinstoreService {
|
|
6
8
|
#httpApi;
|
|
7
|
-
|
|
9
|
+
#logger;
|
|
10
|
+
constructor({ httpApi, logger }) {
|
|
8
11
|
this.#httpApi = httpApi;
|
|
12
|
+
this.#logger = logger;
|
|
9
13
|
}
|
|
10
14
|
async getPublishFormData({ credentials, actionData }) {
|
|
11
15
|
try {
|
|
16
|
+
this.#logger.info('Fetching publish form data');
|
|
12
17
|
const { response: request } = this.#httpApi.getPublishFormData({ actionData, shopUrl: credentials.shopUrl });
|
|
13
18
|
const response = await request;
|
|
14
19
|
if (response?.status !== STATUS_CODES.ok)
|
|
@@ -16,17 +21,64 @@ export class ThemeSkinstoreService {
|
|
|
16
21
|
return response?.data ? toControls(response.data) : [];
|
|
17
22
|
}
|
|
18
23
|
catch (err) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
this.#logger.error('Error fetching publish form data', { error: err });
|
|
25
|
+
this.#handleHttpError(err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async publishTheme({ credentials, actionData, payload }) {
|
|
29
|
+
try {
|
|
30
|
+
this.#logger.info('Publishing theme', {
|
|
31
|
+
details: { actionData }
|
|
32
|
+
});
|
|
33
|
+
const { response: request } = this.#httpApi.publishTheme({
|
|
34
|
+
actionData,
|
|
35
|
+
shopUrl: credentials.shopUrl,
|
|
36
|
+
payload
|
|
37
|
+
});
|
|
38
|
+
const response = await request;
|
|
39
|
+
if (!PUBLISH_RESPONSE_STATUSES.includes(response?.status))
|
|
40
|
+
return;
|
|
41
|
+
this.#logger.info('Successfully published theme');
|
|
42
|
+
return response?.data;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
this.#logger.error('Error publishing theme', { error: err });
|
|
46
|
+
this.#handleHttpError(err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async uploadImage({ credentials, uploadUrl, imagePath }) {
|
|
50
|
+
try {
|
|
51
|
+
this.#logger.info('Uploading image', {
|
|
52
|
+
details: { uploadUrl, imagePath }
|
|
53
|
+
});
|
|
54
|
+
const imageBuffer = fs.readFileSync(imagePath);
|
|
55
|
+
const { response: request } = this.#httpApi.uploadImage({
|
|
56
|
+
shopUrl: credentials.shopUrl,
|
|
57
|
+
url: uploadUrl,
|
|
58
|
+
imageBuffer
|
|
59
|
+
});
|
|
60
|
+
const response = await request;
|
|
61
|
+
if (response?.status !== STATUS_CODES.ok)
|
|
62
|
+
return;
|
|
63
|
+
this.#logger.info('Successfully uploaded image');
|
|
64
|
+
return response?.data;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
this.#logger.error('Error uploading image', { error: err });
|
|
68
|
+
this.#handleHttpError(err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
#handleHttpError(err) {
|
|
72
|
+
//TODO to basic class
|
|
73
|
+
switch (err.response?.status) {
|
|
74
|
+
case 403:
|
|
75
|
+
throw HttpErrorsFactory.createForbiddenError();
|
|
76
|
+
case 401:
|
|
77
|
+
throw HttpErrorsFactory.createUnauthorizedError();
|
|
78
|
+
case 404:
|
|
79
|
+
throw HttpErrorsFactory.createNotFoundError();
|
|
80
|
+
default:
|
|
81
|
+
throw DownloadFileErrorsFactory.downloadError(err.response?.status);
|
|
30
82
|
}
|
|
31
83
|
}
|
|
32
84
|
}
|
|
@@ -2,3 +2,7 @@ export const THEME_SKINSTORE_LOCATION = 'skinstore';
|
|
|
2
2
|
export const THEME_SKINSTORE_SETTINGS_FILE_NAME = 'settings.json';
|
|
3
3
|
export const THEME_SKINSTORE_API_NAME = 'ThemeSkinstoreApi';
|
|
4
4
|
export const THEME_SKINSTORE_FEATURE_NAME = 'ThemeSkinstore';
|
|
5
|
+
export const RELEASE_TYPE_TEST = 0;
|
|
6
|
+
export const RELEASE_TYPE_STABLE = 1;
|
|
7
|
+
export const IMAGE_MAX_SIZE = 5242880;
|
|
8
|
+
export const IMAGE_ALLOWED_EXTENSIONS = ['gif', 'jpeg', 'jpg', 'png', 'webp'];
|
|
@@ -3,11 +3,16 @@ import { THEME_SKINSTORE_FEATURE_NAME } from './theme_publish_constants.js';
|
|
|
3
3
|
import { ThemeSkinstoreService } from './service/theme_skinstore_service.js';
|
|
4
4
|
import { ThemeSkinstoreHttpApi } from './http/theme_skinstore_http_api.js';
|
|
5
5
|
import { ThemeSkinstoreApi } from './api/theme_skinstore_api.js';
|
|
6
|
+
import { LOGGER_API_NAME } from '../../../../cli/utilities/features/logger/logger_constants.js';
|
|
6
7
|
export class ThemeSkinstoreInitializer extends SyncFeatureInitializer {
|
|
7
8
|
static featureName = THEME_SKINSTORE_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 ThemeSkinstoreService({
|
|
13
|
+
httpApi: new ThemeSkinstoreHttpApi(httpApi),
|
|
14
|
+
logger: loggerApi
|
|
15
|
+
});
|
|
11
16
|
return {
|
|
12
17
|
cores: [
|
|
13
18
|
{
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { fileExists, isDirectory, readJSONFile, removeFile, writeJSONFile } from '../../../../../utils/fs/fs_utils.js';
|
|
2
|
-
import { join, looksLikeDirectory,
|
|
2
|
+
import { join, looksLikeDirectory, normalizeObjectPathKeys, toUnixPath } from '../../../../../utils/path_utils.js';
|
|
3
3
|
import { SHOPER_THEME_METADATA_DIR } from '../../../../constants/directory_contstants.js';
|
|
4
4
|
import { THEME_CURRENT_CHECKSUMS_FILE_NAME, THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME, THEME_FILES_STRUCTURE_FILE_NAME, THEME_INITIAL_CHECKSUMS_FILE_NAME, THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME } from '../../theme_constants.js';
|
|
5
|
-
import { isWindowsOs } from '../../../../../utils/platform_utils.js';
|
|
6
5
|
import { validateDirectory } from '../../../../utils/directory_validator/directory_validator_utils.js';
|
|
7
6
|
import path from 'node:path';
|
|
8
7
|
import { THEME_FILES_LIST_FILE_NAME } from '../../push/theme_push_constants.js';
|
|
@@ -12,9 +11,7 @@ import { loadShoperIgnore } from '../../../../utils/shoperignore/shoperignore_ut
|
|
|
12
11
|
export class ThemeFilesUtils {
|
|
13
12
|
static async getThemeFilesStructure(themeDirectory) {
|
|
14
13
|
const filesStructure = await readJSONFile(join(themeDirectory, SHOPER_THEME_METADATA_DIR, THEME_FILES_STRUCTURE_FILE_NAME));
|
|
15
|
-
|
|
16
|
-
return filesStructure;
|
|
17
|
-
return mapKeysPathsToWindowPlatform(filesStructure);
|
|
14
|
+
return normalizeObjectPathKeys(filesStructure);
|
|
18
15
|
}
|
|
19
16
|
static async writeThemeFilesStructure(themeDirectory, filesStructure) {
|
|
20
17
|
const filePath = join(themeDirectory, SHOPER_THEME_METADATA_DIR, THEME_FILES_STRUCTURE_FILE_NAME);
|
|
@@ -114,6 +111,15 @@ export class ThemeFilesUtils {
|
|
|
114
111
|
`${SHOPER_THEME_METADATA_DIR}/${THEME_CURRENT_CHECKSUMS_FILE_NAME}`,
|
|
115
112
|
`${SHOPER_THEME_METADATA_DIR}/${THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME}`
|
|
116
113
|
];
|
|
114
|
+
if (!fileStructure[`${SHOPER_THEME_METADATA_DIR}/`]) {
|
|
115
|
+
fileStructure[`${SHOPER_THEME_METADATA_DIR}/`] = {
|
|
116
|
+
permissions: {
|
|
117
|
+
canAdd: true,
|
|
118
|
+
canEdit: true,
|
|
119
|
+
canDelete: false
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
117
123
|
checksumsFiles.forEach((file) => {
|
|
118
124
|
fileStructure[file] = {
|
|
119
125
|
permissions: {
|
|
@@ -24,6 +24,8 @@ export class HiddenDirectoryUtils {
|
|
|
24
24
|
const fileRelativePath = join(SHOPER_THEME_METADATA_DIR, fileName);
|
|
25
25
|
const currentChecksum = await computeFileChecksum(fileFullPath);
|
|
26
26
|
const initialChecksum = await themeChecksums.getInitialChecksumFromPath(fileRelativePath);
|
|
27
|
+
if (!initialChecksum)
|
|
28
|
+
continue;
|
|
27
29
|
if (currentChecksum !== initialChecksum) {
|
|
28
30
|
throw new Error(`File ${fileName} inside theme metadata directory (${themeMetadataPath}) has been modified. You cannot modify files inside .shoper directory`);
|
|
29
31
|
}
|
package/build/theme/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { ThemeDeleteInitializer } from './features/theme/delete/theme_delete_ini
|
|
|
17
17
|
import { ThemeActionsInitializer } from './features/theme/actions/theme_actions_initializer.js';
|
|
18
18
|
import { ThemeWatchCommand } from './commands/watch/theme_watch_command.js';
|
|
19
19
|
import { ThemeWatchInitializer } from './features/theme/watch/theme_watch_initializer.js';
|
|
20
|
+
import { ThemeSkinstoreInitializer } from './features/theme/skinstore/theme_skinstore_initialzier.js';
|
|
20
21
|
export const COMMANDS = {
|
|
21
22
|
[THEME_COMMANDS_NAME.list]: ThemeListCommand,
|
|
22
23
|
[THEME_COMMANDS_NAME.pull]: ThemePullCommand,
|
|
@@ -34,6 +35,7 @@ export const COMMAND_TO_FEATURES_MAP = {
|
|
|
34
35
|
[THEME_COMMANDS_NAME.push]: [ThemeFetchInitializer, ThemePushInitializer],
|
|
35
36
|
[THEME_COMMANDS_NAME.verify]: ThemeVerifyInitializer,
|
|
36
37
|
[THEME_COMMANDS_NAME.delete]: ThemeDeleteInitializer,
|
|
38
|
+
[THEME_COMMANDS_NAME.publish]: ThemeSkinstoreInitializer,
|
|
37
39
|
[THEME_COMMANDS_NAME.watch]: [ThemeFetchInitializer, ThemePushInitializer, ThemeWatchInitializer]
|
|
38
40
|
};
|
|
39
41
|
export const getThemeInitializersForCommand = (commandName) => {
|
|
@@ -51,12 +51,12 @@ export const isUnixPath = (path) => {
|
|
|
51
51
|
return !path.includes('\\') && (path.startsWith('/') || path.includes('/'));
|
|
52
52
|
};
|
|
53
53
|
export const platformSeparator = sep;
|
|
54
|
-
export const
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
export const normalizeObjectPathKeys = (obj) => {
|
|
55
|
+
if (!obj)
|
|
56
|
+
return {};
|
|
57
|
+
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
58
|
+
acc[key.replace(/[\\/]/g, sep)] = value;
|
|
59
|
+
return acc;
|
|
60
60
|
}, {});
|
|
61
61
|
};
|
|
62
62
|
export const toCurrentPlatformPath = (path) => (isWindowsOs() ? toWinowsPath(path) : toUnixPath(path));
|