@shoper/cli 0.7.1-1 → 0.8.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.
Files changed (52) hide show
  1. package/README.md +11 -1
  2. package/build/cli/class/errors/http/http_errors_factory.js +1 -1
  3. package/build/cli/core/cli_setup.js +6 -6
  4. package/build/cli/index.js +0 -1
  5. package/build/cli/utilities/features/logger/logs/app_logs_constants.js +1 -2
  6. package/build/index.js +2 -2
  7. package/build/theme/class/archive/theme_archive.js +47 -1
  8. package/build/theme/{features/theme/utils/archive/theme_archive_utils_errors_factory.js → class/archive/theme_archive_errors_factory.js} +1 -1
  9. package/build/theme/class/checksums/theme_checksums.js +11 -51
  10. package/build/theme/commands/pull/theme_pull_command.js +3 -4
  11. package/build/theme/commands/push/theme_push_command.js +8 -17
  12. package/build/theme/commands/theme_commands_constants.js +1 -2
  13. package/build/theme/commands/theme_verify_command.js +3 -3
  14. package/build/theme/commands/ui/theme_error.js +6 -6
  15. package/build/theme/features/theme/actions/theme_actions_constants.js +1 -2
  16. package/build/theme/features/theme/actions/theme_actions_utils.js +9 -60
  17. package/build/theme/features/theme/fetch/service/theme_fetch_service.js +4 -2
  18. package/build/theme/features/theme/init/service/theme_init_service.js +4 -2
  19. package/build/theme/features/theme/merge/service/theme_merge_service.js +2 -2
  20. package/build/theme/features/theme/push/api/theme_push_api.js +2 -2
  21. package/build/theme/features/theme/push/service/theme_push_service.js +33 -93
  22. package/build/theme/features/theme/push/theme_push_utils.js +9 -6
  23. package/build/theme/features/theme/utils/files_structure/theme_file_structure_errors_factory.js +9 -1
  24. package/build/theme/features/theme/utils/{files/theme_files_utils.js → files_structure/theme_files_structure_utils.js} +28 -36
  25. package/build/theme/features/theme/utils/meta_data/theme_meta_data_utils.js +28 -0
  26. package/build/theme/features/theme/verify/verify/theme_verify_service.js +12 -19
  27. package/build/theme/index.js +2 -6
  28. package/build/theme/utils/directory_validator/directory_validator_utils.js +11 -4
  29. package/build/theme/utils/shoperignore/shoperignore_utils.js +36 -0
  30. package/build/ui/ui_dump/ui_dump.js +4 -9
  31. package/build/utils/array_utils.js +0 -3
  32. package/build/utils/fs/fs_utils.js +1 -1
  33. package/package.json +6 -10
  34. package/build/theme/class/browser/browser.js +0 -108
  35. package/build/theme/commands/watch/theme_watch_command.js +0 -88
  36. package/build/theme/commands/watch/theme_watch_constants.js +0 -21
  37. package/build/theme/commands/watch/theme_watch_utils.js +0 -32
  38. package/build/theme/commands/watch/theme_watching_info.js +0 -14
  39. package/build/theme/commands/watch/watch.js +0 -55
  40. package/build/theme/features/theme/push/service/theme_push_service_types.js +0 -1
  41. package/build/theme/features/theme/utils/archive/theme_archive_utils.js +0 -26
  42. package/build/theme/features/theme/utils/files/them_files_constants.js +0 -1
  43. package/build/theme/features/theme/watch/api/theme_watch_api.js +0 -19
  44. package/build/theme/features/theme/watch/service/theme_watch_service.js +0 -167
  45. package/build/theme/features/theme/watch/theme_watch_constants.js +0 -4
  46. package/build/theme/features/theme/watch/theme_watch_initializer.js +0 -22
  47. package/build/ui/command_input/command_input.js +0 -25
  48. package/build/ui/logs/log_entry.js +0 -12
  49. package/build/ui/logs/logs_constants.js +0 -20
  50. package/build/ui/logs/logs_list.js +0 -18
  51. package/build/ui/logs/use_logs.js +0 -23
  52. package/build/utils/fs/fs_constants.js +0 -6
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@shoper/cli",
3
3
  "packageManager": "yarn@3.2.0",
4
4
  "sideEffects": false,
5
- "version": "0.7.1-1",
5
+ "version": "0.8.0",
6
6
  "description": "CLI tool for Shoper",
