Skip to content

Commit a246b0d

Browse files
authored
Merge pull request #96 from reactioncommerce/create-local
feat: clone official api plugins command
2 parents feda1de + 8de004c commit a246b0d

File tree

7 files changed

+280
-47
lines changed

7 files changed

+280
-47
lines changed

README.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ npm install -g
6262
```
6363

6464
## Creating a project
65-
---
6665

6766
You can create your Open Commerce project by running:
6867
```
@@ -119,7 +118,7 @@ reaction develop
119118
### Congratulations!! You're ready to start developing with Open Commerce
120119

121120
## Add the Admin/Storefront
122-
---
121+
123122
Open Commerce includes an Admin panel for managing your system plus an example storefront implementation so you can see how you would go about building your own.
124123

125124
## Adding the Admin
@@ -179,8 +178,20 @@ To quickly update all your plugins:
179178
reaction update --all
180179
```
181180
Your outdated plugins will be updated to the latest version.
182-
## Other Commands
183-
---
181+
182+
## Clone plugins
183+
184+
To clone all official Open Commerce plugins locally run:
185+
```
186+
reaction clone-api-plugins
187+
```
188+
If you want to manually clone a specific plugins, use the `-m` flag:
189+
```
190+
reaction clone-api-plugins -m
191+
```
192+
193+
## Other Commands
194+
184195
- For a full list of commands run:
185196
```
186197
reaction help
@@ -200,11 +211,11 @@ cd <your-demo-name>
200211
Check that the storefront (localhost:4000), graphQL server (localhost:3000) and admin (localhost:4080) are all running (this might take a minute or so).
201212

