shoplazza-cli 0.0.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.
Files changed (181) hide show
  1. package/.editorconfig +28 -0
  2. package/.prettierrc +9 -0
  3. package/LICENSE +21 -0
  4. package/README.md +208 -0
  5. package/bin/shoplazza +117 -0
  6. package/fixtures/assets/blog.scss +74 -0
  7. package/fixtures/assets/cart_modal.scss +450 -0
  8. package/fixtures/assets/collection_detail.js +234 -0
  9. package/fixtures/assets/collection_detail.scss +345 -0
  10. package/fixtures/assets/collection_list.scss +11 -0
  11. package/fixtures/assets/collection_slider.scss +169 -0
  12. package/fixtures/assets/feature_columns.scss +26 -0
  13. package/fixtures/assets/feature_product.scss +109 -0
  14. package/fixtures/assets/footer.js +58 -0
  15. package/fixtures/assets/footer.scss +337 -0
  16. package/fixtures/assets/four_images.scss +29 -0
  17. package/fixtures/assets/gallery.scss +55 -0
  18. package/fixtures/assets/header.js +178 -0
  19. package/fixtures/assets/header.scss +929 -0
  20. package/fixtures/assets/image_text.scss +72 -0
  21. package/fixtures/assets/logo_bar.scss +11 -0
  22. package/fixtures/assets/newsletter.scss +90 -0
  23. package/fixtures/assets/not_found.scss +39 -0
  24. package/fixtures/assets/page_detail.scss +16 -0
  25. package/fixtures/assets/pagination.scss +150 -0
  26. package/fixtures/assets/postcss.config.js +6 -0
  27. package/fixtures/assets/product_description.scss +88 -0
  28. package/fixtures/assets/product_detail.js +634 -0
  29. package/fixtures/assets/product_detail.scss +1106 -0
  30. package/fixtures/assets/relative_product.scss +45 -0
  31. package/fixtures/assets/reviews.scss +70 -0
  32. package/fixtures/assets/rich_text.scss +71 -0
  33. package/fixtures/assets/search.js +87 -0
  34. package/fixtures/assets/search.scss +67 -0
  35. package/fixtures/assets/slide.scss +51 -0
  36. package/fixtures/assets/slider.scss +141 -0
  37. package/fixtures/assets/theme.css +976 -0
  38. package/fixtures/assets/theme.scss +1100 -0
  39. package/fixtures/assets/three_images.scss +20 -0
  40. package/fixtures/assets/tools.scss +23 -0
  41. package/fixtures/assets/two_images.scss +24 -0
  42. package/fixtures/assets/video.scss +45 -0
  43. package/fixtures/assets/video_text.scss +63 -0
  44. package/fixtures/config/settings_data.json +107 -0
  45. package/fixtures/config/settings_schema.json +690 -0
  46. package/fixtures/layout/theme.liquid +76 -0
  47. package/fixtures/locales/ar-SA.json +212 -0
  48. package/fixtures/locales/de-DE.json +290 -0
  49. package/fixtures/locales/en-US.json +290 -0
  50. package/fixtures/locales/es-ES.json +290 -0
  51. package/fixtures/locales/fr-FR.json +290 -0
  52. package/fixtures/locales/id-ID.json +212 -0
  53. package/fixtures/locales/it-IT.json +212 -0
  54. package/fixtures/locales/ja-JP.json +289 -0
  55. package/fixtures/locales/ko-KR.json +290 -0
  56. package/fixtures/locales/nl-NL.json +290 -0
  57. package/fixtures/locales/pl-PL.json +290 -0
  58. package/fixtures/locales/pt-PT.json +212 -0
  59. package/fixtures/locales/ru-RU.json +212 -0
  60. package/fixtures/locales/th-TH.json +212 -0
  61. package/fixtures/locales/zh-CN.json +290 -0
  62. package/fixtures/locales/zh-TW.json +290 -0
  63. package/fixtures/sections/apps.liquid +47 -0
  64. package/fixtures/sections/blog.liquid +137 -0
  65. package/fixtures/sections/collection_desc.liquid +34 -0
  66. package/fixtures/sections/collection_detail.liquid +436 -0
  67. package/fixtures/sections/collection_image.liquid +104 -0
  68. package/fixtures/sections/collection_list.liquid +161 -0
  69. package/fixtures/sections/collection_name.liquid +34 -0
  70. package/fixtures/sections/collection_slider.liquid +330 -0
  71. package/fixtures/sections/feature_columns.liquid +275 -0
  72. package/fixtures/sections/feature_product.liquid +227 -0
  73. package/fixtures/sections/footer.liquid +488 -0
  74. package/fixtures/sections/four_images.liquid +160 -0
  75. package/fixtures/sections/gallery.liquid +258 -0
  76. package/fixtures/sections/header.liquid +1157 -0
  77. package/fixtures/sections/html.liquid +40 -0
  78. package/fixtures/sections/image_text.liquid +350 -0
  79. package/fixtures/sections/instagram_plus.liquid +393 -0
  80. package/fixtures/sections/logo_bar.liquid +183 -0
  81. package/fixtures/sections/newsletter.liquid +225 -0
  82. package/fixtures/sections/not_found.liquid +39 -0
  83. package/fixtures/sections/overlay_image.liquid +648 -0
  84. package/fixtures/sections/page_detail.liquid +39 -0
  85. package/fixtures/sections/photo_collection.liquid +433 -0
  86. package/fixtures/sections/product_description.liquid +208 -0
  87. package/fixtures/sections/product_detail.liquid +611 -0
  88. package/fixtures/sections/products.liquid +216 -0
  89. package/fixtures/sections/relative_product.liquid +121 -0
  90. package/fixtures/sections/reviews.liquid +115 -0
  91. package/fixtures/sections/rich_text.liquid +157 -0
  92. package/fixtures/sections/search.liquid +163 -0
  93. package/fixtures/sections/slide.liquid +719 -0
  94. package/fixtures/sections/three_images.liquid +157 -0
  95. package/fixtures/sections/two_images.liquid +125 -0
  96. package/fixtures/sections/video.liquid +95 -0
  97. package/fixtures/sections/video_text.liquid +128 -0
  98. package/fixtures/snippets/bgset.liquid +21 -0
  99. package/fixtures/snippets/card_title.liquid +8 -0
  100. package/fixtures/snippets/cart_modal.liquid +74 -0
  101. package/fixtures/snippets/collection.liquid +77 -0
  102. package/fixtures/snippets/collection_filter_modal.liquid +56 -0
  103. package/fixtures/snippets/default_image_4.liquid +14 -0
  104. package/fixtures/snippets/default_image_6.liquid +18 -0
  105. package/fixtures/snippets/default_image_8.liquid +23 -0
  106. package/fixtures/snippets/four_images_item.liquid +8 -0
  107. package/fixtures/snippets/header_ads.liquid +95 -0
  108. package/fixtures/snippets/hero_image.liquid +94 -0
  109. package/fixtures/snippets/icon_video_play_large.liquid +1 -0
  110. package/fixtures/snippets/icon_video_play_medium.liquid +4 -0
  111. package/fixtures/snippets/icon_video_play_small.liquid +4 -0
  112. package/fixtures/snippets/lazyimg.liquid +22 -0
  113. package/fixtures/snippets/lazyimg_art.liquid +36 -0
  114. package/fixtures/snippets/lazysizes.liquid +41 -0
  115. package/fixtures/snippets/link.liquid +2 -0
  116. package/fixtures/snippets/pagination.liquid +48 -0
  117. package/fixtures/snippets/product.liquid +126 -0
  118. package/fixtures/snippets/product_art_tpl.liquid +152 -0
  119. package/fixtures/snippets/product_info_body.liquid +337 -0
  120. package/fixtures/snippets/product_info_tpl.liquid +423 -0
  121. package/fixtures/snippets/product_label.liquid +46 -0
  122. package/fixtures/snippets/settings.liquid +295 -0
  123. package/fixtures/snippets/social-meta-tags.liquid +106 -0
  124. package/fixtures/snippets/video_html.liquid +11 -0
  125. package/fixtures/snippets/video_source.liquid +98 -0
  126. package/fixtures/snippets/video_thumb_icon.liquid +2 -0
  127. package/fixtures/templates/404.liquid +1 -0
  128. package/fixtures/templates/collection.liquid +92 -0
  129. package/fixtures/templates/index.liquid +206 -0
  130. package/fixtures/templates/page.liquid +1 -0
  131. package/fixtures/templates/product.liquid +99 -0
  132. package/fixtures/templates/search.liquid +1 -0
  133. package/jest.config.js +192 -0
  134. package/lib/__tests__/log.test.js +15 -0
  135. package/lib/__tests__/utils.test.js +69 -0
  136. package/lib/auth/__mocks__/getCode.js +7 -0
  137. package/lib/auth/__mocks__/index.js +0 -0
  138. package/lib/auth/child.js +23 -0
  139. package/lib/auth/getCode.js +35 -0
  140. package/lib/auth/index.js +91 -0
  141. package/lib/commands/__tests__/login.test.js +77 -0
  142. package/lib/commands/__tests__/logout.test.js +29 -0
  143. package/lib/commands/__tests__/store.test.js +44 -0
  144. package/lib/commands/__tests__/switch.test.js +45 -0
  145. package/lib/commands/login.js +99 -0
  146. package/lib/commands/logout.js +14 -0
  147. package/lib/commands/store.js +14 -0
  148. package/lib/commands/switch.js +52 -0
  149. package/lib/commands/theme/__tests__/delete.test.js +49 -0
  150. package/lib/commands/theme/__tests__/init.test.js +21 -0
  151. package/lib/commands/theme/__tests__/list.test.js +80 -0
  152. package/lib/commands/theme/__tests__/package.test.js +17 -0
  153. package/lib/commands/theme/__tests__/publish.test.js +61 -0
  154. package/lib/commands/theme/__tests__/pull.test.js +69 -0
  155. package/lib/commands/theme/__tests__/push.test.js +63 -0
  156. package/lib/commands/theme/__tests__/serve.test.js +107 -0
  157. package/lib/commands/theme/delete.js +64 -0
  158. package/lib/commands/theme/init.js +51 -0
  159. package/lib/commands/theme/list.js +28 -0
  160. package/lib/commands/theme/package.js +37 -0
  161. package/lib/commands/theme/publish.js +56 -0
  162. package/lib/commands/theme/pull.js +62 -0
  163. package/lib/commands/theme/push.js +106 -0
  164. package/lib/commands/theme/serve.js +153 -0
  165. package/lib/commands/theme/share.js +20 -0
  166. package/lib/commands/version.js +6 -0
  167. package/lib/config.js +5 -0
  168. package/lib/db/__mocks__/index.js +9 -0
  169. package/lib/db/__tests__/analytics.test.js +19 -0
  170. package/lib/db/__tests__/user.test.js +20 -0
  171. package/lib/db/analytics.js +48 -0
  172. package/lib/db/index.js +9 -0
  173. package/lib/db/user.js +68 -0
  174. package/lib/log.js +13 -0
  175. package/lib/openAPI/__mocks__/index.js +20 -0
  176. package/lib/openAPI/api.js +76 -0
  177. package/lib/openAPI/index.js +46 -0
  178. package/lib/report.js +37 -0
  179. package/lib/tracing.js +50 -0
  180. package/lib/utils.js +48 -0
  181. package/package.json +54 -0