7
7
  "author": "Joanna Firek",
8
8
  "license": "MIT",
@@ -48,13 +48,14 @@
48
48
  "chalk": "5.4.1",
49
49
  "conf": "13.1.0",
50
50
  "fast-glob": "3.3.3",
51
+ "figlet": "1.9.4",
51
52
  "figures": "6.1.0",
52
53
  "fs-extra": "11.3.0",
53
54
  "fs-tree-diff": "2.0.1",
55
+ "ignore": "7.0.5",
54
56
  "ink": "6.0.1",
55
57
  "ink-link": "4.1.0",
56
58
  "ink-gradient": "3.0.0",
57
- "ink-text-input": "6.0.0",
58
59
  "inquirer": "12.5.2",
59
60
  "inquirer-select-line": "1.1.3",
60
61
  "is-hidden-file": "1.1.2",
@@ -68,18 +69,14 @@
68
69
  "pino-pretty": "13.1.2",
69
70
  "react": "19.1.0",
70
71
  "reflect-metadata": "0.2.2",
71
- "chokidar": "4.0.3",
72
72
  "rxjs": "7.8.2",
73
73
  "semver": "7.7.1",
74
74
  "strip-ansi": "7.1.0",
75
75
  "tmp-promise": "3.0.3",
76
76
  "uuid": "11.1.0",
77
- "micromatch": "4.0.8",
78
77
  "walk-sync": "3.0.0",
79
78
  "yauzl": "3.2.0",
80
- "figlet": "1.9.4",
81
- "yazl": "3.3.1",
82
- "puppeteer": "24.31.0"
79
+ "yazl": "3.3.1"
83
80
  },