202213
## Contribution
203-
---
214+
204215
If you find any issues please reports them [here.](https://github.com/reactioncommerce/cli/issues)
205216

206217
## Telemetry
207-
---
218+
208219
This project sends back anonymous data to our analytics provider so we can understand how users are using the product.
209220
If you want to see what data is being sent you can set the environment variable:
210221
Run:
@@ -225,7 +236,7 @@ reaction telemetry on
225236
```
226237

227238
## Developer Certificate of Origin
228-
---
239+
229240
We use the [Developer Certificate of Origin (DCO)](https://developercertificate.org/) in lieu of a Contributor License Agreement for all contributions to Reaction Commerce open source projects. We request that contributors agree to the terms of the DCO and indicate that agreement by signing all commits made to Reaction Commerce projects by adding a line with your name and email address to every Git commit message contributed:
230241

231242
```
@@ -241,7 +252,7 @@ We use the [Probot DCO GitHub app](https://github.com/apps/dco) to check for DCO
241252
If you forget to sign your commits, the DCO bot will remind you and give you detailed instructions for how to amend your commits to add a signature.
242253

243254
## License
244-
---
255+
245256
Copyright 2022
246257

247258
Licensed under the Apache License, Version 2.0 (the "License");

commands/clone-api-plugins.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { spawn } from "child_process";
2+
import fs from "fs";
3+
import inquirer from "inquirer";
4+
import isProjectOfType from "../utils/isProjectOfType.js";
5+
import Logger from "../utils/logger.js";
6+
import wget from "../utils/wget.js";
7+
8+
/**
9+
* @summary Get and parse local plugins.json file
10+
* @returns {Object} - return the local plugins.json as object
11+
*/
12+
async function getRemotePluginsJson() {
13+
const remotePackageJsonUrl = "https://raw.githubusercontent.com/reactioncommerce/reaction/trunk/plugins.json";
14+
try {
15+
const pluginsJson = await wget(remotePackageJsonUrl);
16+
17+
return JSON.parse(pluginsJson);
18+
} catch (error) {
19+
Logger.error("Unable to get local plugins.json");
20+
Logger.error(error);
21+
throw error;
22+
}
23+
}
24+
25+
/**
26+
* @summary Get the list of all upstream plugins defined in the plugins.json file of the official repo
27+
* @return {Promise<String[]>} - list of all plugins
28+
*/
29+
async function getAllPlugins() {
30+
const plugins = await getRemotePluginsJson();
31+
return Object.values(plugins)
32+
.map((plugin) => plugin.replace("@reactioncommerce/", ""));
33+
}
34+
35+
/**
36+
* @summary Creates api-plugins directory in it doesn't exist
37+
* @return {void}
38+
*/
39+
function ensureApiPluginsDirectoryExists() {
40+
const apiPluginsDir = `${process.cwd()}/api-plugins`;
41+
if (!fs.existsSync(apiPluginsDir)) {
42+
Logger.info("Creating api-plugins directory");
43+
fs.mkdirSync(apiPluginsDir);
44+
}
45+
}
46+
47+
/**
48+
* @summary Checks if plugin is already cloned
49+
* @param {String} plugin - key of the plugin
50+
* @return {boolean} - true if plugin is already cloned
51+
*/
52+
function isPluginAlreadyCloned(plugin) {
53+
const apiPluginsDir = `${process.cwd()}/api-plugins`;
54+
const pluginDir = `${apiPluginsDir}/${plugin}`;
55+
const pluginAlreadyCloned = fs.existsSync(pluginDir);
56+
if (pluginAlreadyCloned) {
57+
Logger.info(`Plugin ${plugin} already cloned`);
58+
}
59+
return pluginAlreadyCloned;
60+
}
61+
62+
/**
63+
* @summary Generate the repo url for a plugin
64+
* @param {String} plugin - key of the plugin
65+
* @return {String} - the repo url
66+
*/
67+
function buildRepoUrlForPlugin(plugin) {
68+
return `https://github.com/reactioncommerce/${plugin}.git`;
69+
}
70+
71+
/**
72+
* @summary Clones a plugin in the api-plugins directory
73+
* @param {String} plugin - key of the plugin
74+
* @return {Promise<void>} - promise that resolves when the plugin is cloned
75+
*/
76+
async function clonePlugin(plugin) {
77+
const apiPluginRepoUrl = buildRepoUrlForPlugin(plugin);
78+
79+
return new Promise((resolve, reject) => {
80+
spawn("git", ["clone", apiPluginRepoUrl], {
81+
stdio: [process.stdout, process.stderr, process.stdin],
82+
cwd: `${process.cwd()}/api-plugins`
83+
})
84+
.on("exit", (errorCode) => {
85+
if (errorCode === 0) {
86+
resolve();
87+
}
88+
reject("Error cloning plugin");
89+
});
90+
});
91+
}
92+
93+
/**
94+
* @summary Opens a prompt for manually selecting plugins to clone
95+
* @param {String[]} allPlugins - list of all plugins
96+
* @return {Promise<String[]>} - list of the selected plugins
97+
*/
98+
async function getManuallySelectedPlugins(allPlugins) {
99+
const { plugins } = await inquirer.prompt([{
100+
type: "checkbox",
101+
message: "Select the plugins you want to clone:",
102+
name: "plugins",
103+
choices: allPlugins,
104+
validate: (ans) => {
105+
if (ans.length === 0) {
106+
return "You must choose at least one plugin.";
107+
}
108+
return true;
109+
}
110+
}]);
111+
return plugins;
112+
}
113+
114+
/**
115+
* @summary Get the plugins.json file
116+
* @return {Object} - the plugins.json as an object
117+
*/
118+
function getPluginsJson() {
119+
const pluginsJson = fs.readFileSync(`${process.cwd()}/plugins.json`, "utf8");
120+
return JSON.parse(pluginsJson);
121+
}
122+
123+
/**
124+
* @summary Link a local plugin in the plugins.json file
125+
* @param {String} plugin - name of the plugin
126+
* @return {void}
127+
*/
128+
function linkLocalPlugin(plugin) {
129+
Logger.info(`Linking local plugin ${plugin} in plugins.json`);
130+
const pluginsJson = getPluginsJson();
131+
for (const key in pluginsJson) {
132+
if (pluginsJson[key] === `@reactioncommerce/${plugin}`) {
133+
pluginsJson[key] = `./api-plugins/${plugin}/index.js`;
134+
break;
135+
}
136+
}
137+
fs.writeFileSync(`${process.cwd()}/plugins.json`, JSON.stringify(pluginsJson, null, 2));
138+
}
139+
140+
/**
141+
* @summary Clone all official Open Commerce API plugins in the api project
142+
* @param {Object} options - Options for cloning api plugins command
143+
* @returns {Promise<boolean>} - return true if successful
144+
*/
145+
export default async function cloneApiPlugins({
146+
manualSelect,
147+
link
148+
}) {
149+
const isApiProject = await isProjectOfType("api");
150+
if (!isApiProject) {
151+
return false;
152+
}
153+
154+
const allPlugins = await getAllPlugins();
155+
156+
ensureApiPluginsDirectoryExists();
157+
158+
const manuallySelectedPlugins = manualSelect && await getManuallySelectedPlugins(allPlugins);
159+
160+
const pluginsToClone = manuallySelectedPlugins || allPlugins;
161+
162+
const cloneAndLinkPromises = pluginsToClone.map(async (plugin) => {
163+
if (!isPluginAlreadyCloned(plugin)) {
164+
await clonePlugin(plugin);
165+
}
166+
if (link) {
167+
linkLocalPlugin(plugin);
168+
}
169+
});
170+
171+
try {
172+
await Promise.all(cloneAndLinkPromises);
173+
} catch (error) {
174+
Logger.error(error);
175+
return false;
176+
}
177+
return true;
178+
}

commands/develop.js

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import fs from "fs";
21
import Logger from "../utils/logger.js";
32
import checkDependencies from "../utils/checkDependencies.js";
3+
import getProjectType from "../utils/getProjectType.js";
44
import developStorefront from "./develop-storefront.js";
55
import developAdmin from "./develop-admin.js";
66
import developApi from "./develop-api.js";
@@ -15,44 +15,6 @@ const extraDependencyMap = {
1515
storefront: ["yarn"]
1616
};
1717

18-
const validProjectTypes = ["api", "admin-meteor", "storefront-example"];
19-
20-
/**
21-
* @summary check if projectType exists, if not return empty string
22-
* @returns {String} - The project type
23-
*/
24-
async function getProjectType() {
25-
Logger.info("Getting project type");
26-
const packageJson = fs.readFileSync("package.json", { encoding: "utf8", flag: "r" });
27-
const packageJsonData = JSON.parse(packageJson);
28-
const { projectType } = packageJsonData;
29-
30-
if (!projectType || projectType === "") {
31-
Logger.error("No project type found in package.json");
32-
return "";
33-
}
34-
35-
if (!validProjectTypes.includes(projectType)) {
36-
Logger.error("No valid project type found in package.json");
37-
return "";
38-
}
39-
40-
switch (projectType) {
41-
case "api":
42-
Logger.info("Found project type: api");
43-
return "api";
44-
case "admin-meteor":
45-
Logger.info("Found project type: admin-meteor");
46-
return "admin";
47-
case "storefront-example":
48-
Logger.info("Found project type: storefront-example");
49-
return "storefront";
50-
default:
51-
Logger.error("No valid project type found in package.json");
52-
return "";
53-
}
54-
}
55-
5618
/**
5719
* @summary Run api in development mode
5820
* @param {String} projectType - Which project type to develop on

commands/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import develop from "./develop.js";
99
import telemetry from "./telemetry.js";
1010
import build from "./build.js";
1111
import update from "./update.js";
12+
import cloneApiPlugins from "./clone-api-plugins.js";
1213

1314
export default {
1415
demo: (demoPath) => {
@@ -32,6 +33,13 @@ export default {
3233
develop(projectType, options);
3334
track(`develop/${projectType}`, {}, options);
3435
},
36+
cloneApiPlugins: (options) => {
37+
versionSupportCheck();
38+
telemetryCheck();
39+
checkForNewVersion();
40+
cloneApiPlugins(options);
41+
track("clone-api-plugins", {}, options);
42+
},
3543
createPlugin: (type, pluginName, options) => {
3644
versionSupportCheck();
3745
telemetryCheck();

index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ const program = new commander.Command();
1212

1313
program.version(pkg.version);
1414

15+
program
16+
.command("clone-api-plugins")
17+
.description("Clone the official Open Commerce API plugins locally")
18+
.option("-m, --manual-select", "Select the specific plugins you want to clone")
19+
.option("--no-link", "Don't link the local plugins to the api project")
20+
.action((options) => {
21+
commands.cloneApiPlugins(options);
22+
});
23+
1524
program
1625
.command("create-project")
1726
.description("Create a new Open Commerce project of one of several types")

utils/getProjectType.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import fs from "fs";
2+
import Logger from "./logger.js";
3+
4+
const validProjectTypes = ["api", "admin-meteor", "storefront-example"];
5+
6+
/**
7+
* @summary check if projectType exists, if not return empty string
8+
* @returns {String} - The project type
9+
*/
10+
export default async function getProjectType() {
11+
Logger.info("Getting project type");
12+
const packageJson = fs.readFileSync("package.json", {
13+
encoding: "utf8",
14+
flag: "r"
15+
});
16+
const packageJsonData = JSON.parse(packageJson);
17+
const { projectType } = packageJsonData;
18+
19+
if (!projectType || projectType === "") {
20+
Logger.error("No project type found in package.json");
21+
return "";
22+
}
23+
24+
if (!validProjectTypes.includes(projectType)) {
25+
Logger.error("No valid project type found in package.json");
26+
return "";
27+
}
28+
29+
switch (projectType) {
30+
case "api":
31+
Logger.info("Found project type: api");
32+
return "api";
33+
case "admin-meteor":
34+
Logger.info("Found project type: admin-meteor");
35+
return "admin";
36+
case "storefront-example":
37+
Logger.info("Found project type: storefront-example");
38+
return "storefront";
39+
default:
40+
Logger.error("No valid project type found in package.json");
41+
return "";
42+
}
43+
}

0 commit comments

Comments
 (0)