@shoper/cli 0.8.3-1 → 0.9.1-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 CHANGED
@@ -2,4 +2,14 @@
2
2
 
3
3
  # Shoper CLI
4
4
 
5
- Learn more in the [commands docs](https://storefront.developers.shoper.pl/cli/)
5
+ Learn more in the [commands docs](https://storefront.developers.shoper.pl/cli/)
6
+
7
+ ## Features
8
+
9
+ ### .shoperignore
10
+
11
+ Plik `.shoperignore` pozwala na wykluczenie określonych plików i katalogów z pakowania, uploadowania oraz weryfikacji checksum.
12
+
13
+ Używa tej samej składni co `.gitignore`. Plik jest automatycznie tworzony podczas inicjalizacji lub pobierania motywu.
14
+
15
+ Więcej informacji: [SHOPERIGNORE.md](docs/SHOPERIGNORE.md)
@@ -154,7 +154,8 @@ export class ThemeChecksums {
154
154
  join(SHOPER_THEME_METADATA_DIR, THEME_INITIAL_CHECKSUMS_FILE_NAME),
155
155
  join(SHOPER_THEME_METADATA_DIR, THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME)
156
156
  ];
157
- const filesToComputeChecksums = (await ThemePushUtils.getAllFilesThatAreSendToRemote(this.#themeDir))
157
+ const allFiles = await ThemePushUtils.getAllFilesThatAreSendToRemote(this.#themeDir);
158
+ const filesToComputeChecksums = allFiles
158
159
  .filter((path) => !filesToIgnoreInChecksums.some((ignorePath) => normalize(path) === ignorePath))
159
160
  .map((relativePath) => join(this.#themeDir, relativePath));
160
161
  this.#loggerApi?.debug('Computing checksums from file structure.', { details: { count: filesToComputeChecksums.length } });
@@ -25,8 +25,6 @@ import { ThemeError } from '../ui/theme_error.js';
25
25
  import { mapToPermissionsTree } from '../../utils/directory_validator/directory_validator_utils.js';
26
26
  import { mapChecksumToTree } from '../../../utils/checksums/checksums_utils.js';
27
27
  import { ThemeUnpermittedActionsError } from './ui/theme_unpermitted_actions_error.js';
28
- import { validateThemeFolderStructure } from '../../utils/push_validators/push_path_validator_utils.js';
29
- import { ThemeInvalidFoldersError } from './ui/theme_invalid_folders_error.js';
30
28
  import { LOGGER_API_NAME } from '../../../cli/utilities/features/logger/logger_constants.js';
31
29
  export class ThemePushCommand extends BaseThemeCommand {
32
30
  static summary = 'Uploads your local theme files to the store and overwrites the current version of the theme in your store.';
@@ -88,11 +86,8 @@ export class ThemePushCommand extends BaseThemeCommand {
88
86
  permissions: mapToPermissionsTree(permissions),
89
87
  rootDirectory: executionContext.themeRootDir
90
88
  });
91
- const folderStructureValidation = await validateThemeFolderStructure(executionContext.themeRootDir);
92
- if (!folderStructureValidation.isValid) {
93
- renderOnce(React.createElement(ThemeInvalidFoldersError, { invalidDirectories: folderStructureValidation.invalidDirectories }));
94
- return;
95
- }
89
+ //TODO jak wysyla folder z nazwa posiadajaco \ na windows, wychodzimy
90
+ //TODO validacja folderów przed pushem
96
91
  if (!validationResult.isValid) {
97
92
  renderOnce(React.createElement(ThemeUnpermittedActionsError, { unpermittedActions: validationResult.unpermittedActions }));
98
93
  return;
@@ -5,6 +5,7 @@ import globs from 'fast-glob';
5
5
  import { THEME_PUSH_WILDCARD_GLOBS_FOR_FILES } from '../push/service/theme_push_service_constants.js';
6
6
  import micromatch from 'micromatch';
7
7
  import difference from 'lodash/difference.js';
8
+ import { filterFiles, loadShoperIgnore } from '../../../utils/shoperignore/shoperignore_utils.js';
8
9
  export class ThemeActionsUtils {
9
10
  static getFilesGlobsThatMatchesAction({ filesStructure, actionValue, actionType }) {
10
11
  return Object.entries(filesStructure).reduce((acc, [filePath, fileStructureItem]) => {
@@ -38,6 +39,7 @@ export class ThemeActionsUtils {
38
39
  static async getFilesRecordsFromActionData({ themeRootDir, themeAction, filesStructure, themeChecksums }) {
39
40
  const filesRecords = [];
40
41
  const initialChecksums = await themeChecksums.getInitialChecksums();
42
+ const ignoreInstance = await loadShoperIgnore(themeRootDir);
41
43
  for (const [actionKey, actionData] of Object.entries(themeAction.data)) {
42
44
  if (actionData.type === THEME_ACTION_DATA_TYPE.file) {
43
45
  const filesGlobs = ThemeActionsUtils.getFilesGlobsThatMatchesAction({
@@ -54,14 +56,15 @@ export class ThemeActionsUtils {
54
56
  onlyFiles: true,
55
57
  cwd: themeRootDir
56
58
  });
59
+ const filteredFiles = await filterFiles(files, themeRootDir, ignoreInstance);
57
60
  let processedFileGlob = fileGlob;
58
61
  if (looksLikeDirectory(fileGlob)) {
59
62
  processedFileGlob = fileGlob.endsWith(THEME_PUSH_WILDCARD_GLOBS_FOR_FILES)
60
63
  ? fileGlob.slice(0, fileGlob.length - 2)
61
64
  : fileGlob;
62
65
  }
63
- const deletedFiles = difference(checksumMatchedFiles, files);
64
- for (const filePath of [...files, ...deletedFiles]) {
66
+ const deletedFiles = difference(checksumMatchedFiles, filteredFiles);
67
+ for (const filePath of [...filteredFiles, ...deletedFiles]) {
65
68
  filesRecords.push({
66
69
  actionData,
67
70
  actionKey,
@@ -66,6 +66,8 @@ export class ThemeFetchService {
66
66
  }
67
67
  this.#loggerApi.debug('Creating .gitignore file.');
68
68
  await ThemeMetaDataUtils.createGitIgnoreFile(themeDir);
69
+ this.#loggerApi.debug('Creating .shoperignore file.');
70
+ await ThemeMetaDataUtils.createShoperIgnoreFile(themeDir);
69
71
  this.#loggerApi.debug('Updating metadata file.');
70
72
  await ThemeMetaDataUtils.updateMetadataFileWithWorkUrl(themeDir, credentials.shopUrl);
71
73
  this.#loggerApi.debug('Updating theme checksums');
@@ -52,6 +52,8 @@ export class ThemeInitService {
52
52
  }
53
53
  this.#loggerApi.debug('Creating .gitignore file.');
54
54
  await ThemeMetaDataUtils.createGitIgnoreFile(themeDir);
55
+ this.#loggerApi.debug('Creating .shoperignore file.');
56
+ await ThemeMetaDataUtils.createShoperIgnoreFile(themeDir);
55
57
  this.#loggerApi.debug('Updating metadata file.');
56
58
  await ThemeMetaDataUtils.updateMetadataFileWithWorkUrl(themeDir, credentials.shopUrl);
57
59
  this.#loggerApi.debug('Updating theme checksums.');
@@ -3,6 +3,7 @@ import { AppError } from '../../../../cli/utilities/features/logger/logs/app_err
3
3
  import { toUnixPath } from '../../../../utils/path_utils.js';
4
4
  import { ThemeFilesUtils } from '../utils/files/theme_files_utils.js';
5
5
  import { THEME_PUSH_WILDCARD_GLOBS_FOR_FILES } from './service/theme_push_service_constants.js';
6
+ import { filterFiles } from '../../../utils/shoperignore/shoperignore_utils.js';
6
7
  export class ThemePushUtils {
7
8
  static async getAllFilesThatAreSendToRemote(themeDir) {
8
9
  const filesStructure = await ThemeFilesUtils.getThemeFilesStructure(themeDir);
@@ -17,11 +18,13 @@ export class ThemePushUtils {
17
18
  .filter((path) => path !== '*');
18
19
  //need unix styles globs
19
20
  filesToArchive.push('styles/src/**/*');
20
- return await globs(filesToArchive, {
21
- suppressErrors: true,
21
+ const allFiles = await globs(filesToArchive, {
22
22
  onlyFiles: true,
23
- cwd: themeDir
24
- }).then((files) => files.sort());
23
+ cwd: themeDir,
24
+ suppressErrors: true
25
+ });
26
+ const filteredFiles = await filterFiles(allFiles, themeDir);
27
+ return filteredFiles.sort();
25
28
  }
26
29
  static _normalizeFileGlob(fileGlob) {
27
30
  if (fileGlob.endsWith('/')) {
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import { THEME_FILES_LIST_FILE_NAME } from '../../push/theme_push_constants.js';
9
9
  import { THEME_ACTIONS_TYPES } from '../../actions/theme_actions_constants.js';
10
10
  import { FILE_STATES } from '../../../../../utils/fs/fs_constants.js';
11
+ import { loadShoperIgnore } from '../../../../utils/shoperignore/shoperignore_utils.js';
11
12
  export class ThemeFilesUtils {
12
13
  static async getThemeFilesStructure(themeDirectory) {
13
14
  const filesStructure = await readJSONFile(join(themeDirectory, SHOPER_THEME_METADATA_DIR, THEME_FILES_STRUCTURE_FILE_NAME));
@@ -29,10 +30,12 @@ export class ThemeFilesUtils {
29
30
  }, {});
30
31
  }
31
32
  static async validateThemeDirectoryStructure({ checksums, permissions, rootDirectory }) {
33
+ const ignoreInstance = await loadShoperIgnore(rootDirectory);
32
34
  return await validateDirectory({
33
35
  permissions,
34
36
  checksums,
35
- rootDirectory
37
+ rootDirectory,
38
+ ignoreInstance
36
39
  });
37
40
  }
38
41
  static async getThemeRootDirectories(themeDirectory) {
@@ -117,7 +120,7 @@ export class ThemeFilesUtils {
117
120
  },
118
121
  _links: { [THEME_ACTIONS_TYPES.push]: '*' }
119
122
  };
120
- ['.gitignore'].forEach((fileName) => {
123
+ ['.gitignore', '.shoperignore'].forEach((fileName) => {
121
124
  fileStructure[fileName] = {
122
125
  permissions: {
123
126
  canAdd: true,
@@ -58,4 +58,32 @@ export class ThemeMetaDataUtils {
58
58
  });
59
59
  });
60
60
  }
61
+ static async createShoperIgnoreFile(themeDir) {
62
+ return new Promise((resolve, reject) => {
63
+ const shoperIgnoreContent = `# .shoperignore
64
+ # Pliki i katalogi wymienione tutaj będą wykluczane z:
65
+ # - pakowania do archiwum podczas publikacji
66
+ # - uploadowania na serwer
67
+ # - weryfikacji checksum
68
+
69
+ # Przykłady:
70
+ # node_modules/
71
+ # .env
72
+ # .env.*
73
+ # *.log
74
+ # temp/
75
+ # .vscode/
76
+ # .idea/
77
+ `;
78
+ const writeStream = createWriteStream(join(themeDir, '.shoperignore'));
79
+ writeStream.write(shoperIgnoreContent);
80
+ writeStream.end();
81
+ writeStream.on('error', (err) => {
82
+ reject(err);
83
+ });
84
+ writeStream.on('finish', () => {
85
+ resolve();
86
+ });
87
+ });
88
+ }
61
89
  }
@@ -3,5 +3,6 @@ export const THEME_COMMANDS_THAT_REQUIRED_ACTIONS_LIST = [
3
3
  THEME_COMMANDS_NAME.pull,
4
4
  THEME_COMMANDS_NAME.init,
5
5
  THEME_COMMANDS_NAME.push,
6
- THEME_COMMANDS_NAME.delete
6
+ THEME_COMMANDS_NAME.delete,
7
+ THEME_COMMANDS_NAME.watch
7
8
  ];
@@ -5,7 +5,7 @@ import { computeFileChecksum } from '../../../utils/checksums/checksums_utils.js
5
5
  import { DEFAULT_PERMISSION_FOR_ALL_FILES_KEY, DEFAULT_PERMISSION_FOR_DIRECTORY_KEY, FILES_MODIFICATION_TYPES, PERMISSION_KEY } from './directory_validator_constants.js';
6
6
  import { CHECKSUM_KEY } from '../../../utils/checksums/checksums_utils_constants.js';
7
7
  import _ from 'lodash';
8
- export const validateDirectory = async ({ rootDirectory, permissions, checksums }) => {
8
+ export const validateDirectory = async ({ rootDirectory, permissions, checksums, ignoreInstance }) => {
9
9
  const unpermittedActions = [];
10
10
  await _checkPermissions({
11
11
  permissions,
@@ -14,14 +14,15 @@ export const validateDirectory = async ({ rootDirectory, permissions, checksums
14
14
  parts: [],
15
15
  checksumsPart: checksums,
16
16
  rootDirectory,
17
- unpermittedActions
17
+ unpermittedActions,
18
+ ignoreInstance
18
19
  });
19
20
  return {
20
21
  isValid: !unpermittedActions.length,
21
22
  unpermittedActions
22
23
  };
23
24
  };
24
- const _checkPermissions = async ({ permissionPart, permissionParts, parts = [], checksumsPart, rootDirectory, unpermittedActions, permissions }) => {
25
+ const _checkPermissions = async ({ permissionPart, permissionParts, parts = [], checksumsPart, rootDirectory, unpermittedActions, permissions, ignoreInstance }) => {
25
26
  if (!permissionPart)
26
27
  return;
27
28
  //TODO required files in a custom module directory
@@ -40,6 +41,11 @@ const _checkPermissions = async ({ permissionPart, permissionParts, parts = [],
40
41
  filesInsideCurrentDirectoryObject[path] = true;
41
42
  });
42
43
  for (const file of files) {
44
+ const relativePath = join(parentPath, file);
45
+ // Skip files that are in .shoperignore
46
+ if (ignoreInstance && ignoreInstance.ignores(relativePath)) {
47
+ continue;
48
+ }
43
49
  const { permission, permissionKey } = _getPermission({
44
50
  path: file,
45
51
  fullPath: join(fullPath, file),
@@ -65,7 +71,8 @@ const _checkPermissions = async ({ permissionPart, permissionParts, parts = [],
65
71
  checksumsPart: checksumsPart?.[file] ?? {},
66
72
  rootDirectory,
67
73
  unpermittedActions,
68
- permissions
74
+ permissions,
75
+ ignoreInstance
69
76
  });
70
77
  }
71
78
  }
@@ -0,0 +1,37 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from '../../../utils/path_utils.js';
3
+ import { fileExists } from '../../../utils/fs/fs_utils.js';
4
+ import ignoreModule from 'ignore';
5
+ const ignore = ignoreModule;
6
+ export const SHOPER_IGNORE_FILE_NAME = '.shoperignore';
7
+ export async function loadShoperIgnore(themeRootDir) {
8
+ const shoperIgnorePath = join(themeRootDir, SHOPER_IGNORE_FILE_NAME);
9
+ if (!(await fileExists(shoperIgnorePath))) {
10
+ return null;
11
+ }
12
+ try {
13
+ const content = await readFile(shoperIgnorePath, 'utf-8');
14
+ const ig = ignore();
15
+ ig.add('.shoperignore');
16
+ ig.add('.gitignore');
17
+ ig.add(content);
18
+ return ig;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export async function filterFiles(files, themeRootDir, ignoreInstance) {
25
+ const ig = ignoreInstance !== undefined ? ignoreInstance : await loadShoperIgnore(themeRootDir);
26
+ if (!ig) {
27
+ return files;
28
+ }
29
+ return files.filter((file) => !ig.ignores(file));
30
+ }
31
+ export async function isIgnored(filePath, themeRootDir, ignoreInstance) {
32
+ const ig = ignoreInstance !== undefined ? ignoreInstance : await loadShoperIgnore(themeRootDir);
33
+ if (!ig) {
34
+ return false;
35
+ }
36
+ return ig.ignores(filePath);
37
+ }
@@ -5,7 +5,7 @@ import { createWriteStream } from 'node:fs';
5
5
  import { StreamReadError } from '../fs/errors/stream_read_error.js';
6
6
  import { CreateZipError } from './errors/create_zip_error.js';
7
7
  import { StreamWriteError } from '../fs/errors/stream_write_error.js';
8
- import { join, toUnixPath } from '../path_utils.js';
8
+ import { join } from '../path_utils.js';
9
9
  import { AppError } from '../../cli/utilities/features/logger/logs/app_error.js';
10
10
  export const createZip = async ({ files, dist, baseDir = process.cwd(), logger }) => {
11
11
  const zipfile = new yazl.ZipFile();
@@ -29,14 +29,14 @@ export const createZip = async ({ files, dist, baseDir = process.cwd(), logger }
29
29
  code: 'FILE_NOT_FOUND',
30
30
  details: { file, baseDir }
31
31
  });
32
- const zipEntryName = toUnixPath(file);
33
32
  if (await isDirectory(fullPath)) {
34
- logger.debug('Adding empty directory to zip', { details: { directory: zipEntryName } });
35
- zipfile.addEmptyDirectory(zipEntryName);
33
+ logger.debug('Adding empty directory to zip', { details: { directory: file } });
34
+ zipfile.addEmptyDirectory(file);
36
35
  }
37
36
  else {
38
- logger.debug('Adding file to zip', { details: { file: zipEntryName, fullPath } });
39
- zipfile.addFile(fullPath, zipEntryName, {
37
+ logger.debug('Adding file to zip', { details: { file, fullPath } });
38
+ zipfile.addFile(fullPath, file, {
39
+ //TODO params
40
40
  compress: true
41
41
  });
42
42
  }
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.8.3-1",
5
+ "version": "0.9.1-0",
6
6
  "description": "CLI tool for Shoper",
7
7
  "author": "Joanna Firek",
8
8
  "license": "MIT",
@@ -48,9 +48,11 @@
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",
@@ -77,7 +79,6 @@
77
79
  "micromatch": "4.0.8",
78
80
  "walk-sync": "3.0.0",
79
81
  "yauzl": "3.2.0",
80
- "figlet": "1.9.4",
81
82
  "yazl": "3.3.1",
82
83
  "puppeteer": "24.31.0",
83
84
  "gradient-string": "3.0.0"
@@ -1,16 +0,0 @@
1
- import { List } from '../../../../ui/list/list.js';
2
- import { Error } from '../../../../ui/message_box/error.js';
3
- import { Text } from '../../../../ui/text.js';
4
- import React from 'react';
5
- import { ALLOWED_THEME_TOP_LEVEL_DIRECTORIES } from '../../../utils/push_validators/push_validator_constants.js';
6
- export const ThemeInvalidFoldersError = ({ invalidDirectories }) => {
7
- const items = invalidDirectories.map((dir) => ({
8
- content: dir
9
- }));
10
- return (React.createElement(Error, { header: "Invalid theme folder structure" },
11
- React.createElement(Text, null, "The following directories are not allowed in the theme root:"),
12
- React.createElement(List, { items: items }),
13
- React.createElement(Text, null,
14
- "Allowed top-level directories: ",
15
- ALLOWED_THEME_TOP_LEVEL_DIRECTORIES.join(', '))));
16
- };
@@ -1,16 +0,0 @@
1
- import { getAllDirectoriesNamesInside } from '../../../utils/fs/fs_utils.js';
2
- import { ALLOWED_THEME_TOP_LEVEL_DIRECTORIES } from './push_validator_constants.js';
3
- export const validateThemeFolderStructure = async (themeRootDir) => {
4
- const directories = await getAllDirectoriesNamesInside(themeRootDir, { recursive: false, hidden: true });
5
- const allowedSet = new Set(ALLOWED_THEME_TOP_LEVEL_DIRECTORIES);
6
- const invalidDirectories = directories.filter((dir) => {
7
- if (dir.startsWith('.') && !allowedSet.has(dir)) {
8
- return false;
9
- }
10
- return !allowedSet.has(dir);
11
- });
12
- return {
13
- isValid: invalidDirectories.length === 0,
14
- invalidDirectories
15
- };
16
- };
@@ -1,8 +0,0 @@
1
- export const ALLOWED_THEME_TOP_LEVEL_DIRECTORIES = [
2
- '.shoper',
3
- 'settings',
4
- 'styles',
5
- 'macros',
6
- 'modules',
7
- 'skinstore'
8
- ];