84
81
  "devDependencies": {
85
82
  "@babel/core": "7.27.1",
@@ -90,15 +87,14 @@
90
87
  "@types/fs-extra": "11.0.4",
91
88
  "@types/jest": "29.5.14",
92
89
  "@types/jsonwebtoken": "9.0.9",
90
+ "@types/klaw": "3.0.7",
91
+ "@types/lodash": "4.17.17",
93
92
  "@types/node": "18.19.84",
94
93
  "@types/react": "19.1.8",
95
94
  "@types/semver": "7.7.0",
96
95
  "@types/tmp": "0.2.6",
97
96
  "@types/yauzl": "2.10.3",
98
97
  "@types/yazl": "2.4.6",
99
- "@types/klaw": "3.0.7",
100
- "@types/lodash": "4.17.17",
101
- "@types/micromatch": "4.0.9",
102
98
  "@typescript-eslint/eslint-plugin": "8.29.1",
103
99
  "babel-jest": "29.7.0",
104
100
  "eslint": "9.39.1",
@@ -1,108 +0,0 @@
1
- import puppeteer from 'puppeteer';
2
- import { v4 as uuid } from 'uuid';
3
- export class Browser {
4
- #browser = null;
5
- #tabs = new Map();
6
- #onError;
7
- #onInfo;
8
- #onDisconnected;
9
- constructor({ onError, onInfo, onDisconnected } = {}) {
10
- this.#onError = onError;
11
- this.#onInfo = onInfo;
12
- this.#onDisconnected = onDisconnected;
13
- }
14
- async launch() {
15
- this.#onInfo?.(`Launching browser...`);
16
- try {
17
- if (!this.#browser) {
18
- this.#onInfo?.(`Launching browser...`);
19
- this.#browser = await puppeteer.launch({
20
- headless: false,
21
- defaultViewport: null
22
- });
23
- this.#browser.on('disconnected', () => {
24
- this.#onDisconnected?.('Browser disconnected');
25
- this.#browser = null;
26
- this.#tabs.clear();
27
- });
28
- }
29
- }
30
- catch (error) {
31
- this.#onError?.(`Error launch browser:`, error);
32
- throw error;
33
- }
34
- }
35
- async openTab(url, tabId) {
36
- try {
37
- if (!this.#browser || !this.isBrowserOpen()) {
38
- this.#onInfo?.(`Browser not open, launching browser...`);
39
- await this.launch();
40
- }
41
- if (!this.#browser)
42
- return;
43
- const id = tabId ?? `tab-${uuid()}`;
44
- this.#onInfo?.(`Opening new tab ${id}: ${url}`);
45
- const page = await this.#browser.newPage();
46
- await page.goto(url, { waitUntil: 'networkidle2' });
47
- this.#tabs.set(id, { page, url });
48
- this.#onInfo?.(`Tab ${id} opened successfully`);
49
- }
50
- catch (error) {
51
- this.#onError?.(`Error opening tab:`, error);
52
- throw error;
53
- }
54
- }
55
- async refresh(tabId) {
56
- const tab = this.#tabs.get(tabId);
57
- if (!tab) {
58
- this.#onError?.(`Tab ${tabId} not found`);
59
- return;
60
- }
61
- try {
62
- this.#onInfo?.(`Refreshing tab ${tabId}...`);
63
- await tab.page.reload({ waitUntil: 'networkidle2' });
64
- this.#onInfo?.(`Tab ${tabId} refreshed`);
65
- }
66
- catch (error) {
67
- this.#onError?.(`Error refreshing tab ${tabId}:`, error);
68
- throw error;
69
- }
70
- }
71
- async closeTab(tabId) {
72
- const tab = this.#tabs.get(tabId);
73
- if (!tab) {
74
- this.#onError?.(`Tab ${tabId} not found`);
75
- return;
76
- }
77
- try {
78
- await tab.page.close();
79
- this.#tabs.delete(tabId);
80
- this.#onInfo?.(`Tab ${tabId} closed`);
81
- }
82
- catch (error) {
83
- this.#onError?.(`Error closing tab ${tabId}:`, error);
84
- throw error;
85
- }
86
- }
87
- async close() {
88
- if (!this.isBrowserOpen())
89
- return;
90
- await this.#browser?.close();
91
- this.#browser = null;
92
- this.#tabs.clear();
93
- this.#onInfo?.('Browser closed with all tabs');
94
- }
95
- isBrowserOpen() {
96
- return this.#browser !== null && this.#browser.connected;
97
- }
98
- isTabOpen(tabId) {
99
- const tab = this.#tabs.get(tabId);
100
- return tab !== undefined && !tab.page.isClosed();
101
- }
102
- getOpenTabs() {
103
- return Array.from(this.#tabs.keys()).filter((tabId) => this.isTabOpen(tabId));
104
- }
105
- getTabUrl(tabId) {
106
- return this.#tabs.get(tabId)?.url ?? null;
107
- }
108
- }
@@ -1,88 +0,0 @@
1
- import { BaseThemeCommand } from '../../class/base_theme_command.js';
2
- import { CLI_AUTH_API_NAME } from '../../../cli/auth/cli_auth_constants.js';
3
- import { THEME_ACTIONS_API_NAME, THEME_ACTIONS_TYPES } from '../../features/theme/actions/theme_actions_constants.js';
4
- import { EXECUTION_CONTEXT_API_NAME, EXECUTION_CONTEXTS } from '../../../cli/features/execution_context/execution_context_constants.js';
5
- import { THEME_WATCH_API_NAME } from '../../features/theme/watch/theme_watch_constants.js';
6
- import { render, renderOnce } from '../../../ui/ui_utils.js';
7
- import { MissingCredentialsError } from '../../../cli/commands/auth/ui/missing_credentials_error.js';
8
- import { OutsideOfThemeDirectoryContextError } from '../ui/ouside_of_theme_directory_context_error.js';
9
- import { UnpermittedCommandError } from '../ui/unpermitted_command_error.js';
10
- import React from 'react';
11
- import { ThemeChecksums } from '../../class/checksums/theme_checksums.js';
12
- import { ThemeMetaDataUtils } from '../../features/theme/utils/meta_data/theme_meta_data_utils.js';
13
- import { ThemeFilesUtils } from '../../features/theme/utils/files/theme_files_utils.js';
14
- import { MissingThemeFiles } from '../ui/missing_theme_files.js';
15
- import { SHOPER_THEME_METADATA_DIR } from '../../constants/directory_contstants.js';
16
- import { THEME_FILES_STRUCTURE_FILE_NAME } from '../../features/theme/theme_constants.js';
17
- import { ThemeFilesUpload } from '../../class/files_upload/theme_files_upload.js';
18
- import { ThemeFilesUploadHttpApi } from '../../class/files_upload/theme_files_upload_http_api.js';
19
- import { HTTP_REQUESTER_API_NAME } from '@dreamcommerce/star_core';
20
- import { LOGGER_API_NAME } from '../../../cli/utilities/features/logger/logger_constants.js';
21
- import { Watch } from './watch.js';
22
- import { Flags } from '@oclif/core';
23
- import { ThemeError } from '../ui/theme_error.js';
24
- export class ThemeWatchCommand extends BaseThemeCommand {
25
- static summary = 'Watch local theme files for changes and update your store preview in real time.';
26
- static description = `Continuously monitors your local theme files for any changes and uploads them automatically to your store. Your store preview will refresh in real time to reflect updates immediately.\n\nYou must run this command from a specific theme directory (ID not needed).`;
27
- static examples = [
28
- {
29
- description: 'This will replace the store version with your local copy on files modification.',
30
- command: '<%= config.bin %> <%= command.id %>'
31
- }
32
- ];
33
- static flags = {
34
- open: Flags.boolean({
35
- description: 'Open the store preview in your default web browser when watching starts.',
36
- required: false,
37
- default: false
38
- })
39
- };
40
- async run() {
41
- const cliAuthApi = this.getApi(CLI_AUTH_API_NAME);
42
- const loggerApi = this.getApi(LOGGER_API_NAME);
43
- const themeWatchApi = this.getApi(THEME_WATCH_API_NAME);
44
- const themeActionsApi = this.getApi(THEME_ACTIONS_API_NAME);
45
- const executionContextApi = this.getApi(EXECUTION_CONTEXT_API_NAME);
46
- const credentials = cliAuthApi.getCredentials();
47
- const { flags } = await this.parse(ThemeWatchCommand);
48
- if (!credentials) {
49
- renderOnce(React.createElement(MissingCredentialsError, null));
50
- return;
51
- }
52
- const executionContext = await executionContextApi.getExecutionContext();
53
- if (executionContext.type !== EXECUTION_CONTEXTS.theme) {
54
- renderOnce(React.createElement(OutsideOfThemeDirectoryContextError, null));
55
- return;
56
- }
57
- try {
58
- const themeChecksums = new ThemeChecksums({
59
- themeDir: executionContext.themeRootDir,
60
- loggerApi
61
- });
62
- await ThemeMetaDataUtils.ensureThemeWorkUrlMatch(executionContext.themeRootDir, credentials.shopUrl);
63
- const watchAction = themeActionsApi.getThemeAction({
64
- actionType: THEME_ACTIONS_TYPES.partialPush,
65
- themeId: executionContext.themeId,
66
- credentials
67
- });
68
- if (!watchAction) {
69
- renderOnce(React.createElement(UnpermittedCommandError, { themeId: executionContext.themeId, commandName: "watch" }));
70
- return;
71
- }
72
- const filesStructure = await ThemeFilesUtils.getThemeFilesStructure(executionContext.themeRootDir);
73
- if (!filesStructure) {
74
- renderOnce(React.createElement(MissingThemeFiles, { files: `${SHOPER_THEME_METADATA_DIR}/${THEME_FILES_STRUCTURE_FILE_NAME}` }));
75
- return;
76
- }
77
- const themeFilesUploadApi = new ThemeFilesUpload({
78
- themeRootDir: executionContext.themeRootDir,
79
- credentials,
80
- themeFilesUploadHttpApi: new ThemeFilesUploadHttpApi(this.getApi(HTTP_REQUESTER_API_NAME))
81
- });
82
- return render(React.createElement(Watch, { executionContext: executionContext, themeChecksums: themeChecksums, credentials: credentials, themeWatchApi: themeWatchApi, themeFilesUploadApi: themeFilesUploadApi, filesStructure: filesStructure, action: watchAction, openBrowser: flags.open }));
83
- }
84
- catch (err) {
85
- renderOnce(React.createElement(ThemeError, { err: err, executionContext: executionContext }));
86
- }
87
- }
88
- }
@@ -1,21 +0,0 @@
1
- export const WATCH_COMMANDS_NAMES = {
2
- quit: 'quit',
3
- open: 'open',
4
- clear: 'clear'
5
- };
6
- export const WATCH_COMMANDS = [
7
- {
8
- key: WATCH_COMMANDS_NAMES.quit,
9
- description: 'Exit watch mode'
10
- },
11
- {
12
- key: WATCH_COMMANDS_NAMES.open,
13
- description: "Open the theme in a new tab in the browser if it's not already open."
14
- },
15
- {
16
- key: WATCH_COMMANDS_NAMES.clear,
17
- description: 'Clear the logs from the screen.'
18
- }
19
- ];
20
- export const MAX_WATCH_LOGS = 300;
21
- export const COMMAND_PREFIX = '/';
@@ -1,32 +0,0 @@
1
- import { COMMAND_PREFIX, WATCH_COMMANDS_NAMES } from './theme_watch_constants.js';
2
- import { APP_LOGS_TYPES } from '../../../cli/utilities/features/logger/logs/app_logs_constants.js';
3
- import { ThemeError } from '../ui/theme_error.js';
4
- import React from 'react';
5
- import chalk from 'chalk';
6
- import { CSS_TEXT_COLORS } from '../../../ui/color_constants.js';
7
- export class ThemeWatchUtils {
8
- static mapAppLogToUiMessage(log) {
9
- switch (log.level) {
10
- case APP_LOGS_TYPES.success:
11
- return this._mapSuccessLogToUiMessage(log);
12
- case APP_LOGS_TYPES.info:
13
- return this._mapInfoLogToUiMessage(log);
14
- case APP_LOGS_TYPES.error:
15
- return this._mapErrorLogToUiMessage(log);
16
- default:
17
- return log.message;
18
- }
19
- }
20
- static _mapInfoLogToUiMessage(log) {
21
- if (log.message === 'Browser closed') {
22
- return `Browser closed. If you want to open it again, use the ${COMMAND_PREFIX}${WATCH_COMMANDS_NAMES.open} command or execute again "shoper theme watch --open".`;
23
- }
24
- return log.message;
25
- }
26
- static _mapErrorLogToUiMessage(log) {
27
- return React.createElement(ThemeError, { err: log });
28
- }
29
- static _mapSuccessLogToUiMessage(log) {
30
- return chalk.hex(CSS_TEXT_COLORS.success)(log.message);
31
- }
32
- }
@@ -1,14 +0,0 @@
1
- import { Info } from '../../../ui/message_box/info.js';
2
- import React from 'react';
3
- import { Text } from '../../../ui/text.js';
4
- import { CSS_COLOR_TOKENS_VALUES } from '../../../ui/color_constants.js';
5
- import { COMMAND_PREFIX, WATCH_COMMANDS_NAMES } from './theme_watch_constants.js';
6
- export const ThemeWatchingInfo = ({ themeId, withPreview }) => {
7
- return (React.createElement(Info, { header: `Watching theme (ID: ${themeId}) files for changes... ` },
8
- React.createElement(Text, null, "Any modifications will be reflected in real-time."),
9
- withPreview ? React.createElement(Text, null, "Live preview opened in your browser.") : null,
10
- React.createElement(Text, { color: CSS_COLOR_TOKENS_VALUES.theme4 },
11
- "(Press Ctrl+C or type ",
12
- `${COMMAND_PREFIX}${WATCH_COMMANDS_NAMES.quit}`,
13
- " to stop watching)")));
14
- };
@@ -1,55 +0,0 @@
1
- import { ThemeWatchingInfo } from './theme_watching_info.js';
2
- import { Box } from '../../../ui/box.js';
3
- import React, { useEffect } from 'react';
4
- import { CommandInput } from '../../../ui/command_input/command_input.js';
5
- import { useApp } from 'ink';
6
- import { COMMAND_PREFIX, MAX_WATCH_LOGS, WATCH_COMMANDS, WATCH_COMMANDS_NAMES } from './theme_watch_constants.js';
7
- import { useLogs } from '../../../ui/logs/use_logs.js';
8
- import { LogsList } from '../../../ui/logs/logs_list.js';
9
- import { ThemeWatchUtils } from './theme_watch_utils.js';
10
- export const Watch = ({ executionContext, credentials, openBrowser, themeChecksums, themeFilesUploadApi, filesStructure, action, themeWatchApi }) => {
11
- const { exit } = useApp();
12
- const { logs, addLog, clearLogs } = useLogs(MAX_WATCH_LOGS);
13
- useEffect(() => {
14
- themeWatchApi.watchTheme({
15
- themeChecksums,
16
- executionContext,
17
- credentials,
18
- themeFilesUploadApi,
19
- filesStructure,
20
- action,
21
- openBrowser,
22
- onMessage: (log) => {
23
- addLog(ThemeWatchUtils.mapAppLogToUiMessage(log));
24
- }
25
- });
26
- return () => {
27
- themeWatchApi.stopWatching();
28
- };
29
- }, []);
30
- const handleCommandSubmit = async (value) => {
31
- const command = value.trim();
32
- addLog(`> ${command}`);
33
- switch (command) {
34
- case `${COMMAND_PREFIX}${WATCH_COMMANDS_NAMES.open}`: {
35
- await themeWatchApi.ensureBrowserTabOpen();
36
- break;
37
- }
38
- case `${COMMAND_PREFIX}${WATCH_COMMANDS_NAMES.quit}`: {
39
- await themeWatchApi.stopWatching();
40
- exit();
41
- break;
42
- }
43
- case `${COMMAND_PREFIX}${WATCH_COMMANDS_NAMES.clear}`: {
44
- clearLogs();
45
- break;
46
- }
47
- default:
48
- addLog(`Unknown command: ${command}`);
49
- }
50
- };
51
- return (React.createElement(Box, { flexDirection: "column", padding: 1 },
52
- React.createElement(ThemeWatchingInfo, { withPreview: openBrowser, themeId: executionContext.themeId }),
53
- React.createElement(LogsList, { logs: logs }),
54
- React.createElement(CommandInput, { commands: WATCH_COMMANDS, onSubmit: handleCommandSubmit })));
55
- };
@@ -1,26 +0,0 @@
1
- import { createZip } from '../../../../../utils/zip/create_zip_utils.js';
2
- import { extname, join } from '../../../../../utils/path_utils.js';
3
- import { formatJSONFile } from '../../../../../utils/fs/fs_utils.js';
4
- import { ThemeArchiveErrorsFactory } from './theme_archive_utils_errors_factory.js';
5
- export class ThemeArchiveUtils {
6
- static async create({ dist, files, rootDir, logger }) {
7
- try {
8
- // TODO spradzić czy potrzebne, ew po formatowanu sprawdzic czy plik sie zmienił jak nie to nie podmieniac contentu zformatowanego
9
- // await this._formatJsonFiles(rootDir, files);
10
- return createZip({
11
- files,
12
- baseDir: rootDir,
13
- logger,
14
- dist
15
- });
16
- }
17
- catch (err) {
18
- throw ThemeArchiveErrorsFactory.createErrorWhileCreatingThemeArchive(err);
19
- }
20
- }
21
- static async _formatJsonFiles(themeRootDir, filesToUpload) {
22
- await Promise.all(filesToUpload
23
- .filter((path) => extname(path).toLowerCase() === '.json')
24
- .map((jsonFile) => formatJSONFile(join(themeRootDir, jsonFile))));
25
- }
26
- }
@@ -1 +0,0 @@
1
- export const FILES_LIST_CUSTOM_MODULES_TO_KEEP_IDS = 'customModulesIdsToKeep';
@@ -1,19 +0,0 @@
1
- import { FeatureApi } from '@dreamcommerce/star_core';
2
- import { THEME_WATCH_API_NAME } from '../theme_watch_constants.js';
3
- export class ThemeWatchApi extends FeatureApi {
4
- moduleName = THEME_WATCH_API_NAME;
5
- #service;
6
- constructor(service) {
7
- super();
8
- this.#service = service;
9
- }
10
- async watchTheme(props) {
11
- return this.#service.watchTheme(props);
12
- }
13
- stopWatching() {
14
- return this.#service.stopWatching();
15
- }
16
- ensureBrowserTabOpen() {
17
- return this.#service.ensureBrowserTabOpen();
18
- }
19
- }
@@ -1,167 +0,0 @@
1
- import { AppLog } from '../../../../../cli/utilities/features/logger/logs/app_log.js';
2
- import chokidar from 'chokidar';
3
- import { APP_LOGS_TYPES } from '../../../../../cli/utilities/features/logger/logs/app_logs_constants.js';
4
- import { relative } from '../../../../../utils/path_utils.js';
5
- import { SHOP_BROWSER_TAB_ID, THEME_BATCH_DELAY_MS } from '../theme_watch_constants.js';
6
- import { FILE_STATES } from '../../../../../utils/fs/fs_constants.js';
7
- export class ThemeWatchService {
8
- #themePushApi;
9
- #isPushing = false;
10
- #pendingPush = false;
11
- #browserApi;
12
- #batchTimeout = null;
13
- #onMessage = null;
14
- #changedFiles = new Set();
15
- #watcher = null;
16
- #urlForWatchedTheme = null;
17
- constructor({ themePushApi, browserClient }) {
18
- this.#themePushApi = themePushApi;
19
- this.#browserApi = new browserClient({
20
- onDisconnected: () => {
21
- this._onInfoMessage('Browser closed');
22
- }
23
- });
24
- }
25
- async watchTheme({ themeChecksums, executionContext, credentials, themeFilesUploadApi, filesStructure, action, openBrowser = false, onMessage }) {
26
- this.#onMessage = onMessage || null;
27
- const themePath = executionContext.themeRootDir;
28
- this.#urlForWatchedTheme = `${credentials.shopUrl}/admin/configSkins/skin-preview/id/${executionContext.themeId}`;
29
- if (openBrowser) {
30
- await this.#browserApi.launch();
31
- await this.#browserApi.openTab(this.#urlForWatchedTheme, SHOP_BROWSER_TAB_ID);
32
- }
33
- this._onInfoMessage(`Watching for changes in: ${themePath}`);
34
- this.#watcher = chokidar.watch(themePath, {
35
- ignored: /^(?:.*[/\\])?\..*$/,
36
- persistent: true,
37
- ignoreInitial: true,
38
- awaitWriteFinish: {
39
- stabilityThreshold: 100,
40
- pollInterval: 50
41
- }
42
- });
43
- const handleFileChange = async (filePath) => {
44
- const relativePath = relative(themePath, filePath);
45
- await themeChecksums.updateCurrentChecksums();
46
- const fileState = await themeChecksums.getFileState(relativePath);
47
- if (fileState == FILE_STATES.unchanged)
48
- return;
49
- this._onInfoMessage(`Detected change in: ${relativePath} ${fileState}`);
50
- this.#changedFiles.add(relativePath);
51
- if (this.#batchTimeout)
52
- this._clearQueuedBatch();
53
- this.#batchTimeout = setTimeout(async () => {
54
- const fileCount = this.#changedFiles.size;
55
- this._onInfoMessage(`Processing batch of ${fileCount} file(s)`);
56
- this.#batchTimeout = null;
57
- if (this.#isPushing) {
58
- this._onInfoMessage(`Push in progress, queuing another push...`);
59
- this.#pendingPush = true;
60
- return;
61
- }
62
- try {
63
- await this._executePush({
64
- credentials,
65
- filesStructure,
66
- action,
67
- executionContext,
68
- themeChecksums,
69
- themeFilesUploadApi
70
- });
71
- }
72
- catch (err) {
73
- this.#onMessage?.(err);
74
- }
75
- }, THEME_BATCH_DELAY_MS);
76
- };
77
- this.#watcher
78
- .on('add', handleFileChange)
79
- .on('change', handleFileChange)
80
- .on('unlink', handleFileChange)
81
- .on('error', (error) => this._onErrorMessage(`Watcher error:`, error))
82
- .on('ready', () => this._onInfoMessage('Initial scan complete. Ready for changes.'));
83
- }
84
- async ensureBrowserTabOpen() {
85
- if (!this.#urlForWatchedTheme)
86
- return;
87
- if (!this.#browserApi.isBrowserOpen()) {
88
- this._onInfoMessage(`Browser is not open, launching...`);
89
- await this.#browserApi.launch();
90
- await this.#browserApi.openTab(this.#urlForWatchedTheme, SHOP_BROWSER_TAB_ID);
91
- this._onInfoMessage(`Browser opened with new tab`);
92
- return;
93
- }
94
- if (!this.#browserApi.isTabOpen(SHOP_BROWSER_TAB_ID)) {
95
- this._onInfoMessage(`Tab was closed, reopening...`);
96
- await this.#browserApi.openTab(this.#urlForWatchedTheme, SHOP_BROWSER_TAB_ID);
97
- return;
98
- }
99
- this._onInfoMessage(`Browser and tab already open`);
100
- }
101
- async stopWatching() {
102
- this._onInfoMessage('Shutting down...');
103
- if (this.#batchTimeout)
104
- this._clearQueuedBatch();
105
- await this.#watcher?.close();
106
- await this.#browserApi.close();
107
- this.#urlForWatchedTheme = null;
108
- }
109
- _clearQueuedBatch() {
110
- if (this.#batchTimeout) {
111
- clearTimeout(this.#batchTimeout);
112
- this.#batchTimeout = null;
113
- }
114
- }
115
- async _refreshBrowserTab() {
116
- if (!SHOP_BROWSER_TAB_ID || !this.#browserApi.isTabOpen(SHOP_BROWSER_TAB_ID))
117
- return;
118
- try {
119
- this._onInfoMessage(`Refreshing browser tab...`);
120
- await this.#browserApi.refresh(SHOP_BROWSER_TAB_ID);
121
- this._onInfoMessage(`Browser tab refreshed`);
122
- }
123
- catch (error) {
124
- this._onErrorMessage(`Error refreshing browser:`, error);
125
- }
126
- }
127
- async _executePush(params) {
128
- this.#isPushing = true;
129
- this.#pendingPush = false;
130
- try {
131
- this.#changedFiles.clear();
132
- this._onInfoMessage(`Pushing theme...`);
133
- await this.#themePushApi.partialPush(params);
134
- this._onSuccessMessage(`Theme pushed successfully`);
135
- await this._refreshBrowserTab();
136
- if (this.#pendingPush) {
137
- this._onInfoMessage(`Executing queued push...`);
138
- this.#isPushing = false;
139
- await this._executePush(params);
140
- }
141
- }
142
- finally {
143
- this.#isPushing = false;
144
- }
145
- }
146
- _onInfoMessage(message, details) {
147
- this.#onMessage?.(new AppLog({
148
- level: APP_LOGS_TYPES.info,
149
- message,
150
- details
151
- }));
152
- }
153
- _onErrorMessage(message, error) {
154
- this.#onMessage?.(new AppLog({
155
- level: APP_LOGS_TYPES.error,
156
- message,
157
- details: error
158
- }));
159
- }
160
- _onSuccessMessage(message, details) {
161
- this.#onMessage?.(new AppLog({
162
- level: APP_LOGS_TYPES.success,
163
- message,
164
- details
165
- }));
166
- }
167
- }
@@ -1,4 +0,0 @@
1
- export const THEME_WATCH_FEATURE_NAME = 'ThemeWatch';
2
- export const THEME_WATCH_API_NAME = 'ThemeWatchApi';
3
- export const THEME_BATCH_DELAY_MS = 750;
4
- export const SHOP_BROWSER_TAB_ID = 'shop-page';
@@ -1,22 +0,0 @@
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
- import { Browser } from '../../../class/browser/browser.js';
7
- //TODO to pownna byc inicjalizacja w komponencie reaktowym/komendzie watch.tsx
8
- export class ThemeWatchInitializer extends SyncFeatureInitializer {
9
- static featureName = THEME_WATCH_FEATURE_NAME;
10
- init() {
11
- const themePushApi = this.getApiSync(THEME_PUSH_API_NAME);
12
- const service = new ThemeWatchService({ themePushApi, browserClient: Browser });
13
- return {
14
- cores: [
15
- {
16
- type: FEATURE_CORES_TYPES.api,
17
- instance: new ThemeWatchApi(service)
18
- }
19
- ]
20
- };
21
- }
22
- }
@@ -1,25 +0,0 @@
1
- import { Box } from '../box.js';
2
- import { Text } from '../text.js';
3
- import React, { useState } from 'react';
4
- import TextInput from 'ink-text-input';
5
- import { COMMAND_PREFIX } from '../../theme/commands/watch/theme_watch_constants.js';
6
- export const CommandInput = ({ onSubmit, commands = [] }) => {
7
- const [query, setQuery] = useState('');
8
- const showSuggestions = query.startsWith(COMMAND_PREFIX);
9
- const searchTerm = query.substring(1);
10
- const filteredCommands = commands.filter((cmd) => cmd.key.startsWith(searchTerm));
11
- const handleSubmit = (value) => {
12
- onSubmit(value);
13
- setQuery('');
14
- };
15
- return (React.createElement(Box, { flexDirection: "column" },
16
- React.createElement(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "row" },
17
- React.createElement(Text, { color: "blue" }, '> '),
18
- React.createElement(TextInput, { value: query, onChange: setQuery, onSubmit: handleSubmit, placeholder: `Type ${COMMAND_PREFIX} to see commands...` })),
19
- showSuggestions ? (React.createElement(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2 },
20
- filteredCommands.map((cmd) => (React.createElement(Box, { key: cmd.key },
21
- React.createElement(Box, { width: 15 },
22
- React.createElement(Text, { color: "cyan", bold: true }, cmd.key)),
23
- React.createElement(Text, { color: "gray" }, cmd.description)))),
24
- filteredCommands.length === 0 && React.createElement(Text, { color: "red" }, "No matching commands found"))) : null));
25
- };