@@ -0,0 +1,106 @@
1
+ const ora = require('ora');
2
+ const chalk = require('chalk');
3
+ const inquirer = require('inquirer');
4
+ const Sentry = require('@sentry/node');
5
+ const fs = require('fs-extra');
6
+ const { get } = require('../../db/user');
7
+ const { getThemeDetail, getThemes, getDefaultThemeDetail, pushTheme, getPushTask } = require('../../openAPI/api');
8
+ const { sleep, formatThemeList } = require('../../utils');
9
+ const { checkAndZipThemes } = require('./package');
10
+ const log = require('../../log');
11
+
12
+ const checkPushTask = (taskId) => {
13
+ let maxCheckCount = 60;
14
+ let state = -1;
15
+ return new Promise(async (resolve, reject) => {
16
+ while (maxCheckCount--) {
17
+ const {
18
+ data: {
19
+ task: { status, info, message }
20
+ }
21
+ } = await getPushTask(taskId);
22
+ status == 0 && (await sleep(2000));
23
+ if (status == 1 || status == 2) {
24
+ state = status;
25
+ state == 1 && resolve(info);
26
+ state == 2 && reject(`Status Detection failure (2), Error message: ${message}`);
27
+ break;
28
+ }
29
+ }
30
+ state == -1 && reject('Retry timeout');
31
+ }).catch((error) => {
32
+ log.error(chalk.red(`\n✗ Failed to push theme`, error));
33
+ Sentry.captureException(error);
34
+ process.exit(-1);
35
+ });
36
+ };
37
+
38
+ const pushThemeFiles = async (theme) => {
39
+ let merchant_theme_id = '';
40
+ let name = '';
41
+ if (theme) {
42
+ const { data } = await getThemeDetail(theme);
43
+ if (!data) {
44
+ log.error(chalk.red(`✗ Theme ${theme} does not exist`));
45
+ process.exit(-1);
46
+ }
47
+ name = data.data.name;
48
+ merchant_theme_id = data.data.merchant_theme_id;
49
+ }
50
+
51
+ const { theme_name, theme_version = '', zipPath } = checkAndZipThemes();
52
+ const {
53
+ data: {
54
+ task: {
55
+ task: { id }
56
+ }
57
+ }
58
+ } = await pushTheme({
59
+ name: name || theme_name,
60
+ version: theme_version,
61
+ merchant_theme_id,
62
+ theme_id: theme || '',
63
+ zipPath
64
+ });
65
+ fs.rmSync(zipPath);
66
+ return checkPushTask(id);
67
+ };
68
+
69
+ const execPushTheme = async (theme) => {
70
+ const storeDomain = get('store_domain');
71
+ const spinner = ora(chalk.cyan(`Pushing theme files on ${storeDomain}`)).start();
72
+ const { name } = JSON.parse(await pushThemeFiles(theme));
73
+ spinner.stop();
74
+ log.info(chalk.green(`✓ The ${name} theme pushed successfully`));
75
+ };
76
+
77
+ const pushCommand = async (options) => {
78
+ try {
79
+ if (options.theme) {
80
+ await execPushTheme(options.theme);
81
+ } else {
82
+ const storeDomain = get('store_domain');
83
+ const spinner = ora(chalk.cyan(`Fetching theme lists from ${storeDomain}`)).start();
84
+ const [themesRes, defaultThemeRes] = await Promise.all([getThemes(), getDefaultThemeDetail()]);
85
+ spinner.stop();
86
+ const answers = await inquirer.prompt([
87
+ {
88
+ name: 'theme',
89
+ type: 'rawlist',
90
+ message: `Select a theme to push ${chalk.cyan('(Choose with ↑ ↓ ⏎)')}`,
91
+ choices: [
92
+ { name: ' Create a new unpublished theme', value: '' },
93
+ ...formatThemeList([defaultThemeRes.data.data, ...themesRes.data.data.themes], defaultThemeRes.data.data.id)
94
+ ]
95
+ }
96
+ ]);
97
+ await execPushTheme(answers.theme);
98
+ }
99
+ } catch (error) {
100
+ log.error(chalk.red(`✗ Failed to push theme`));
101
+ Sentry.captureException(error);
102
+ }
103
+ };
104
+
105
+ exports.pushCommand = pushCommand;
106
+ exports.pushThemeFiles = pushThemeFiles;
@@ -0,0 +1,153 @@
1
+ const path = require('path');
2
+ const os = require('os');
3
+ const fs = require('fs-extra');
4
+ const ora = require('ora');
5
+ const chalk = require('chalk');
6
+ const inquirer = require('inquirer');
7
+ const Sentry = require('@sentry/node');
8
+ const livereload = require('livereload');
9
+ const watch = require('node-watch');
10
+ const { get, set } = require('../../db/user');
11
+ const { pushThemeFiles } = require('./push');
12
+ const { getThemes, getDefaultThemeDetail, deleteFile, updateFile, addFile, getFileList } = require('../../openAPI/api');
13
+ const log = require('../../log');
14
+ const { THEME_DIRS } = require('../../config');
15
+ const { getThemeFilenameTypeAndLocation, formatThemeList } = require('../../utils');
16
+
17
+ const existInList = (filename, fileList) => {
18
+ const { type, location } = getThemeFilenameTypeAndLocation(filename);
19
+ return fileList[type] && fileList[type].find((item) => item.location == location);
20
+ };
21
+ const deleteInList = (filename, fileList) => {
22
+ const { type, location } = getThemeFilenameTypeAndLocation(filename);
23
+ fileList[type] = fileList[type].filter((i) => i.location !== location);
24
+ };
25
+ const addInList = (filename, fileList) => {
26
+ const { type, location } = getThemeFilenameTypeAndLocation(filename);
27
+ fileList[type].push({ location });
28
+ };
29
+
30
+ const startDevServer = async (theme) => {
31
+ const { data: fileList } = await getFileList(theme);
32
+ const server = livereload.createServer({ port: '21647', applyCSSLive: false, applyImgLive: false });
33
+ const watcher = watch(process.cwd(), {
34
+ recursive: true,
35
+ filter: (f, skip) => {
36
+ const { type } = getThemeFilenameTypeAndLocation(f);
37
+ return THEME_DIRS.includes(type);
38
+ }
39
+ });
40
+ return new Promise((resolve, reject) => {
41
+ watcher
42
+ .on('change', async (evt, filename) => {
43
+ try {
44
+ log.info(chalk.green(`[${evt}]: ${filename}`));
45
+ if (
46
+ fs.existsSync(path.join(process.cwd(), filename)) &&
47
+ fs.lstatSync(path.join(process.cwd(), filename)).isDirectory()
48
+ )
49
+ return; // windows change return directory always
50
+ if (evt == 'remove') {
51
+ await deleteFile(theme, filename);
52
+ deleteInList(filename, fileList);
53
+ } else if (!/\/\.\w+/.test(filename)) {
54
+ // path not contain .xxx hidden file/folder
55
+ if (existInList(filename, fileList)) {
56
+ await updateFile(theme, filename);
57
+ } else {
58
+ await addFile(theme, filename);
59
+ addInList(filename, fileList);
60
+ if (fs.readFileSync(filename, 'utf-8')) {
61
+ // addFile api not support post with content
62
+ await updateFile(theme, filename);
63
+ }
64
+ }
65
+ }
66
+ server.refresh();
67
+ log.info(chalk.cyan(`Updated, please refresh your browser, will continue listening for file changes ...`));
68
+ } catch (err) {
69
+ log.error(chalk.red(`✗ ${err.message || 'Unknown error'}`));
70
+ }
71
+ })
72
+ .on('error', (err) => {
73
+ reject(err);
74
+ log.error(chalk.red(`✗ Error `, err));
75
+ Sentry.captureException(err);
76
+ })
77
+ .on('ready', () => {
78
+ resolve(watcher);
79
+ const storeDomain = get('store_domain');
80
+ const url = new URL(`https://${storeDomain}`);
81
+ url.searchParams.set('preview_theme_id', theme);
82
+ log.info(
83
+ `Please open this URL in your browser:
84
+ ${chalk.green(url.href)}
85
+
86
+ Customize this theme in the Theme Editor, and use 'theme pull' to get the changes:
87
+ ${chalk.green(`https://${storeDomain}/admin/smart_apps/editor?theme_id=${theme}`)}`.replace(/^[^\S\n]+/gm, '')
88
+ );
89
+ log.info(`\nListening for file changes ...`);
90
+ });
91
+ });
92
+ };
93
+
94
+ module.exports = async (options) => {
95
+ try {
96
+ const storeDomain = get('store_domain');
97
+ if (options.theme) {
98
+ const spinner = ora(chalk.cyan(`Syncing theme files on ${storeDomain}`)).start();
99
+ const { name } = await pushThemeFiles(options.theme);
100
+ set({
101
+ theme_id: options.theme,
102
+ theme_name: `${name} (${options.theme})`
103
+ });
104
+ spinner.stop();
105
+ return startDevServer(options.theme);
106
+ } else {
107
+ const spinner = ora(chalk.cyan(`Fetching theme lists`)).start();
108
+ const [themesRes, defaultThemeRes] = await Promise.all([getThemes(), getDefaultThemeDetail()]);
109
+ spinner.stop();
110
+ const themeList = [defaultThemeRes.data.data, ...themesRes.data.data.themes];
111
+ const choices = [
112
+ { name: 'Create a new unpublished theme', value: '' },
113
+ ...formatThemeList(themeList, defaultThemeRes.data.data.id)
114
+ ];
115
+ const theme_id = get('theme_id');
116
+ const theme_name = get('theme_id');
117
+
118
+ if (theme_id && theme_name && themeList.find((theme) => theme.id == theme_id)) {
119
+ choices.unshift({ name: `Use previous selected ${theme_name}`, value: theme_id });
120
+ }
121
+
122
+ const answers = await inquirer.prompt([
123
+ {
124
+ name: 'theme',
125
+ type: 'rawlist',
126
+ message: `Select a theme to push ${chalk.cyan('(Choose with ↑ ↓ ⏎)')}`,
127
+ choices: choices
128
+ }
129
+ ]);
130
+
131
+ const syncSpinner = ora(chalk.cyan(`Syncing theme files on ${storeDomain}`)).start();
132
+ if (answers.theme) {
133
+ const themeObj = themeList.find((theme) => theme.id == answers.theme);
134
+ set({
135
+ theme_id: themeObj.id,
136
+ theme_name: themeObj.name
137
+ });
138
+ await pushThemeFiles(answers.theme);
139
+ } else {
140
+ const { name, theme_id } = JSON.parse(await pushThemeFiles(answers.theme));
141
+ set({
142
+ theme_id,
143
+ theme_name: name
144
+ });
145
+ }
146
+ syncSpinner.stop();
147
+ return startDevServer(get('theme_id'));
148
+ }
149
+ } catch (error) {
150
+ log.error(chalk.red(`✗ Failed to serve theme`));
151
+ Sentry.captureException(error);
152
+ }
153
+ };
@@ -0,0 +1,20 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const Sentry = require('@sentry/node');
4
+ const log = require('../../log');
5
+ const { getShopDetail } = require('../../openAPI/api');
6
+ const { pushThemeFiles } = require('./push');
7
+
8
+ module.exports = async () => {
9
+ try {
10
+ const { data } = await getShopDetail();
11
+ const spinner = ora(chalk.cyan(`Pushing theme files on ${data.shop.domain}`)).start();
12
+ const { name, theme_id } = JSON.parse(await pushThemeFiles());
13
+ spinner.stop();
14
+ log.info(chalk.green(`✓ The ${name} Theme pushed successfully`));
15
+ log.info(`Share your theme preview:\n${chalk.cyan(`https://${data.shop.domain}?preview_theme_id=${theme_id}`)}`);
16
+ } catch (error) {
17
+ log.error(chalk.red(`✗ Failed to share theme`));
18
+ Sentry.captureException(error);
19
+ }
20
+ };
@@ -0,0 +1,6 @@
1
+ const pkg = require('../../package.json');
2
+ const log = require('../log');
3
+
4
+ module.exports = () => {
5
+ log.info(pkg.version);
6
+ };
package/lib/config.js ADDED
@@ -0,0 +1,5 @@
1
+ exports.SSO_AUTH_URL = 'https://sso.shoplazza.com';
2
+ exports.ACCOUNT_URL = 'https://myaccount.shoplazza.com';
3
+ exports.CLIENT_ID = '807525c9-dd77-4f4e-a738-d1e9d02c593d';
4
+ exports.REDIRECT_URI = 'http://127.0.0.1:3456';
5
+ exports.THEME_DIRS = ['assets', 'config', 'layout', 'locales', 'sections', 'snippets', 'templates'];
@@ -0,0 +1,9 @@
1
+ const path = require('path');
2
+ const os = require('os');
3
+ const fs = require('fs-extra');
4
+
5
+ fs.ensureDirSync(path.join(os.homedir(), '/.tmp/shoplazza'));
6
+
7
+ const db = require('better-sqlite3')(path.join(os.homedir(), '/.tmp/shoplazza/.user.db'), {});
8
+
9
+ module.exports = db;
@@ -0,0 +1,19 @@
1
+ const { hasBeenSetAnalytics, isEnabledAnalytics, setAnalyticsConfig, emptyAnalyticsData } = require('../analytics');
2
+ jest.mock('../index');
3
+
4
+ describe('db/analytics.js', () => {
5
+ beforeEach(() => {
6
+ emptyAnalyticsData();
7
+ });
8
+
9
+ it('get and set', () => {
10
+ expect(hasBeenSetAnalytics('user_id')).toBe(false);
11
+ expect(isEnabledAnalytics('user_id')).toBe(false);
12
+ setAnalyticsConfig({
13
+ user_id: 'user_id',
14
+ enabled: 0
15
+ });
16
+ expect(hasBeenSetAnalytics('user_id')).toBe(true);
17
+ expect(isEnabledAnalytics('user_id')).toBe(false);
18
+ });
19
+ });
@@ -0,0 +1,20 @@
1
+ const { get, set, empty } = require('../user');
2
+ jest.mock('../index');
3
+
4
+ describe('db/user.js', () => {
5
+ beforeEach(() => {
6
+ empty();
7
+ });
8
+
9
+ it('get and set', () => {
10
+ expect(get('access_token')).toBe(null);
11
+ set({
12
+ access_token: 'access_token',
13
+ session_id: 'session_id',
14
+ exchange_token: 'exchange_token'
15
+ });
16
+ expect(get('access_token')).toBe('access_token');
17
+ expect(get('session_id')).toBe('session_id');
18
+ expect(get('exchange_token')).toBe('exchange_token');
19
+ });
20
+ });
@@ -0,0 +1,48 @@
1
+ const db = require('./index');
2
+
3
+ const createTableIfNeeded = () => {
4
+ const createTable = db.prepare(`create table if not exists analytics (
5
+ id integer primary key AUTOINCREMENT,
6
+ user_id text,
7
+ enabled integer
8
+ )`);
9
+ createTable.run();
10
+ };
11
+ createTableIfNeeded();
12
+
13
+ const getItem = (userId) => {
14
+ const stmt = db.prepare('select * from analytics where user_id = ?');
15
+ return stmt.get(userId);
16
+ };
17
+
18
+ const insertOrReplace = ({ user_id, enabled }) => {
19
+ const stmt = db.prepare(
20
+ `insert or replace into analytics (id, user_id, enabled) values (
21
+ (select id from analytics where user_id = @user_id),
22
+ @user_id,
23
+ @enabled
24
+ )`
25
+ );
26
+ stmt.run({ user_id, enabled });
27
+ };
28
+
29
+ const hasBeenSetAnalytics = (userId) => {
30
+ const item = getItem(userId);
31
+ return [0, 1].includes(item?.enabled);
32
+ };
33
+
34
+ const isEnabledAnalytics = (userId) => {
35
+ const item = getItem(userId);
36
+ return item?.enabled === 1;
37
+ };
38
+
39
+ const setAnalyticsConfig = (keyValueObj) => {
40
+ insertOrReplace(keyValueObj);
41
+ };
42
+
43
+ const emptyAnalyticsData = () => {
44
+ const stmt = db.prepare(`delete from analytics`);
45
+ stmt.run();
46
+ };
47
+
48
+ module.exports = { hasBeenSetAnalytics, isEnabledAnalytics, setAnalyticsConfig, emptyAnalyticsData };
@@ -0,0 +1,9 @@
1
+ const path = require('path');
2
+ const os = require('os');
3
+ const fs = require('fs-extra');
4
+
5
+ fs.ensureDirSync(path.join(os.homedir(), '/.cache/shoplazza'));
6
+
7
+ const db = require('better-sqlite3')(path.join(os.homedir(), '/.cache/shoplazza/.user.db'), {});
8
+
9
+ module.exports = db;
package/lib/db/user.js ADDED
@@ -0,0 +1,68 @@
1
+ const db = require('./index');
2
+
3
+ const createTableIfNeeded = () => {
4
+ const createTable = db.prepare(`create table if not exists user (
5
+ id integer primary key AUTOINCREMENT,
6
+ access_token text,
7
+ session_id text,
8
+ exchange_token text,
9
+ theme_id text,
10
+ theme_name text,
11
+ store_domain text,
12
+ user_id text
13
+ )`);
14
+ createTable.run();
15
+ };
16
+ createTableIfNeeded();
17
+
18
+ const getUserObj = () => {
19
+ const stmt = db.prepare('select * from user limit 0,1;');
20
+ return stmt.get();
21
+ };
22
+
23
+ const insertOrReplace = ({
24
+ access_token = get('access_token'),
25
+ session_id = get('session_id'),
26
+ exchange_token = get('exchange_token'),
27
+ theme_id = get('theme_id'),
28
+ theme_name = get('theme_name'),
29
+ store_domain = get('store_domain'),
30
+ user_id = get('user_id')
31
+ }) => {
32
+ const stmt = db.prepare(
33
+ `insert or replace into user (id, access_token, session_id, exchange_token, theme_id, theme_name, store_domain, user_id) values (
34
+ (select id from user where access_token = @access_token),
35
+ @access_token,
36
+ @session_id,
37
+ @exchange_token,
38
+ @theme_id,
39
+ @theme_name,
40
+ @store_domain,
41
+ @user_id
42
+ )`
43
+ );
44
+ stmt.run({ access_token, session_id, exchange_token, theme_id, theme_name, store_domain, user_id });
45
+ };
46
+
47
+ const emptyUserData = () => {
48
+ const stmt = db.prepare(`delete from user`);
49
+ stmt.run();
50
+ };
51
+
52
+ const get = (key) => {
53
+ const userObj = getUserObj();
54
+ if (userObj) {
55
+ return userObj[key];
56
+ }
57
+ return null;
58
+ };
59
+
60
+ const set = (keyValueObj) => {
61
+ insertOrReplace(keyValueObj);
62
+ };
63
+
64
+ const empty = () => {
65
+ emptyUserData();
66
+ };
67
+
68
+ module.exports = { get, set, empty };
package/lib/log.js ADDED
@@ -0,0 +1,13 @@
1
+ const report = require('./report');
2
+
3
+ module.exports = {
4
+ error: (...args) => {
5
+ console.log(...args);
6
+ report('cli_usage_error', {
7
+ args: args
8
+ });
9
+ },
10
+ info: (...args) => {
11
+ console.log(...args);
12
+ }
13
+ };
@@ -0,0 +1,20 @@
1
+ const axios = require('axios');
2
+
3
+ const openAPI = axios.create({
4
+ baseURL: `https://developer.myshoplaza.com/openapi/2020-07`
5
+ });
6
+
7
+ openAPI.interceptors.request.use((config) => {
8
+ return config;
9
+ });
10
+
11
+ openAPI.interceptors.response.use(
12
+ function (response) {
13
+ return response;
14
+ },
15
+ function (error) {
16
+ return Promise.reject(error);
17
+ }
18
+ );
19
+
20
+ module.exports = openAPI;
@@ -0,0 +1,76 @@
1
+ const fs = require('fs-extra');
2
+ const FormData = require('form-data');
3
+ const { getThemeFilenameTypeAndLocation } = require('../utils');
4
+ const openAPI = require('./index');
5
+
6
+ exports.getShopDetail = (config = {}) => {
7
+ return openAPI.get('/shop', config);
8
+ };
9
+
10
+ exports.getThemes = () => {
11
+ return openAPI.get('/themes');
12
+ };
13
+
14
+ exports.getThemeDetail = (theme) => {
15
+ return openAPI.get(`/themes/${theme}`);
16
+ };
17
+
18
+ exports.getDefaultThemeDetail = () => {
19
+ return openAPI.get(`/themes/default-theme`);
20
+ };
21
+
22
+ exports.pullTheme = (theme) => {
23
+ return openAPI.get(`/themes/${theme}/download`, { responseType: 'stream' });
24
+ };
25
+
26
+ exports.pushTheme = ({ zipPath, name, version, merchant_theme_id, theme_id }) => {
27
+ const form = new FormData();
28
+ form.append('file', fs.createReadStream(zipPath));
29
+ return openAPI.post('/themes/upload', form, {
30
+ headers: form.getHeaders(),
31
+ params: { name, version, merchant_theme_id, theme_id }
32
+ });
33
+ };
34
+
35
+ exports.getPushTask = (taskId) => {
36
+ return openAPI.get(`/themes/task/${taskId}`);
37
+ };
38
+
39
+ exports.publishTheme = (theme) => {
40
+ return openAPI.patch(`/themes/${theme}/publish`);
41
+ };
42
+
43
+ exports.deleteTheme = (theme) => {
44
+ return openAPI.delete(`/themes/${theme}`);
45
+ };
46
+
47
+ exports.getFileList = (theme) => {
48
+ return openAPI.get(`/themes/${theme}/doctree`);
49
+ };
50
+
51
+ exports.deleteFile = (theme, filename) => {
52
+ const { type, location } = getThemeFilenameTypeAndLocation(filename);
53
+ return openAPI.delete(`/themes/${theme}/doc`, { params: { type, location } });
54
+ };
55
+
56
+ exports.updateFile = (theme, filename) => {
57
+ const { type, location } = getThemeFilenameTypeAndLocation(filename);
58
+ return openAPI.patch(`/themes/${theme}/doc`, {
59
+ doc: {
60
+ type,
61
+ location,
62
+ content: fs.readFileSync(filename, 'utf-8')
63
+ }
64
+ });
65
+ };
66
+
67
+ exports.addFile = (theme, filename) => {
68
+ const { type, location } = getThemeFilenameTypeAndLocation(filename);
69
+ return openAPI.post(`/themes/${theme}/doc`, {
70
+ doc: {
71
+ type,
72
+ location,
73
+ content: fs.readFileSync(filename, 'utf-8')
74
+ }
75
+ });
76
+ };
@@ -0,0 +1,46 @@
1
+ const axios = require('axios');
2
+ const fs = require('fs-extra');
3
+ const chalk = require('chalk');
4
+ const Sentry = require('@sentry/node');
5
+ const { get, set } = require('../db/user');
6
+ const { getThemeFilenameTypeAndLocation } = require('../utils');
7
+ const { postExchangeToken } = require('../auth');
8
+ const log = require('../log');
9
+
10
+ const openAPI = axios.create({
11
+ baseURL: `https://${get('store_domain')}/openapi/2020-07`,
12
+ headers: { 'Access-Token': get('exchange_token') }
13
+ });
14
+
15
+ openAPI.interceptors.request.use((config) => {
16
+ if (!get('store_domain')) {
17
+ log.error(
18
+ chalk.red(
19
+ `\n✗ No store found. Please run ${chalk.cyan('shoplazza login --store STORE')} to login to a specific store`
20
+ )
21
+ );
22
+ process.exit(-1);
23
+ }
24
+ return config;
25
+ });
26
+
27
+ openAPI.interceptors.response.use(
28
+ function (response) {
29
+ return response;
30
+ },
31
+ function (error) {
32
+ // Token expired will return a 400 error
33
+ if (error?.response?.status === 400) {
34
+ set({
35
+ exchange_token: ''
36
+ });
37
+ return postExchangeToken(get('store_domain'), { ignoreLogError: error.config.ignoreLogError }).then(() => {
38
+ error.config.headers['Access-Token'] = get('exchange_token');
39
+ return openAPI(error.config);
40
+ });
41
+ }
42
+ return Promise.reject(error);
43
+ }
44
+ );
45
+
46
+ module.exports = openAPI;
package/lib/report.js ADDED
@@ -0,0 +1,37 @@
1
+ const { get } = require('./db/user');
2
+ const { isEnabledAnalytics } = require('./db/analytics');
3
+ const pkg = require('../package.json');
4
+ const axios = require('axios');
5
+
6
+ const report = (eventName, properties = {}) => {
7
+ const user_id = get('user_id');
8
+ if (!isEnabledAnalytics(user_id)) return;
9
+ axios.post(
10
+ `https://r.shoplazza.com/beacon/sa.gif`,
11
+ `data=${encodeURIComponent(
12
+ Buffer.from(
13
+ JSON.stringify({
14
+ distinct_id: user_id,
15
+ event: eventName,
16
+ type: 'track',
17
+ _track_id: parseInt(Math.random() * (9999999999 - 999999999 + 1) + 999999999, 10),
18
+ properties: {
19
+ platform: 'shoplazza-cli',
20
+ cli_version: pkg.version,
21
+ user_id,
22
+ ...properties
23
+ }
24
+ })
25
+ ).toString('base64')
26
+ )}`,
27
+ {
28
+ headers: { 'content-type': 'text/plain;charset=UTF-8' },
29
+ params: {
30
+ project: 'production',
31
+ gzip: 0
32
+ }
33
+ }
34
+ );
35
+ };
36
+
37
+ module.exports = report;