@ordergroove/smi-serve 1.7.3 → 1.7.5

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/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.7.5](https://github.com/ordergroove/plush-toys/compare/@ordergroove/smi-serve@1.7.4...@ordergroove/smi-serve@1.7.5) (2024-04-22)
7
+
8
+ **Note:** Version bump only for package @ordergroove/smi-serve
9
+
10
+
11
+
12
+
13
+
14
+ ## [1.7.4](https://github.com/ordergroove/plush-toys/compare/@ordergroove/smi-serve@1.7.3...@ordergroove/smi-serve@1.7.4) (2024-04-22)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * use cwd when constructing filepaths ([9dd0743](https://github.com/ordergroove/plush-toys/commit/9dd0743ade2daad9deb0f0c551162db4c52d6f03))
20
+
21
+
22
+
23
+
24
+
6
25
  ## [1.7.3](https://github.com/ordergroove/plush-toys/compare/@ordergroove/smi-serve@1.7.2...@ordergroove/smi-serve@1.7.3) (2024-04-19)
7
26
 
8
27
 
package/README.md CHANGED
@@ -1,14 +1,10 @@
1
1
  # @ordergroove/smi-serve
2
2
 
3
- This README document provides an overview and usage instructions for the `smi-serve` program.
4
-
5
- ## Overview
6
-
7
- `smi-serve` is a command-line tool designed to manage authentication credentials, initialize directories for use with Ordergroove's services, and start a development server.
3
+ `smi-serve` is a CLI tool to scaffold and run a local dev environment for Ordergroove's Subscription Manager templates.
8
4
 
9
5
  ## Installation
10
6
 
11
- Before using `smi-serve`, ensure you have Node.js installed on your system. If not, you can download and install it from [nodejs.org](https://nodejs.org/).
7
+ Before using `smi-serve`, ensure you have Node.js installed on your system. If not, you can download and install it from [nodejs.org](https://nodejs.org/). The smi-serve tool requires Node 18 or later.
12
8
 
13
9
  ## Usage
14
10
 
@@ -0,0 +1,2 @@
1
+ const { fs } = require('memfs');
2
+ module.exports = fs.promises;
@@ -0,0 +1,2 @@
1
+ const { fs } = require('memfs');
2
+ module.exports = fs;
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ // needed to prevent error when requiring esbuild during unit tests
3
+ // "Invariant violation: "Buffer.from("") instanceof Uint8Array" is incorrectly false"
4
+ globals: {
5
+ Uint8Array: Uint8Array
6
+ }
7
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ordergroove/smi-serve",
3
- "version": "1.7.3",
4
- "description": "Utility to serve a SMI template locally",
3
+ "version": "1.7.5",
4
+ "description": "Utility to serve a Subscription Manager template locally",
5
5
  "keywords": [],
6
6
  "author": "Eugenio Lattanzio <eugenio.lattanzio@ordergroove.com>",
7
7
  "license": "ISC",
@@ -11,7 +11,7 @@
11
11
  "url": "git+https://github.com/ordergroove/plush-toys.git"
12
12
  },
13
13
  "scripts": {
14
- "test": "echo \"Error: run tests from root\" && exit 0"
14
+ "test": "../../node_modules/.bin/jest"
15
15
  },
16
16
  "bugs": {
17
17
  "url": "https://github.com/ordergroove/plush-toys/issues"
@@ -32,5 +32,8 @@
32
32
  "ora": "^5.4.1",
33
33
  "yargs": "^17.7.2"
34
34
  },
35
- "gitHead": "9ed3442b896ca9d86606d9ae93fda79c330ba575"
35
+ "devDependencies": {
36
+ "memfs": "^4.8.2"
37
+ },
38
+ "gitHead": "53bdf51afea74383c5e1dc80e12b3ae92116192c"
36
39
  }
package/smi-serve.js CHANGED
@@ -10,7 +10,7 @@ const figures = require('figures');
10
10
  const { getcwd, getNetFreePort, readRcEnv } = require('./src/utils');
11
11
  const { cliCallSelectMerchant } = require('./src/select-merchant');
12
12
  const { deploy } = require('./src/deploy');
13
- const { init, DirNotEmpty } = require('./src/init');
13
+ const { init, DirNotEmpty, OG_RC_FILE } = require('./src/init');
14
14
  const { serve } = require('./src/serve');
15
15
 
16
16
  function box(text) {
@@ -25,7 +25,7 @@ ${text
25
25
  }
26
26
 
27
27
  async function initOrServe(args) {
28
- const files = await glob(`${getcwd()}/**/*.*`, { ignore: ['**/node_modules/**', 'node_modules/**'] });
28
+ const files = await glob(`${getcwd(args)}/**/*.*`, { ignore: ['**/node_modules/**', 'node_modules/**'] });
29
29
  const mainLiquid = files.find(it => it.includes('main.liquid'));
30
30
  if (!mainLiquid) {
31
31
  const { initialize } = await inquirer.prompt([
@@ -63,7 +63,6 @@ Platform: ${merchant.ecommerce_platform}
63
63
  Alterations, additions, or deletions to the Subscription Manager will be visible to your customers
64
64
  in real-time after you deploy.\n`);
65
65
 
66
- if (args.verbose) console.log(args);
67
66
  try {
68
67
  await fn(args);
69
68
  console.log(`${figures.tick} SUCCESS`);
@@ -71,8 +70,8 @@ Platform: ${merchant.ecommerce_platform}
71
70
  if (err instanceof DirNotEmpty) {
72
71
  console.error(`${figures.cross} ERROR`);
73
72
  console.error(`\
74
- The directory is not empty and fetching will override your local changes
75
- try force fetch by using -f modifier
73
+ The directory is not empty and fetching will override your local changes.
74
+ To overwrite your local changes, re-run the command with "-f".
76
75
  `);
77
76
  } else {
78
77
  throw err;
@@ -82,88 +81,89 @@ try force fetch by using -f modifier
82
81
  }
83
82
 
84
83
  async function program() {
85
- yargs(hideBin(process.argv))
86
- .command({
87
- command: 'select-merchant',
88
- describe: 'Select a different Ordergroove merchant',
89
- handler: wrapHandler(cliCallSelectMerchant)
90
- })
91
- .command({
92
- command: 'init',
93
- alias: ['fetch'],
94
- describe: 'Initialize the current directory with smi assets from og rc3',
95
- builder: y =>
96
- y
97
- .option('overwrite', {
98
- alias: 'f',
99
- type: 'boolean',
100
- description: 'Overwrite files if exists locally'
101
- })
102
- .option('entrypoint', {
103
- type: 'boolean',
104
- description: 'generate entrypoint.js'
105
- })
106
- .option('yes', {
107
- alias: 'y',
108
- type: 'boolean',
109
- description: 'Initilizes it with rc3 config, otherwise it prompts'
110
- }),
84
+ return (
85
+ yargs(hideBin(process.argv))
86
+ .command({
87
+ command: 'select-merchant',
88
+ describe: 'Select a different Ordergroove merchant',
89
+ handler: wrapHandler(cliCallSelectMerchant)
90
+ })
91
+ .command({
92
+ command: 'init',
93
+ alias: ['fetch'],
94
+ describe: 'Initialize the current directory with the assets from your live Subscription Manager theme',
95
+ builder: y =>
96
+ y
97
+ .option('overwrite', {
98
+ alias: 'f',
99
+ type: 'boolean',
100
+ description: 'Allow overwriting existing files'
101
+ })
102
+ .option('yes', {
103
+ alias: 'y',
104
+ type: 'boolean',
105
+ description: 'Skip prompts and initialize with your live theme'
106
+ }),
111
107
 
112
- handler: wrapHandler(init)
113
- })
114
- .command({
115
- command: 'deploy',
116
- describe: 'Deploy changes to ordergroove',
117
- handler: wrapHandler(deploy)
118
- })
119
- .command({
120
- command: '$0',
121
- alias: ['serve'],
122
- describe: 'Start the dev server',
123
- handler: wrapHandler(initOrServe)
124
- })
125
- .option('verbose', {
126
- alias: 'v',
127
- type: 'boolean',
128
- description: 'Run with verbose logging'
129
- })
130
- .option('outdir', {
131
- alias: 'o',
132
- type: 'string',
133
- default: 'node_modules/.smi-serve-build',
134
- description: 'Output directory'
135
- })
136
- .option('configFile', {
137
- alias: ['c', 'config-file'],
138
- type: 'string',
139
- default: '.ogrc.json',
140
- description: 'Ordergroove configuration file'
141
- })
142
- .option('cwd', {
143
- alias: ['w', 'working-dir'],
144
- type: 'string',
145
- default: '.',
146
- description: 'Sets the current working directory'
147
- })
148
- .option('port', {
149
- alias: 'p',
150
- type: 'number',
151
- description: 'http port',
152
- default: await getNetFreePort()
153
- })
154
- .option('impersonate', {
155
- alias: 'i',
156
- type: 'string',
157
- description: 'Enable customer impersonation'
158
- })
159
- .option('env', {
160
- alias: 'e',
161
- type: 'string',
162
- choices: ['prod', 'staging', 'local'],
163
- default: 'prod',
164
- description: 'Ordergroove enviromnet staging/prod'
165
- })
166
- .parse();
108
+ handler: wrapHandler(init)
109
+ })
110
+ .command({
111
+ command: 'deploy',
112
+ describe: 'Publish your template changes to your live theme',
113
+ handler: wrapHandler(deploy)
114
+ })
115
+ .command({
116
+ command: '$0',
117
+ alias: ['serve'],
118
+ describe: 'Start the dev server',
119
+ handler: wrapHandler(initOrServe)
120
+ })
121
+ .option('verbose', {
122
+ alias: 'v',
123
+ type: 'boolean',
124
+ description: 'Enable verbose logging'
125
+ })
126
+ .option('outdir', {
127
+ alias: 'o',
128
+ type: 'string',
129
+ default: 'node_modules/.smi-serve-build',
130
+ description: 'Where to output the bundle for local dev'
131
+ })
132
+ .option('configFile', {
133
+ alias: ['c', 'config-file'],
134
+ type: 'string',
135
+ default: OG_RC_FILE,
136
+ description: 'Name of the Ordergroove configuration file'
137
+ })
138
+ .option('cwd', {
139
+ alias: ['w', 'working-dir'],
140
+ type: 'string',
141
+ default: '.',
142
+ description: 'Path to the working directory. The directory must already exist.'
143
+ })
144
+ .option('port', {
145
+ alias: 'p',
146
+ type: 'number',
147
+ description: 'Which local port to run the dev server on',
148
+ default: await getNetFreePort()
149
+ })
150
+ .option('impersonate', {
151
+ alias: 'i',
152
+ type: 'string'
153
+ })
154
+ // impersonate only works for superusers; hide it from help for now
155
+ .hide('impersonate')
156
+ .option('env', {
157
+ alias: 'e',
158
+ type: 'string',
159
+ choices: ['prod', 'staging', 'local'],
160
+ default: 'prod',
161
+ description: 'Which Ordergroove environment to use'
162
+ })
163
+ .parse()
164
+ );
167
165
  }
168
166
 
169
167
  if (require.main === module) program();
168
+
169
+ module.exports = program;
@@ -0,0 +1,118 @@
1
+ // mock Node fs to be in memory
2
+ jest.mock('fs');
3
+ jest.mock('fs/promises');
4
+ const { vol } = require('memfs');
5
+
6
+ // mock all fetch calls
7
+ jest.mock('node-fetch', () => require('fetch-mock').sandbox());
8
+ const fetchMock = require('node-fetch');
9
+
10
+ // don't run local dev server logic
11
+ jest.mock('./src/serve');
12
+ // mock login function to return a mock token
13
+ jest.mock('./src/login', () => ({
14
+ ...jest.requireActual('./src/login'),
15
+ login: jest.fn().mockReturnValue(Promise.resolve('mock-auth'))
16
+ }));
17
+ jest.mock('./src/utils', () => ({
18
+ ...jest.requireActual('./src/utils'),
19
+ exec: jest.fn() // don't attempt to spawn new processes
20
+ }));
21
+
22
+ const smiServe = require('./smi-serve');
23
+
24
+ function runCommand(command) {
25
+ // the first two args don't matter, but it sends the correct number of CLI args
26
+ process.argv = ['node', 'smi-serve.js', ...command.split(' ')];
27
+ return smiServe();
28
+ }
29
+
30
+ describe('smi-serve', () => {
31
+ let originalArgv;
32
+
33
+ beforeEach(() => {
34
+ // wipe mock filesystem
35
+ vol.fromJSON({}, '/');
36
+ originalArgv = process.argv;
37
+ });
38
+
39
+ afterEach(() => {
40
+ process.argv = originalArgv;
41
+ fetchMock.reset();
42
+ });
43
+
44
+ it('inits with expected files', async () => {
45
+ fetchMock.get('https://rc3.ordergroove.com/api/merchants/', mockSingleMerchantResponse());
46
+ fetchMock.get('https://rc3.ordergroove.com/configs/msi/?merchant_public_id=yum-id', mockMSIConfig());
47
+
48
+ await runCommand('init --cwd / -y');
49
+
50
+ const files = vol.toJSON();
51
+ for (const file of Object.keys(files)) {
52
+ if (file.endsWith('.json')) {
53
+ files[file] = JSON.parse(files[file]);
54
+ }
55
+ }
56
+ expect(files).toEqual({
57
+ '/.gitignore': '.ogrc.json\nnode_modules/\n',
58
+ '/.ogrc.json': {
59
+ prod: {
60
+ token: 'mock-auth'
61
+ }
62
+ },
63
+ '/package.json': {
64
+ author: '',
65
+ description: 'Ordergroove Subscription Manager for test merchant on shopify platform (yum-id)}',
66
+ keywords: ['Ordergroove Subscription Manager', 'test merchant', 'yum-id'],
67
+ main: 'views/main.liquid',
68
+ ordergroove: {
69
+ coreVersion: '0.31.6',
70
+ templatesVersion: '0.40.1'
71
+ },
72
+ scripts: {
73
+ deploy: 'smi-serve deploy',
74
+ start: 'smi-serve'
75
+ }
76
+ },
77
+ '/styles/main.less': '* { color: red; }',
78
+ '/views/main.liquid': '<h1>Hello world</h1>'
79
+ });
80
+ });
81
+ });
82
+
83
+ function mockSingleMerchantResponse() {
84
+ return [
85
+ {
86
+ name: 'test merchant',
87
+ public_id: 'yum-id',
88
+ ecommerce_platform: 'shopify'
89
+ }
90
+ ];
91
+ }
92
+
93
+ function mockMSIConfig() {
94
+ return {
95
+ configs: {
96
+ smi: {
97
+ files: [
98
+ {
99
+ name: '/views/main.liquid',
100
+ content: '<h1>Hello world</h1>'
101
+ },
102
+ {
103
+ name: '/styles/main.less',
104
+ content: '* { color: red; }'
105
+ }
106
+ ]
107
+ },
108
+ provisioned_with: {
109
+ '@ordergroove/smi-templates': '0.40.1'
110
+ }
111
+ },
112
+ meta_fields: {
113
+ dependencies: {
114
+ '@ordergroove/smi-core': '0.31.6'
115
+ }
116
+ }
117
+ };
118
+ }
package/src/auth.js CHANGED
@@ -1,19 +1,11 @@
1
- const http = require('http');
2
1
  const inquirer = require('inquirer');
3
2
  const inquirerPrompt = require('inquirer-autocomplete-prompt');
4
3
 
5
- const { open, getNetFreePort, readRcEnv, writeRcEnv, isValidToken } = require('./utils');
4
+ const { readRcEnv, writeRcEnv, isValidToken } = require('./utils');
5
+ const { login, getRC3Url } = require('./login');
6
6
 
7
7
  inquirer.registerPrompt('autocomplete', inquirerPrompt);
8
8
 
9
- function getRC3Url({ env }) {
10
- if (env.startsWith('st')) return `https://rc3.stg.ordergroove.com/`;
11
- if (env.startsWith('l')) return `http://0.0.0.0:3000/`;
12
- return `https://rc3.ordergroove.com/`;
13
- }
14
-
15
- exports.getRC3Url = getRC3Url;
16
-
17
9
  async function getValidLoginAndCurrentMerchant(args) {
18
10
  const existingSettings = await readRcEnv(args);
19
11
  let token, merchant;
@@ -35,92 +27,4 @@ async function getValidLoginAndCurrentMerchant(args) {
35
27
  }
36
28
  exports.getValidLoginAndCurrentMerchant = getValidLoginAndCurrentMerchant;
37
29
 
38
- async function login(args) {
39
- /* eslint-disable no-async-promise-executor */
40
- return await new Promise(async (resolveAuth, rejectAuth) => {
41
- const port = await getNetFreePort();
42
-
43
- let server;
44
-
45
- const sockets = new Set();
46
- /**
47
- * Forcefully terminates HTTP server.
48
- */
49
- const terminateServer = () => {
50
- clearTimeout(authRejectTimeout);
51
- return new Promise((resolve, reject) => {
52
- [...sockets].forEach(socket => {
53
- socket.destroy();
54
- sockets.delete(socket);
55
- });
56
-
57
- server.close(err => {
58
- process.nextTick(() => {
59
- if (err) {
60
- reject(err);
61
- } else {
62
- resolve();
63
- }
64
- });
65
- });
66
- });
67
- };
68
-
69
- async function requestListener(req, res) {
70
- if (req.method === 'POST' && req.headers['content-type'] === 'application/x-www-form-urlencoded') {
71
- let body = '';
72
-
73
- req.on('data', chunk => {
74
- body += chunk.toString();
75
- });
76
-
77
- req.on('end', async () => {
78
- const params = new URLSearchParams(body);
79
- const token = params.get('token');
80
-
81
- if (!isValidToken(token)) {
82
- res.writeHead(400);
83
- res.end('Invalid request\n');
84
- return;
85
- }
86
-
87
- resolveAuth(token);
88
-
89
- res.writeHead(302, { Location: getRC3Url(args) });
90
- res.end();
91
- await terminateServer();
92
- });
93
- } else {
94
- res.writeHead(200, { 'Content-Type': 'text/html' });
95
- res.end('Invalid request\n');
96
- }
97
- }
98
-
99
- server = http.createServer(requestListener);
100
- server.on('connection', socket => {
101
- sockets.add(socket);
102
-
103
- server.once('close', () => {
104
- sockets.delete(socket);
105
- });
106
- });
107
-
108
- server.listen(port, '127.0.0.1', () => {
109
- const loginUrl = `${getRC3Url(args)}?${new URLSearchParams([
110
- ['login_redirect', `http://localhost:${port}`],
111
- ['sm_local_dev', 'true']
112
- ])}`;
113
- console.log('A browser window has been opened to Ordergroove. Please log in through your browser to continue.');
114
- console.log(`Browsing ${loginUrl}`);
115
- open(loginUrl);
116
- });
117
-
118
- // terminate the program after 5 minutes waiting for auth.
119
- const authRejectTimeout = setTimeout(
120
- () => {
121
- rejectAuth(new Error('Auth timeout'));
122
- },
123
- 5 * 60 * 1000
124
- );
125
- });
126
- }
30
+ exports.getRC3Url = getRC3Url;
package/src/deploy.js CHANGED
@@ -63,11 +63,11 @@ async function deploy(args) {
63
63
  '**/package-lock.json'
64
64
  ];
65
65
 
66
- const files = await glob(`${getcwd()}/**/*.*`, { ignore: ignoreFiles });
66
+ const files = await glob(`${getcwd(args)}/**/*.*`, { ignore: ignoreFiles });
67
67
 
68
68
  const fileList = await Promise.all(
69
69
  files.map(async file => ({
70
- name: file.substring(getcwd().length),
70
+ name: file.substring(getcwd(args).length),
71
71
  content: await fs.promises.readFile(file, 'utf8')
72
72
  }))
73
73
  );
@@ -90,7 +90,7 @@ async function deploy(args) {
90
90
  console.log(err);
91
91
  }
92
92
 
93
- const packageJson = readPackageJson();
93
+ const packageJson = readPackageJson(getcwd(args));
94
94
  const smiTemplatesVersion = packageJson[packageJsonKeys.OG_SECTION]?.[packageJsonKeys.TEMPLATES_VERSION];
95
95
 
96
96
  const newRequest = {
@@ -135,7 +135,7 @@ async function deploy(args) {
135
135
  }
136
136
  );
137
137
  if (response.status === 201) {
138
- console.log('SMI changes deployed');
138
+ console.log('Subscription Manager changes deployed');
139
139
  } else {
140
140
  console.error(response.status, await response.text());
141
141
  }
@@ -65,10 +65,10 @@ async function impersonate(args) {
65
65
  console.log(`\
66
66
 
67
67
  -------------------------------------------------------------------------------
68
- Browsing SMI for customer: ${customer.first_name} ${customer.last_name} (${customer.email})
68
+ Browsing Subscription Manager for customer: ${customer.first_name} ${customer.last_name} (${customer.email})
69
69
 
70
70
  Please note:
71
- This tool browses the live SMI of the customer you are impersonating.
71
+ This tool browses the live Subscription Manager of the customer you are impersonating.
72
72
  Do not make any changes to orders/subscriptions as they will actually
73
73
  take effect. This tool is intended for troubleshooting and previewing
74
74
  only. Please use the CSA to edit order/subscription data.
package/src/init.js CHANGED
@@ -3,7 +3,7 @@ const fetch = require('node-fetch');
3
3
  const path = require('path');
4
4
  const inquirer = require('inquirer');
5
5
 
6
- const { chooseMerchant, getRC3Url } = require('./auth');
6
+ const { getRC3Url } = require('./auth');
7
7
  const {
8
8
  getcwd,
9
9
  exec,
@@ -16,7 +16,6 @@ const { serve } = require('./serve');
16
16
  const { getValidSettings } = require('./select-merchant');
17
17
 
18
18
  const SUBSCRIPTION_MANAGEMENT_ENDPOINT = 'msi';
19
- const SMI_CORE_NAME = '@ordergroove/smi-core';
20
19
  const SMI_TEMPLATES_NAME = '@ordergroove/smi-templates';
21
20
  const OG_RC_FILE = '.ogrc.json';
22
21
 
@@ -46,7 +45,7 @@ async function updateJsonFile(filename, config) {
46
45
  await fs.promises.writeFile(filename, JSON.stringify(original, null, 4), { encoding: 'utf8' });
47
46
  }
48
47
 
49
- const downloadAndExtract = async url => {
48
+ const downloadAndExtract = async (url, args) => {
50
49
  try {
51
50
  console.log(`Downloading ZIP file from ${url}`);
52
51
  const response = await fetch(url);
@@ -54,7 +53,7 @@ const downloadAndExtract = async url => {
54
53
  if (!response.ok) {
55
54
  throw new Error(`Failed to download ZIP file. Status code: ${response.status}`);
56
55
  }
57
- const cwd = getcwd();
56
+ const cwd = getcwd(args);
58
57
  const zipFileName = path.basename(url);
59
58
  const zipFilePath = path.join(cwd, zipFileName);
60
59
 
@@ -62,7 +61,7 @@ const downloadAndExtract = async url => {
62
61
 
63
62
  console.log('ZIP file downloaded successfully');
64
63
 
65
- await unzip(zipFilePath, getcwd());
64
+ await unzip(zipFilePath, cwd);
66
65
  } catch (error) {
67
66
  console.error('Error:', error.message);
68
67
  }
@@ -101,10 +100,10 @@ async function init(args) {
101
100
  {
102
101
  type: 'list',
103
102
  name: 'useMerchantSpecific',
104
- message: 'What templates do you want to initialize it with?',
103
+ message: 'Which templates do you want to use?',
105
104
  choices: [
106
- { value: true, name: `Pull "${merchant.name}" templates stored in ${getRC3Url(args)}` },
107
- { value: false, name: 'Fresh install latest version of @ordergroove/smi-templates' }
105
+ { value: true, name: `Pull the live theme for "${merchant.name}" from Ordergroove` },
106
+ { value: false, name: 'Use the latest files from the default Subscription Manager template' }
108
107
  ]
109
108
  }
110
109
  ]);
@@ -117,7 +116,7 @@ async function init(args) {
117
116
  }
118
117
 
119
118
  (msiConfigs.smi.files || []).forEach(({ content, name }) => {
120
- const filepath = `.${name}`;
119
+ const filepath = path.join(args.cwd, name);
121
120
  fs.mkdirSync(path.dirname(filepath), { recursive: true });
122
121
  fs.writeFileSync(filepath, content, { encoding: 'utf8' });
123
122
  });
@@ -129,7 +128,8 @@ async function init(args) {
129
128
  smiTemplateVersion = await getMerchantTemplatesVersion(smiVersion);
130
129
 
131
130
  await downloadAndExtract(
132
- `https://static.ordergroove.com/@ordergroove/smi-templates/${smiTemplateVersion}/dist/smi-template.zip`
131
+ `https://static.ordergroove.com/@ordergroove/smi-templates/${smiTemplateVersion}/dist/smi-template.zip`,
132
+ args
133
133
  );
134
134
  }
135
135
 
@@ -139,52 +139,24 @@ async function init(args) {
139
139
  smiVersion = 'latest';
140
140
  }
141
141
 
142
- // skip this for now
143
- const installEntrypoint = args.entrypoint;
144
-
145
- // this auth_config will be the one in rc, since sos will update
146
- // it with whatever is on subscription-settings endpoint
147
- //
148
- // const { configs: merchantSettings } = await getConfigs(SUBSCRIPTION_SETTINGS_ENDPOINT, merchant, token);
149
- // const auth_config = {
150
- // auth_url: merchantSettings.authUrl,
151
- // env: merchantSettings.env
152
- // };
153
-
154
- const auth_config = {};
155
-
156
- //
157
- if (installEntrypoint) {
158
- // put some runtime config with merchant id
159
- fs.writeFileSync(
160
- './runtime-config.js',
161
- `\
162
- export const merchant_id = ${JSON.stringify(merchant.public_id)};
163
- export const auth_config = ${JSON.stringify(auth_config)};
164
- `
165
- );
166
-
167
- fs.writeFileSync('entrypoint.js', fs.readFileSync(`${__dirname}/../entrypoint.js`, 'utf8'));
168
- }
169
-
170
142
  // write a .gitignore just in case
171
143
  fs.writeFileSync(
172
- '.gitignore',
144
+ path.join(args.cwd, '.gitignore'),
173
145
  `\
174
146
  ${OG_RC_FILE}
175
147
  node_modules/
176
148
  `
177
149
  );
178
150
 
179
- await updateJsonFile('package.json', {
151
+ await updateJsonFile(path.join(args.cwd, 'package.json'), {
180
152
  scripts: {
181
153
  start: 'smi-serve',
182
154
  deploy: 'smi-serve deploy'
183
155
  },
184
- description: `Ordergroove SMI for ${merchant.name} on ${merchant.ecommerce_platform} platform (${merchant.public_id})}`,
156
+ description: `Ordergroove Subscription Manager for ${merchant.name} on ${merchant.ecommerce_platform} platform (${merchant.public_id})}`,
185
157
  author: getAuthorNameFromToken(token),
186
- main: installEntrypoint ? 'entrypoint.js' : 'views/main.liquid',
187
- keywords: ['Ordergroove SMI', merchant.name, merchant.public_id],
158
+ main: 'views/main.liquid',
159
+ keywords: ['Ordergroove Subscription Manager', merchant.name, merchant.public_id],
188
160
  [packageJsonKeys.OG_SECTION]: {
189
161
  [packageJsonKeys.CORE_VERSION]: smiVersion,
190
162
  [packageJsonKeys.TEMPLATES_VERSION]: smiTemplateVersion
@@ -192,7 +164,7 @@ node_modules/
192
164
  });
193
165
 
194
166
  // initialize it as npm package
195
- await exec('npm', 'init', '-y');
167
+ await exec(args.cwd, 'npm', 'init', '-y');
196
168
 
197
169
  // install used smi-serve to allow dev
198
170
  // await exec('npm', 'install', '--save-dev', '@ordergroove/smi-serve');
package/src/login.js ADDED
@@ -0,0 +1,102 @@
1
+ const http = require('http');
2
+ const { open, getNetFreePort, isValidToken } = require('./utils');
3
+
4
+ async function login(args) {
5
+ /* eslint-disable no-async-promise-executor */
6
+ return await new Promise(async (resolveAuth, rejectAuth) => {
7
+ const port = await getNetFreePort();
8
+
9
+ let server;
10
+
11
+ const sockets = new Set();
12
+ /**
13
+ * Forcefully terminates HTTP server.
14
+ */
15
+ const terminateServer = () => {
16
+ clearTimeout(authRejectTimeout);
17
+ return new Promise((resolve, reject) => {
18
+ [...sockets].forEach(socket => {
19
+ socket.destroy();
20
+ sockets.delete(socket);
21
+ });
22
+
23
+ server.close(err => {
24
+ process.nextTick(() => {
25
+ if (err) {
26
+ reject(err);
27
+ } else {
28
+ resolve();
29
+ }
30
+ });
31
+ });
32
+ });
33
+ };
34
+
35
+ async function requestListener(req, res) {
36
+ if (req.method === 'POST' && req.headers['content-type'] === 'application/x-www-form-urlencoded') {
37
+ let body = '';
38
+
39
+ req.on('data', chunk => {
40
+ body += chunk.toString();
41
+ });
42
+
43
+ req.on('end', async () => {
44
+ const params = new URLSearchParams(body);
45
+ const token = params.get('token');
46
+
47
+ if (!isValidToken(token)) {
48
+ res.writeHead(400);
49
+ res.end('Invalid request\n');
50
+ return;
51
+ }
52
+
53
+ resolveAuth(token);
54
+
55
+ res.writeHead(302, { Location: getRC3Url(args) });
56
+ res.end();
57
+ await terminateServer();
58
+ });
59
+ } else {
60
+ res.writeHead(200, { 'Content-Type': 'text/html' });
61
+ res.end('Invalid request\n');
62
+ }
63
+ }
64
+
65
+ server = http.createServer(requestListener);
66
+ server.on('connection', socket => {
67
+ sockets.add(socket);
68
+
69
+ server.once('close', () => {
70
+ sockets.delete(socket);
71
+ });
72
+ });
73
+
74
+ server.listen(port, '127.0.0.1', () => {
75
+ const loginUrl = `${getRC3Url(args)}?${new URLSearchParams([
76
+ ['login_redirect', `http://localhost:${port}`],
77
+ ['sm_local_dev', 'true']
78
+ ])}`;
79
+ console.log('A browser window has been opened to Ordergroove. Please log in through your browser to continue.');
80
+ console.log(`Browsing ${loginUrl}`);
81
+ open(loginUrl);
82
+ });
83
+
84
+ // terminate the program after 5 minutes waiting for auth.
85
+ const authRejectTimeout = setTimeout(
86
+ () => {
87
+ rejectAuth(new Error('Auth timeout'));
88
+ },
89
+ 5 * 60 * 1000
90
+ );
91
+ });
92
+ }
93
+
94
+ exports.login = login;
95
+
96
+ function getRC3Url({ env }) {
97
+ if (env.startsWith('st')) return `https://rc3.stg.ordergroove.com/`;
98
+ if (env.startsWith('l')) return `http://0.0.0.0:3000/`;
99
+ return `https://rc3.ordergroove.com/`;
100
+ }
101
+
102
+ exports.getRC3Url = getRC3Url;
@@ -1,6 +1,6 @@
1
1
  <html>
2
2
  <head>
3
- <title>SMI Demo Page</title>
3
+ <title>Subscription Manager Demo Page</title>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <link rel="stylesheet" href="/main.css" />
package/src/serve.js CHANGED
@@ -20,7 +20,7 @@ async function getGlobals(argv) {
20
20
  hmacAuth = customer && [customer.merchant_user_id, customer.ts, customer.hash].join('|');
21
21
  }
22
22
 
23
- const packageJson = readPackageJson();
23
+ const packageJson = readPackageJson(getcwd(argv));
24
24
  const smiCoreVersion = packageJson[packageJsonKeys.OG_SECTION]?.[packageJsonKeys.CORE_VERSION] || 'latest';
25
25
 
26
26
  return {
@@ -31,20 +31,21 @@ async function getGlobals(argv) {
31
31
  };
32
32
  }
33
33
 
34
- const smiDevModePlugin = globals => {
34
+ const smiDevModePlugin = (globals, args) => {
35
+ const cwd = getcwd(args);
35
36
  /** @type {import('esbuild').Plugin} */
36
37
  const plugin = {
37
38
  name: 'resolve_smi_templates',
38
39
  setup(build) {
39
40
  build.onLoad({ filter: /main\.liquid/ }, async () => {
40
41
  // load only liquid,js,json files (less files are handled by lessPlugin)
41
- const files = await glob(`${getcwd()}/**/*.{liquid,json,js}`, {
42
+ const files = await glob(`${cwd}/**/*.{liquid,json,js}`, {
42
43
  ignore: ['**/node_modules/**', 'node_modules/**']
43
44
  });
44
45
 
45
46
  const fileList = await Promise.all(
46
47
  files.map(async file => ({
47
- name: file.substring(getcwd().length),
48
+ name: file.substring(cwd.length),
48
49
  content: await fs.promises.readFile(file, 'utf8').catch(() => '')
49
50
  }))
50
51
  );
@@ -77,7 +78,7 @@ const smiDevModePlugin = globals => {
77
78
  build.onResolve({ filter: /^~\// }, args => {
78
79
  return build.resolve(args.path.replace('~/', './'), {
79
80
  kind: 'import-statement',
80
- resolveDir: getcwd()
81
+ resolveDir: cwd
81
82
  });
82
83
  });
83
84
  }
@@ -93,12 +94,13 @@ async function serve(argv) {
93
94
  const [mainLess] = await glob('**/main.@(less|css)');
94
95
 
95
96
  if (!mainLiquid) {
96
- throw new Error('main.liquid not found, please create a main.liquid file under src/views/main.liquid');
97
+ throw new Error('You need a main.liquid file in the /views folder to run the Subscription Manager.');
97
98
  }
98
99
 
99
- fs.mkdirSync(path.join(getcwd(), outdir), { recursive: true });
100
+ const cwd = getcwd(argv);
101
+ fs.mkdirSync(path.join(cwd, outdir), { recursive: true });
100
102
 
101
- const smiIndexFile = path.join(getcwd(), outdir, 'index.html');
103
+ const smiIndexFile = path.join(cwd, outdir, 'index.html');
102
104
  const smiIndexSource = fs.readFileSync(`${__dirname}/partials/index.html`, 'utf8');
103
105
  fs.writeFileSync(smiIndexFile, smiIndexSource, { encoding: 'utf8' });
104
106
 
@@ -110,7 +112,7 @@ async function serve(argv) {
110
112
  entryNames: '[name]',
111
113
 
112
114
  logLevel: verbose ? 'verbose' : 'error',
113
- nodePaths: [path.join(getcwd(), 'node_modules')],
115
+ nodePaths: [path.join(cwd, 'node_modules')],
114
116
  format: 'esm',
115
117
  globalName: 'smiTemplate',
116
118
  loader: {
@@ -120,12 +122,12 @@ async function serve(argv) {
120
122
  legalComments: 'none',
121
123
  sourcemap: true,
122
124
 
123
- outdir: path.join(getcwd(), outdir),
124
- plugins: [smiDevModePlugin(globals), lessLoader()]
125
+ outdir: path.join(cwd, outdir),
126
+ plugins: [smiDevModePlugin(globals, argv), lessLoader()]
125
127
  };
126
128
 
127
129
  if (!mainLess) {
128
- console.warn('styles not found, you can create a main.less file under src/styles/main.less');
130
+ console.warn('No styles found. To apply CSS, create a main.less file inside the /styles folder.');
129
131
  }
130
132
 
131
133
  const ctx = await esbuild.context(buildConf);
@@ -133,8 +135,7 @@ async function serve(argv) {
133
135
  await ctx.watch();
134
136
 
135
137
  const { port: actualPort, host: actualHost } = await ctx.serve({
136
- servedir: path.join(getcwd(), outdir),
137
- // host,
138
+ servedir: path.join(cwd, outdir),
138
139
  port
139
140
  });
140
141
 
@@ -149,7 +150,7 @@ async function serve(argv) {
149
150
  const text = await esbuild.analyzeMetafile(result.metafile, {
150
151
  verbose: true
151
152
  });
152
- fs.writeFileSync(path.join(getcwd(), outdir, 'bundle-report.html'), `<pre id="esbuild-metadata">${text}</pre>`);
153
+ fs.writeFileSync(path.join(cwd, outdir, 'bundle-report.html'), `<pre id="esbuild-metadata">${text}</pre>`);
153
154
 
154
155
  return result;
155
156
  }
package/src/utils.js CHANGED
@@ -10,8 +10,8 @@ const fetch = require('node-fetch');
10
10
 
11
11
  exports.glob = util.promisify(require('glob'));
12
12
 
13
- function getcwd() {
14
- return process.cwd();
13
+ function getcwd(argv) {
14
+ return path.join(process.cwd(), argv.cwd);
15
15
  }
16
16
 
17
17
  exports.getcwd = getcwd;
@@ -50,9 +50,11 @@ exports.open = function open(loginUrl) {
50
50
  * @param {...any} args
51
51
  * @returns
52
52
  */
53
- exports.exec = async function exec(cmd, ...args) {
53
+ exports.exec = async function exec(cwd, cmd, ...args) {
54
54
  return new Promise((resolve, reject) => {
55
- const child = spawn(cmd, args);
55
+ const child = spawn(cmd, args, {
56
+ cwd
57
+ });
56
58
  const stdout = '';
57
59
  const stderr = '';
58
60
 
@@ -94,9 +96,10 @@ exports.unzip = async function unzip(zipFilePath, outdir) {
94
96
  };
95
97
 
96
98
  async function readRc(args) {
97
- if (fs.existsSync(args.configFile)) {
99
+ const configPath = path.join(args.cwd, args.configFile);
100
+ if (fs.existsSync(configPath)) {
98
101
  try {
99
- const source = await fs.promises.readFile(args.configFile, 'utf8');
102
+ const source = await fs.promises.readFile(configPath, 'utf8');
100
103
  return JSON.parse(source);
101
104
  } catch (err) {
102
105
  console.error(err);
@@ -114,7 +117,7 @@ exports.readRcEnv = readRcEnv;
114
117
 
115
118
  async function writeRc(args, settings) {
116
119
  await fs.promises.writeFile(
117
- args.configFile,
120
+ path.join(args.cwd, args.configFile),
118
121
  JSON.stringify(
119
122
  {
120
123
  ...(await readRc(args)),
@@ -134,9 +137,9 @@ async function writeRcEnv(args, settings) {
134
137
 
135
138
  exports.writeRcEnv = writeRcEnv;
136
139
 
137
- function readPackageJson() {
140
+ function readPackageJson(cwd) {
138
141
  try {
139
- const packageJson = fs.readFileSync(path.join(getcwd(), 'package.json'), 'utf8');
142
+ const packageJson = fs.readFileSync(path.join(cwd, 'package.json'), 'utf8');
140
143
  return JSON.parse(packageJson);
141
144
  } catch {
142
145
  return {};