Skip to content

Commit 26c1dd1

Browse files
authored
Merge pull request #9 from NMFS-RADFish/radfish-6-download-zip
Rework CLI to download latest boilerplate release
2 parents 4d1dd11 + a550cb8 commit 26c1dd1

File tree

9 files changed

+345
-251
lines changed

9 files changed

+345
-251
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Test fixtures
2+
__tests__/fixtures/output
3+
14
# Logs
25
logs
36
*.log

README.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@
44

55
![radfish_logo](https://github.com/NMFS-RADFish/boilerplate/assets/11274285/f0c1f78d-d2bd-4590-897c-c6ec87522dd1)
66

7-
# USAGE
7+
# System Requirements
8+
- Node.js v18+
9+
- `tar` v3.3+
810

9-
The cli supports executing the program via cli with or without arguments.
11+
# Creating a project
1012

11-
The program also supports only adding certain arguments. In this case, the program will recognize which arguments are missing, and prompt the user to add the rest of them via cli.
13+
```
14+
npx @nmfs-radfish/create-radfish-app my-pwa
15+
```
1216

13-
In the project root:
14-
15-
`npx create-radfish-app` and the program will prompt you to add in the required fields (region and type)
16-
17-
Or
18-
19-
`npx create-radfish-app --region=alaska --type=evtr`
17+
Provide the cli the name of your new project. A new project will be created using the [default boilerplate template](https://github.com/NMFS-RADFish/boilerplate).

__tests__/download.spec.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
const path = require("path");
2+
const https = require("https");
3+
const fs = require("fs");
4+
const child_process = require("child_process");
5+
6+
jest.mock("https");
7+
8+
describe("downloadFile", () => {
9+
it("should download a file", (done) => {
10+
https.get.mockImplementationOnce((url, callback) => {
11+
const response = {
12+
statusCode: 200,
13+
pipe: jest.fn(),
14+
};
15+
callback(response);
16+
return response;
17+
});
18+
jest.mock("fs");
19+
const fs = require("fs");
20+
fs.createWriteStream.mockReturnValueOnce({
21+
on: jest.fn((event, callback) => {
22+
callback(null);
23+
}),
24+
close: jest.fn((callback) => {
25+
callback(null);
26+
}),
27+
});
28+
const download = require("../lib/download");
29+
30+
download.downloadFile("https://example.com", "download/path", (err, res) => {
31+
try {
32+
expect(fs.createWriteStream).toHaveBeenCalledWith(expect.stringMatching("download/path"));
33+
expect(https.get).toHaveBeenCalledWith("https://example.com", expect.any(Function));
34+
35+
done();
36+
} catch (err) {
37+
done(err);
38+
}
39+
});
40+
});
41+
});
42+
43+
describe("unzip", () => {
44+
beforeEach(() => {
45+
jest.resetModules();
46+
fs.rmSync(path.resolve(__dirname, "fixtures", "output"), { recursive: true, force: true });
47+
});
48+
49+
it("should correctly pass arguments when spawning the tar command process", (done) => {
50+
jest.doMock("child_process", () => ({
51+
exec: jest.fn((command, options, callback) => {
52+
callback(null, "stdout", "stderr");
53+
}),
54+
}));
55+
const child_process = require("child_process");
56+
const download = require("../lib/download");
57+
download.unzip("filepath", () => {
58+
try {
59+
expect(child_process.exec).toHaveBeenCalledWith(
60+
`tar -xf filepath --exclude .github`,
61+
{
62+
cwd: process.cwd(),
63+
},
64+
expect.any(Function),
65+
);
66+
done();
67+
} catch (err) {
68+
done(err);
69+
}
70+
});
71+
});
72+
73+
it("should ignore the .github folder", (done) => {
74+
jest.unmock("child_process");
75+
jest.unmock("fs");
76+
const child_process = require("child_process");
77+
const fs = require("fs");
78+
const download = require("../lib/download");
79+
const zippath = path.resolve(__dirname, "fixtures", "output.tar.gz");
80+
81+
download.unzip(zippath, (err) => {
82+
try {
83+
expect(err).toBeNull();
84+
fs.readdir(path.resolve(__dirname, "fixtures", "output"), (err, files) => {
85+
expect(files).not.toContain(".github");
86+
done();
87+
});
88+
} catch (err) {
89+
done(err);
90+
}
91+
});
92+
});
93+
});

config.js

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,5 @@
11
import { Separator } from "@inquirer/select";
22

3-
const regionConfig = [
4-
{
5-
value: "Alaska",
6-
code: "alaska",
7-
description:
8-
"Extending to the Arctic, Alaska’s culturally-diverse people, infrastructure, economy, and ecosystems are already experiencing the effects of climate change. Obtaining a better understanding of these early impacts will provide an integration of science and decision-making for adaptation on a global scale.",
9-
},
10-
{
11-
value: "Central",
12-
code: "central",
13-
description:
14-
"NOAA’s Central region includes the “bread basket” of the Nation. A significant portion of the Nation’s agriculture, particularly wheat and corn, comes from this region. In addition to agriculture, an integrated advanced technology corridor stretches along the Front Range of the Rockies, with assets and commercial interests in climate research and space environment.)",
15-
},
16-
{
17-
value: "Great Lakes",
18-
code: "greatlakes",
19-
description:
20-
"While the Great Lakes region has been a leader for innovative science and advances in natural resources management, there are still significant gaps in knowledge about ecological processes and key indicators of ecosystem health. The Great Lakes face new and emerging problems due to the effects of climate change, including potentially changing long-term water levels and the timing and duration of weather events.",
21-
},
22-
{
23-
value: "Gulf of Mexico",
24-
code: "gulfofmexico",
25-
description:
26-
"The Gulf of Mexico provides the Nation with valuable energy resources, tasty seafood, extraordinary beaches and leisure activities, and a rich cultural heritage. It is also home to some of the most devastating weather in the Nation, including the most costly natural disaster in U.S. history.",
27-
},
28-
{
29-
value: "North Atlantic",
30-
code: "northatlantic",
31-
description:
32-
"NOAA's North Atlantic region extends from Maine to Virginia and is rich with history, culture, and economic opportunities. It is home to more than 70 million people, more than 80 percent of which live in the region's 180 coastal counties.",
33-
},
34-
{
35-
value: "Pacific",
36-
code: "pacific",
37-
description:
38-
"NOAA's North Atlantic region extends from Maine to Virginia and is rich with history, culture, and economic opportunities. It is home to more than 70 million people, more than 80 percent of which live in the region's 180 coastal counties.",
39-
},
40-
{
41-
value: "Southeast & Caribbean",
42-
code: "southwest",
43-
description:
44-
"The Southeast and Caribbean region is one of the fastest growing in the US, with a rapidly transforming economic base. Increasing population, particularly along coasts, drives a strong demand for ecosystem services, and puts more people at risk to hazards and changing climate.",
45-
},
46-
{
47-
value: "Western Region",
48-
description:
49-
"In many regards, the West is still “wild” with over 753 million acres of land held by the Federal government in public trust, much of which is managed as national parks and forests. From the deserts to the coastal temperate rain forests, the West is characterized by numerous distinct and complex terrestrial and marine ecosystems.",
50-
},
51-
];
52-
533
const applicationTypes = [
544
{
555
name: "Electronic Vessel Trip Reporting (eVTR)",
@@ -65,4 +15,4 @@ const applicationTypes = [
6515
},
6616
];
6717

68-
export { regionConfig, applicationTypes };
18+
export { applicationTypes };

index.js

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
#!/usr/bin/env node
2-
import { execSync } from "child_process";
3-
import { confirm } from "@inquirer/prompts";
4-
import select from "@inquirer/select";
5-
import { Command } from "commander";
6-
import ora from "ora";
7-
import { validateRegion } from "./validators.js";
8-
import { regionConfig } from "./config.js";
9-
import path from "path";
2+
const { execSync } = require("child_process");
3+
const { confirm } = require("@inquirer/prompts");
4+
const { Command } = require("commander");
5+
const ora = require("ora");
6+
const path = require("path");
7+
const { downloadFile, unzip } = require("./lib/download.js");
8+
const fs = require("fs");
109

1110
const program = new Command();
1211

@@ -18,54 +17,74 @@ program
1817
// program options
1918
program.argument("<projectDirectoryPath>").option("-r --region <string>", "specified region");
2019

21-
program.action((projectDirectoryPath, options) => {
22-
const isValidRegion = validateRegion(options.region);
23-
24-
if (!isValidRegion) {
25-
const regionCodes = regionConfig
26-
.map((region) => region.code)
27-
.join(" , ")
28-
.replace(/, $/, ""); // remove comma from last elem
29-
console.error("Invalid region code. Here are the valid regions: ", regionCodes);
30-
}
31-
20+
program.action((projectDirectoryPath) => {
3221
scaffoldRadFishApp(projectDirectoryPath);
3322
});
3423

35-
// check options passed in via cli command
36-
const options = program.opts();
37-
3824
async function scaffoldRadFishApp(projectDirectoryPath) {
3925
const targetDirectory = path.resolve(
4026
process.cwd(),
4127
`${projectDirectoryPath.trim().replace(/\s+/g, "-")}`, // replace whitespaces in the filepath
4228
);
4329

44-
async function defineRegion() {
45-
return await select({
46-
name: "region",
47-
message: "Which NOAA region will you be building your app for?",
48-
choices: regionConfig,
49-
});
50-
}
51-
52-
async function confirmConfiguration(region) {
30+
async function confirmConfiguration() {
5331
return await confirm({
54-
message: `You are about to scaffold an application for the region of ${region} in the following project directory: ${targetDirectory}
32+
message: `You are about to scaffold an application in the following project directory: ${targetDirectory}
5533
Okay to proceed?`,
5634
});
5735
}
5836

59-
// this will clone the radfish app boilerplate and spin it up
60-
function bootstrapApp() {
61-
const repoUrl = "git@github.com:NMFS-RADFish/boilerplate.git"; // via ssh each user/developer will need to have ssh keypair setup in github org
37+
async function bootstrapApp() {
6238
const spinner = ora("Setting up application").start();
6339

64-
// Clone the repository
6540
try {
66-
execSync(`git clone ${repoUrl} ${targetDirectory}`);
67-
console.log(`Repository cloned successfully.`);
41+
await new Promise((resolve, reject) => {
42+
downloadFile(
43+
"https://github.com/NMFS-RADFish/boilerplate/archive/refs/tags/latest.tar.gz",
44+
"boilerplate.tar.gz",
45+
(err, res) => {
46+
if (err) {
47+
return reject(err);
48+
}
49+
resolve(res);
50+
},
51+
);
52+
});
53+
54+
await new Promise((resolve, reject) => {
55+
unzip("boilerplate.tar.gz", (err, res) => {
56+
if (err) {
57+
return reject(err);
58+
}
59+
resolve(res);
60+
});
61+
});
62+
63+
await new Promise((resolve, reject) => {
64+
fs.rename(
65+
path.resolve(process.cwd(), "boilerplate-latest"),
66+
path.resolve(process.cwd(), targetDirectory),
67+
(err, res) => {
68+
if (err) {
69+
return reject(err);
70+
}
71+
resolve(res);
72+
},
73+
);
74+
});
75+
76+
await new Promise((resolve, reject) => {
77+
fs.rm(path.resolve(process.cwd(), "boilerplate.tar.gz"), (err, res) => {
78+
if (err) {
79+
return reject(err);
80+
}
81+
resolve(res);
82+
});
83+
});
84+
85+
console.log(`Project successfully created.`);
6886
} catch (error) {
87+
console.log(error);
6988
console.error(`Error cloning repository: ${error.message}`);
7089
process.exit(1);
7190
}
@@ -94,9 +113,7 @@ async function scaffoldRadFishApp(projectDirectoryPath) {
94113
spinner.stop();
95114
}
96115

97-
const region = options.region ? options.region : await defineRegion();
98-
99-
const confirmation = await confirmConfiguration(region);
116+
const confirmation = await confirmConfiguration();
100117

101118
if (confirmation) {
102119
await bootstrapApp();

lib/download.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const child_process = require("child_process");
2+
const fs = require("fs");
3+
const https = require("https");
4+
const path = require("path");
5+
6+
/**
7+
*
8+
* @param {string} url - the url to download
9+
* @param {string} outputFilepath - the name of the output file
10+
* @param {Function<Error?, response>} callback - callback function
11+
* @returns void
12+
*/
13+
module.exports.downloadFile = async function (
14+
url,
15+
outputFilepath = "download",
16+
callback = () => {},
17+
) {
18+
https.get(url, function (response) {
19+
if (response.statusCode >= 400) {
20+
return callback(new Error("Failed to download file"), null);
21+
}
22+
23+
if (response.statusCode === 302) {
24+
return module.exports.downloadFile(response.headers.location, outputFilepath, callback);
25+
}
26+
27+
if (response.statusCode === 200) {
28+
const file = fs.createWriteStream(path.resolve(process.cwd(), outputFilepath));
29+
response.pipe(file);
30+
file.on("finish", function () {
31+
file.close(callback);
32+
});
33+
}
34+
});
35+
};
36+
37+
/**
38+
* Expands a zip file into a directory
39+
* @param {string} filepath
40+
* @param {string} destinationPath
41+
* @param {Function<Error?, response>} callback
42+
*/
43+
module.exports.unzip = async (filepath, callback = () => {}) => {
44+
const filepathDir = path.resolve(path.dirname(filepath));
45+
child_process.exec(
46+
`tar -xf ${filepath} --exclude .github`,
47+
{ cwd: filepathDir },
48+
(err, stdout, stderr) => {
49+
if (err) {
50+
return callback(err, stderr);
51+
}
52+
return callback(null, stdout);
53+
},
54+
);
55+
};

0 commit comments

Comments
 (0)