Comparing version 1.11.3 to 2.0.0
55
index.js
@@ -1,6 +0,13 @@ | ||
const program = require('commander'); | ||
const Configstore = require('configstore'); | ||
const program = require("commander"); | ||
const Configstore = require("configstore"); | ||
const { clean, build, deploy, backup, updateFramework, create } = require('./utils/commands'); | ||
const { version, name } = require('./package.json'); | ||
const { | ||
clean, | ||
build, | ||
deploy, | ||
backup, | ||
updateFramework, | ||
create, | ||
} = require("./utils/commands"); | ||
const { version, name } = require("./package.json"); | ||
@@ -10,42 +17,32 @@ log = console.log; | ||
remakeCliConfig = new Configstore(name, { user: {} }); | ||
remakeServiceHost = 'https://remakeapps.com'; | ||
remakeServiceHost = "https://remakeapps.com"; | ||
program | ||
.version('v' + version, '-v, --version', 'output the current version') | ||
.name('remake') | ||
.usage('command [--help]') | ||
.version("v" + version, "-v, --version", "output the current version") | ||
.name("remake") | ||
.usage("command [--help]"); | ||
program | ||
.command('create <projectDir>') | ||
.description('Create a new Remake project') | ||
.option('-m, --multitenant', 'create a multi tenant Remake app') | ||
.command("create <projectDir>") | ||
.description("Create a new Remake project") | ||
.option("-m, --multitenant", "create a multi tenant Remake app") | ||
.action((projectDir, options) => create(projectDir, options)); | ||
program | ||
.command('update-framework') | ||
.description('Update the Remake framework in your project') | ||
.command("update-framework") | ||
.description("Update the Remake framework in your project") | ||
.action(() => updateFramework()); | ||
program | ||
.command('deploy') | ||
.description('Deploy your Remake app on the Remake server') | ||
.command("deploy") | ||
.description("Deploy your Remake app on the Remake server") | ||
.action(() => deploy()); | ||
program | ||
.command('build') | ||
.description('Build your Remake app') | ||
.action(() => build()); | ||
program | ||
.command('clean') | ||
.description('Wipe the local Remake environment including caches and build assets') | ||
.action(() => clean()); | ||
program | ||
.command('backup') | ||
.description('Backup the deployed version of your app') | ||
.command("backup") | ||
.description("Backup the deployed version of your app") | ||
.action(() => backup()); | ||
module.exports = async () => { | ||
program.parse(process.argv) | ||
} | ||
program.parse(process.argv); | ||
}; |
{ | ||
"name": "remake", | ||
"version": "1.11.3", | ||
"version": "2.0.0", | ||
"description": "Generate a full-stack Remake web app", | ||
@@ -35,2 +35,3 @@ "license": "MIT", | ||
"form-data": "^2.5.1", | ||
"husky": "^4.3.0", | ||
"inquirer": "^7.0.0", | ||
@@ -40,5 +41,14 @@ "nanoid": "^2.0.3", | ||
"ora": "^4.0.2", | ||
"replace": "^1.2.0", | ||
"rimraf": "^3.0.0", | ||
"shelljs": "^0.8.4" | ||
}, | ||
"devDependencies": { | ||
"prettier": "2.1.2" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "prettier --check ." | ||
} | ||
} | ||
} |
222
README.md
@@ -1,190 +0,110 @@ | ||
<h1>Remake</h1> | ||
<p align="center"> | ||
<a href="https://storybook.js.org/"> | ||
<img src="https://user-images.githubusercontent.com/364330/98124113-bc603180-1e80-11eb-882e-e2246940c7a4.png" alt="Remake" width="400" /> | ||
</a> | ||
</p> | ||
### An easy-to-use web app framework | ||
<p align="center">Build full-stack web apps with only HTML and CSS</p> | ||
* **Create editable pages using only HTML templates** | ||
* **Easy to learn, powerful data attribute syntax** | ||
* **Perfect for rapidly prototyping ideas** | ||
<br/> | ||
<p align="center"> | ||
<a href="https://github.com/remake/remake-cli/blob/master/LICENSE"> | ||
<img src="https://img.shields.io/github/license/remake/remake-cli" alt="License" /> | ||
</a> | ||
<a href="https://discord.gg/FB3gNxw"> | ||
<img src="https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat" alt="Discord Channel" /> | ||
</a> | ||
<a href="https://github.com/sponsors/remake"> | ||
<img src="https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=<url>" alt="Sponsor" /> | ||
</a> | ||
<a href="https://twitter.com/intent/follow?screen_name=remaketheweb"> | ||
<img src="https://badgen.net/twitter/follow/remaketheweb?icon=twitter&label=%40remake" alt="Remake Twitter" /> | ||
</a> | ||
</p> | ||
## Get started | ||
Remake is a simple, open source framework. It lets you transform a static website into an interactive, editable web app with a few custom HTML attributes. | ||
1. Install the command line tool | ||
- Simple syntax | ||
- User accounts & persistent data | ||
- Inline editing & file uploads | ||
- No backend coding | ||
``` | ||
npm install remake -g | ||
``` | ||
<b><a href="https://blog.remaketheweb.com/intro-to-remake-part-1-make-web-apps-with-html/">→ Learn more about Remake</a></b> | ||
2. Create a new project | ||
## Why Remake? | ||
``` | ||
remake create <project-dir> | ||
``` | ||
> What if every HTML webpage knew how to save, edit, and add new items to itself? | ||
3. Run the development server | ||
![Diagram of how Remake works](https://user-images.githubusercontent.com/364330/98125645-b5d2b980-1e82-11eb-909f-527bf0ff224e.png) | ||
``` | ||
cd <project-dir> | ||
npm run dev | ||
``` | ||
Remake is ideal for indie hackers who want to build editable web apps quickly. Have you ever created a static website and wished people could just start using it? Remake lets you do that. | ||
4. Start building a web app with Remake! | ||
- **Remake lets you build full-stack apps with front-end code.** Remake comes with user accounts, a persistent database, and everything you need to deploy a working application. | ||
- **Remake lets you build CMS-like features on top of a static template.** Users can login to your site and edit their own copy of it. | ||
- **Remake is so easy to use it feels like prototyping.** But it's designed for building scalable, production web apps. | ||
- **Remake gives you control over your design.** You can use any CSS framework and style your pages however you want. | ||
- **Remake is server rendered.** This makes it ideal for SEO and loading pages quickly. The front-end framework isn't even loaded if a user can't edit the current page. | ||
<b><a href="#">→ Learn what makes Remake different</a></b> (coming later today) | ||
## Example | ||
## Get started | ||
![Todos example app](https://remake-website.s3.amazonaws.com/todos-example.gif) | ||
**1. Install [Node.js (12.16+)](https://nodejs.org/)** | ||
*An example of editing, adding, and removing items with the starter todos project.* | ||
**2. Create a project using the Remake CLI** | ||
## What is Remake? | ||
**Remake is everything you need to create a web app in record time, with very little overhead.** | ||
Remake provides all the tools you need to: | ||
* Instantly add [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) to your page | ||
* Have all your pages' data auto-save after it's been edited | ||
* Store your data in a format that's easy to access and understand | ||
Out of the box, Remake comes with: | ||
* **User accounts:** People can sign up and log in to your app! | ||
* **Flat file database:** No need to install, configure, or host a database | ||
* **Simple data handling:** Accessing and saving nested objects is easy | ||
If you know how to build a website with HTML and CSS — and know the basics of [Handlebars](https://handlebarsjs.com/) templating — you can build a full web app with Remake! | ||
## Quickstart resources | ||
* [A simple todo list app in 12 lines of HTML](https://docs.remaketheweb.com/a-simple-example-app/) | ||
* [30 minute tutorial on how to build a Trello clone](https://tutorials.remaketheweb.com/) | ||
## How does it work? | ||
Remake is based around a simple idea: | ||
**What if every HTML webpage knew how to save, edit, and add new items to itself?** | ||
* Remake uses simple data attributes, starting with `data-o` for attributes that output data and starting with `data-i` for attributes that input data | ||
* Using a simple data attribute (`data-i-editable`), you can make the data on an element editable and have it auto-save to the database | ||
* Using another simple data attribute (`data-i-new`), you can easily render back-end partial templates and add them to the page | ||
## Get to know how the data works | ||
#### Saving Data | ||
HTML is formatted like a tree 🌳 in that it has a root node and every other element on a page branches off of that root node. | ||
So, what if we were able to transform HTML into an **object** that we could save to a database just by looking at its natural tree structure? | ||
We can do this in Remake by tagging elements with data. **Remake will parse and save this data automatically** whenever it changes. | ||
Here's how it works in Remake: | ||
```html | ||
<div data-o-type="object"></div> | ||
``` | ||
This element has been tagged as an `Object`, which means Remake will convert it into this: | ||
```javascript | ||
{} | ||
npx remake create my-app | ||
``` | ||
Let's go through a few more examples: | ||
**3. Run the project** | ||
##### 1. Key/value pairs | ||
```html | ||
<div data-o-type="object" data-o-key-name="David"></div> | ||
``` | ||
This will be converted into an object with a key/value pair inside of it: | ||
```javascript | ||
{name: "David"} | ||
cd my-app | ||
npm run dev | ||
``` | ||
The first attribute (`data-o-type`) tells us which data type to expect. It can be set to *only* `object` or `list`. | ||
You now have an app running at `http://localhost:3000`. Your app's code is in the `/app` directory and your database is in the `/_remake-data` directory. | ||
The second attribute (`data-o-key-name`) tells us that this `object` has a key of `name` (the key is always the part that comes after `data-o-key-`). And we look at the attribute's value to get the key's value. | ||
<b><a href="https://docs.remaketheweb.com/introducing-remake/">→ Start learning how to build a web app with Remake</a></b> | ||
##### 2. Nested data | ||
## What can you build? | ||
```html | ||
<div data-o-type="object"> | ||
<div data-o-type="object" data-o-key="person" data-o-key-name="David"> | ||
</div> | ||
</div> | ||
``` | ||
Remake is **great at building page builders,** where each user can edit their own content. | ||
This example is a bit more advanced, as it relies on **nested** elements to create **nested** data: | ||
- **[Todo app](https://docs.remaketheweb.com/a-simple-example-app/)** (Build time: 7 min) | ||
- **[Trello clone](https://tutorials.remaketheweb.com/)** (Build time: 30 min) | ||
- (In progress) **[Resume builder](https://resume-builder-remake.netlify.app/)** (Build time: 30 min) | ||
- (In progress) [**Reading list builder**](https://shelfpage.remakeapps.com/) (Build time: 30 min) | ||
- **Landing page builder** (planned) | ||
- **Directory website builder** (planned) | ||
```javascript | ||
{person: {name: "David"}} | ||
``` | ||
<b><a href="https://blog.remaketheweb.com/intro-to-remake-part-2-what-you-can-and-cant-build/">→ Learn what else you can build with Remake</a></b> | ||
In this example, we use the `data-o-key` attribute — with nothing after it — to create an object inside of an object. The value of `data-o-key` tells us which key the nested object will be. | ||
![Trello clone built with Remake](https://user-images.githubusercontent.com/364330/98126081-2f6aa780-1e83-11eb-8367-e582daaf8997.png) | ||
#### 3. Lists/Arrays of objects | ||
<p align="center">An example app built with Remake in 30 minutes</p> | ||
Let's look at the only other data type that Remake supports: `Arrays`. In Remake, we use the term `list`. | ||
## Remake’s Mission | ||
How do we create a list in Remake? | ||
Remake aims to equalize power on the internet. A few companies own the platforms where the rest of us publish posts and websites — but owning a platform is usually beyond our reach. Remake switches this narrative and empowers you to build your own publishing platform. | ||
```html | ||
<div data-o-type="list"></div> | ||
``` | ||
<b><a href="https://discord.gg/FB3gNxw">→ Join and contribute to our Discord community</a></b> | ||
This is a pretty simple example and will compile into just a simple, empty array: | ||
## Contributing | ||
``` | ||
[] | ||
``` | ||
Remake is an open-source, and contributions are always welcome. If you identify with Remake's mission, we'd be delighted to have you on board! | ||
How would we go about adding objects to this array? We'd just nest them of course! | ||
- Report bugs | ||
- Suggest features | ||
- Fix issues | ||
- Improve documentation | ||
- Make and share tutorials | ||
```html | ||
<div data-o-type="list"> | ||
<div data-o-type="object" data-o-key-name="David"> | ||
<div data-o-type="object" data-o-key-name="John"> | ||
<div data-o-type="object" data-o-key-name="Mary"> | ||
</div> | ||
``` | ||
<b><a href="https://github.com/remake/remake-cli/issues/new?assignees=&labels=&template=feature_request.md&title=My%20first%20issue">→ Start by creating your first issue</a></b> | ||
When Remake looks at this, all it sees is: | ||
## Our Contributors | ||
```javascript | ||
[ | ||
{name: "David"}, | ||
{name: "John"}, | ||
{name: "Mary"} | ||
] | ||
``` | ||
## Stay in the loop | ||
Sign up for [the newsletter](https://form.remaketheweb.com/) to get updates as this framework develops | ||
## Find out more | ||
* [Contact the author on Twitter](https://twitter.com/panphora) | ||
* [View the public roadmap](https://trello.com/b/BXvugSjT/remake) | ||
* [View a live production app: RequestCreative](https://requestcreative.com) | ||
## Contributors | ||
* **[Andrew de Jong](https://gitlab.com/android4682)** | ||
- [Andrew de Jong](https://gitlab.com/android4682) | ||
- [Painatalman](https://github.com/Painatalman) |
@@ -1,13 +0,18 @@ | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const chalk = require('chalk'); | ||
const shell = require('shelljs'); | ||
const inquirer = require('inquirer'); | ||
const { promisify } = require('es6-promisify'); | ||
const rimraf = promisify(require('rimraf')); | ||
const ora = require('ora'); | ||
const process = require('process'); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const chalk = require("chalk"); | ||
const shell = require("shelljs"); | ||
const inquirer = require("inquirer"); | ||
const { promisify } = require("es6-promisify"); | ||
const rimraf = promisify(require("rimraf")); | ||
const ora = require("ora"); | ||
const process = require("process"); | ||
const replace = require("replace"); | ||
const { readDotRemake, writeDotRemake, generateDotRemakeContent } = require('./dot-remake'); | ||
const { | ||
readDotRemake, | ||
writeDotRemake, | ||
generateDotRemakeContent, | ||
} = require("./dot-remake"); | ||
const { | ||
registerUser, | ||
@@ -20,14 +25,14 @@ checkSubdomain, | ||
getAppsList, | ||
backupApp } = require('./helpers'); | ||
const { questions } = require('./inquirer-questions'); | ||
const { showSuccessfulCreationMessage } = require('./messages'); | ||
backupApp, | ||
} = require("./helpers"); | ||
const { questions } = require("./inquirer-questions"); | ||
const { showSuccessfulCreationMessage } = require("./messages"); | ||
const { exit } = require("process"); | ||
let spinner = null; | ||
const create = async (projectDir, options) => { | ||
const cwd = process.cwd(); | ||
const newProjectDirPath = path.join(cwd, projectDir); | ||
let rimrafError = null; | ||
const create = async (projectName, options) => { | ||
const projectPath = getProjectPath(projectName); | ||
if (fs.existsSync(newProjectDirPath)) { | ||
if (fs.existsSync(projectPath)) { | ||
log(chalk.bgRed("Error: Cannot write to a directory that already exists.")); | ||
@@ -37,51 +42,23 @@ process.exit(); | ||
// STEP 1 | ||
spinner = ora("Creating new project.").start(); | ||
shell.exec(`git clone --depth 1 https://github.com/remake/remake-framework.git ${projectDir}`, { silent: true }); | ||
spinner.succeed(); | ||
cloneRemakeFramework(projectName); | ||
await removeDotGit(projectName); | ||
cleanPackageJson(projectName); | ||
moveReadme(); | ||
await setupTemplate(); | ||
installNpmPackages(); | ||
createDotRemakeFile(projectName, options); | ||
initializeGitRepo(); | ||
process.exit(0); | ||
}; | ||
// STEP 2a & 2b | ||
spinner = ora("Tidy up new project directory.").start(); | ||
rimrafError = await rimraf(path.join(newProjectDirPath, ".git")); | ||
if (rimrafError) { | ||
spinner.fail(chalk.bgRed("Error: Couldn't remove old .git directory from new project.")); | ||
process.exit(); | ||
} | ||
spinner.succeed(); | ||
// put project README in the right place | ||
shell.mv(path.join(newProjectDirPath, "README-FOR-BUNDLE.md"), path.join(newProjectDirPath, "README.md")); | ||
// STEP 3 | ||
spinner = ora("Installing npm dependencies.").start(); | ||
shell.cd(newProjectDirPath); | ||
shell.exec("npm install", { silent: true }); | ||
spinner.succeed(); | ||
// STEP 4 | ||
// write project name and env variables to .remake file | ||
spinner = ora("Setting up .remake").start(); | ||
const dotRemakeObj = { | ||
...generateDotRemakeContent(options.multitenant) | ||
} | ||
spinner.succeed(); | ||
const dotRemakeReady = writeDotRemake(dotRemakeObj); | ||
if (dotRemakeReady) { | ||
showSuccessfulCreationMessage(projectDir); | ||
} | ||
} | ||
const clean = () => { | ||
let dotRemakeObj = readDotRemake(); | ||
if (!dotRemakeObj) { | ||
log(chalk.bgRed('You are not in the root directory of a remake project.')); | ||
log(chalk.bgRed("You are not in the root directory of a remake project.")); | ||
process.exit(); | ||
} | ||
spinner = ora('Cleaning project.').start(); | ||
shell.exec('npm run clean', { silent:true }); | ||
spinner = ora("Cleaning project.").start(); | ||
shell.exec("npm run clean", { silent: true }); | ||
spinner.succeed(); | ||
} | ||
}; | ||
@@ -91,7 +68,7 @@ const build = () => { | ||
if (!dotRemakeObj) { | ||
log(chalk.bgRed('You are not in the root directory of a remake project.')); | ||
log(chalk.bgRed("You are not in the root directory of a remake project.")); | ||
process.exit(); | ||
} | ||
spinner = ora('Building project.').start(); | ||
const result = shell.exec('npm run build', { silent: true }); | ||
spinner = ora("Building project.").start(); | ||
const result = shell.exec("npm run build", { silent: true }); | ||
if (result.code === 0) { | ||
@@ -102,11 +79,8 @@ spinner.succeed(); | ||
} | ||
} | ||
}; | ||
const deploy = async () => { | ||
clean(); | ||
build(); | ||
let dotRemakeObj = readDotRemake(); | ||
if (!dotRemakeObj) { | ||
log(chalk.bgRed('You are not in the root directory of a remake project.')); | ||
log(chalk.bgRed("You are not in the root directory of a remake project.")); | ||
process.exit(); | ||
@@ -118,16 +92,24 @@ } | ||
const subdomainAnswer = await inquirer.prompt([questions.INPUT_SUBDOMAIN]); | ||
spinner = ora(`Checking if ${subdomainAnswer.subdomain}.remakeapps.com is available`).start(); | ||
spinner = ora( | ||
`Checking if ${subdomainAnswer.subdomain}.remakeapps.com is available` | ||
).start(); | ||
// check if name is available | ||
const isSubdomainAvailable = await checkSubdomain(subdomainAnswer.subdomain); | ||
const isSubdomainAvailable = await checkSubdomain( | ||
subdomainAnswer.subdomain | ||
); | ||
if (!isSubdomainAvailable) { | ||
spinner.fail(`${subdomainAnswer.subdomain}.remakeapps.com is not available`); | ||
spinner.fail( | ||
`${subdomainAnswer.subdomain}.remakeapps.com is not available` | ||
); | ||
process.exit(); | ||
} | ||
spinner.succeed(`${subdomainAnswer.subdomain}.remakeapps.com is available`); | ||
// prompt yes to confirm | ||
const confirmSubdomainAnswer = await inquirer.prompt([questions.CONFIRM_SUBDOMAIN]); | ||
const confirmSubdomainAnswer = await inquirer.prompt([ | ||
questions.CONFIRM_SUBDOMAIN, | ||
]); | ||
if (confirmSubdomainAnswer.deployOk === false) { | ||
log(chalk.bgRed('Stopped deployment.')); | ||
log(chalk.bgRed("Stopped deployment.")); | ||
process.exit(); | ||
@@ -137,14 +119,18 @@ } | ||
spinner = ora(`Registering ${subdomainAnswer.subdomain}`).start(); | ||
const subdomainRegistered = await registerSubdomain(subdomainAnswer.subdomain); | ||
const subdomainRegistered = await registerSubdomain( | ||
subdomainAnswer.subdomain | ||
); | ||
if (!subdomainRegistered.success) { | ||
spinner.fail(subdomainRegistered.message) | ||
spinner.fail(subdomainRegistered.message); | ||
process.exit(); | ||
} | ||
spinner.succeed(`${subdomainAnswer.subdomain}.remakeapps.com is belonging to your app.`); | ||
spinner.succeed( | ||
`${subdomainAnswer.subdomain}.remakeapps.com is belonging to your app.` | ||
); | ||
spinner = ora(`Writing .remake file.`).start(); | ||
dotRemakeObj.projectName = subdomainAnswer.subdomain | ||
const writtenDotRemake = writeDotRemake(dotRemakeObj) | ||
dotRemakeObj.projectName = subdomainAnswer.subdomain; | ||
const writtenDotRemake = writeDotRemake(dotRemakeObj); | ||
if (!writtenDotRemake) { | ||
spinner.fail('Could not write subdomain to .remake'); | ||
spinner.fail("Could not write subdomain to .remake"); | ||
process.exit(); | ||
@@ -159,3 +145,7 @@ } | ||
removeDeploymentZip(dotRemakeObj.projectName); | ||
log(chalk.greenBright(`The app is accessible at the URL: https://${dotRemakeObj.projectName}.remakeapps.com`)) | ||
log( | ||
chalk.greenBright( | ||
`The app is accessible at the URL: https://${dotRemakeObj.projectName}.remakeapps.com` | ||
) | ||
); | ||
process.exit(); | ||
@@ -166,3 +156,3 @@ } catch (err) { | ||
} | ||
} | ||
}; | ||
@@ -173,4 +163,4 @@ const updateFramework = async () => { | ||
const remakeFrameworkPathInApplicationDirectory = path.join(cwd, "_remake"); | ||
log (remakeFrameworkPathInApplicationDirectory); | ||
log(remakeFrameworkPathInApplicationDirectory); | ||
// 1. CHECK IF _remake DIRECTORY EXISTS | ||
@@ -194,11 +184,19 @@ if (!fs.existsSync(remakeFrameworkPathInApplicationDirectory)) { | ||
spinner = ora("Copying latest framework into _remake directory.").start(); | ||
shell.exec("git clone --depth 1 https://github.com/remake/remake-framework.git", { silent: true }); | ||
shell.exec( | ||
"git clone --depth 1 https://github.com/remake/remake-framework.git", | ||
{ silent: true } | ||
); | ||
// 4. MOVE THE _remake DIRECTORY TO WHERE THE OLD _remake DIRECTORY WAS | ||
shell.mv(path.join(cwd, "remake-framework/_remake"), remakeFrameworkPathInApplicationDirectory); | ||
shell.mv( | ||
path.join(cwd, "remake-framework/_remake"), | ||
remakeFrameworkPathInApplicationDirectory | ||
); | ||
rimrafError = await rimraf(path.join(cwd, "remake-framework")) | ||
rimrafError = await rimraf(path.join(cwd, "remake-framework")); | ||
if (rimrafError) { | ||
spinner.fail("Error cleaning up: Couldn't remove the ./remake-framework directory."); | ||
spinner.fail( | ||
"Error cleaning up: Couldn't remove the ./remake-framework directory." | ||
); | ||
return; | ||
@@ -208,4 +206,4 @@ } | ||
log(chalk.greenBright('Framework successfully updated.')) | ||
} | ||
log(chalk.greenBright("Framework successfully updated.")); | ||
}; | ||
@@ -216,7 +214,10 @@ const backup = async () => { | ||
if (appsList.length === 0) { | ||
log(chalk.yellow('No apps deployed yet.')); | ||
log(chalk.yellow("No apps deployed yet.")); | ||
process.exit(); | ||
} else { | ||
const question = questions.APP_BACKUP; | ||
question.choices = appsList.map((app) => ({ name: app.name, value: app.id })); | ||
question.choices = appsList.map((app) => ({ | ||
name: app.name, | ||
value: app.id, | ||
})); | ||
const backupAnswer = await inquirer.prompt([question]); | ||
@@ -226,4 +227,88 @@ await backupApp(backupAnswer.appId); | ||
} | ||
}; | ||
module.exports = { create, deploy, clean, build, updateFramework, backup }; | ||
function createDotRemakeFile(projectName, options) { | ||
spinner = ora("Setting up .remake").start(); | ||
const dotRemakeObj = { | ||
...generateDotRemakeContent(options.multitenant), | ||
}; | ||
spinner.succeed(); | ||
const dotRemakeReady = writeDotRemake(dotRemakeObj); | ||
if (!dotRemakeReady) { | ||
spinner.fail(chalk.bgRed("Error: Couldn't create .remake file")); | ||
exit(1); | ||
} | ||
showSuccessfulCreationMessage(projectName); | ||
} | ||
module.exports = { create, deploy, clean, build, updateFramework, backup } | ||
function getProjectPath(projectName) { | ||
const cwd = process.cwd(); | ||
const projectPath = path.join(cwd, projectName); | ||
return projectPath; | ||
} | ||
function installNpmPackages() { | ||
spinner = ora("Installing npm dependencies.").start(); | ||
shell.exec("npm install", { silent: true }); | ||
spinner.succeed(); | ||
} | ||
async function setupTemplate() { | ||
const { starter } = await inquirer.prompt([questions.CHOOSE_STARTER]); | ||
spinner = ora(`Cloning ${starter}`).start(); | ||
shell.mkdir("starter-tmp"); | ||
shell.exec(`git clone ${starter} starter-tmp`, { silent: true }); | ||
shell.rm("starter-tmp/README.md"); | ||
shell.rm("starter-tmp/.gitignore"); | ||
rimrafError = await rimraf(path.join("starter-tmp", ".git")); | ||
shell.mv("starter-tmp/*", "app"); | ||
rimrafError = await rimraf(path.join("starter-tmp")); | ||
spinner.succeed(); | ||
} | ||
function moveReadme() { | ||
shell.mv("README-FOR-BUNDLE.md", "README.md"); | ||
} | ||
function cleanPackageJson(projectName) { | ||
replace({ | ||
regex: `"name": "remake-framework"`, | ||
replacement: `"name": "${projectName}"`, | ||
paths: [`${projectName}/package.json`], | ||
silent: true, | ||
}); | ||
shell.cd(projectName); | ||
shell.exec(`npm version 1.0.0`); | ||
shell.cd(process.cwd()); | ||
} | ||
function initializeGitRepo() { | ||
shell.exec("git init --quiet"); | ||
shell.exec("git add . && git commit -m 'Initial commit' --quiet"); | ||
} | ||
async function removeDotGit(projectName) { | ||
spinner = ora("Tidy up new project directory.").start(); | ||
const projectPath = getProjectPath(projectName); | ||
const rimrafError = await rimraf(path.join(projectPath, ".git")); | ||
if (rimrafError) { | ||
spinner.fail( | ||
chalk.bgRed("Error: Couldn't remove old .git directory from new project.") | ||
); | ||
process.exit(); | ||
} | ||
spinner.succeed(); | ||
} | ||
function cloneRemakeFramework(projectName) { | ||
spinner = ora("Creating new project.").start(); | ||
shell.exec( | ||
`git clone --branch master https://github.com/remake/remake-framework.git ${projectName}`, | ||
{ silent: true } | ||
); | ||
spinner.succeed(); | ||
} |
const fs = require("fs"); | ||
const path = require('path'); | ||
const crypto = require('crypto'); | ||
const process = require('process'); | ||
const nanoidGenerate = require('nanoid/generate'); | ||
const path = require("path"); | ||
const crypto = require("crypto"); | ||
const process = require("process"); | ||
const nanoidGenerate = require("nanoid/generate"); | ||
const log = console.log; | ||
function getUniqueId () { | ||
return nanoidGenerate("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 30); | ||
function getUniqueId() { | ||
return nanoidGenerate( | ||
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", | ||
30 | ||
); | ||
} | ||
function generateDotRemakeContent (multitenant) { | ||
function generateDotRemakeContent(multitenant) { | ||
const dotRemakeContent = { | ||
port: 3000, | ||
sessionSecret: getUniqueId() | ||
} | ||
sessionSecret: getUniqueId(), | ||
}; | ||
if (multitenant) { | ||
dotRemakeContent.remakeMultiTenant = true; | ||
dotRemakeContent.jwtSecret = crypto.randomBytes(30).toString('base64'); | ||
dotRemakeContent.jwtSecret = crypto.randomBytes(30).toString("base64"); | ||
} | ||
@@ -25,7 +28,7 @@ return dotRemakeContent; | ||
function writeDotRemake (content) { | ||
function writeDotRemake(content) { | ||
const cwd = process.cwd(); | ||
const dotRemakePath = path.join(cwd, '.remake'); | ||
const dotRemakePath = path.join(cwd, ".remake"); | ||
try { | ||
fs.writeFileSync(dotRemakePath, JSON.stringify(content, null, 4)) | ||
fs.writeFileSync(dotRemakePath, JSON.stringify(content, null, 4)); | ||
return true; | ||
@@ -38,5 +41,5 @@ } catch (err) { | ||
function readDotRemake () { | ||
function readDotRemake() { | ||
const cwd = process.cwd(); | ||
const dotRemakePath = path.join(cwd, '.remake'); | ||
const dotRemakePath = path.join(cwd, ".remake"); | ||
@@ -49,7 +52,7 @@ // check if .remake file exists | ||
try { | ||
const dotRemake = fs.readFileSync(dotRemakePath, 'utf8'); | ||
const dotRemake = fs.readFileSync(dotRemakePath, "utf8"); | ||
const dotRemakeObj = JSON.parse(dotRemake); | ||
return dotRemakeObj; | ||
} catch (err) { | ||
log(err) | ||
log(err); | ||
return false; | ||
@@ -59,2 +62,2 @@ } | ||
module.exports = { generateDotRemakeContent, writeDotRemake, readDotRemake } | ||
module.exports = { generateDotRemakeContent, writeDotRemake, readDotRemake }; |
@@ -1,13 +0,13 @@ | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const process = require('process'); | ||
const inquirer = require('inquirer'); | ||
const axios = require('axios'); | ||
const chalk = require('chalk'); | ||
const archiver = require('archiver'); | ||
const shell = require('shelljs'); | ||
const FormData = require('form-data'); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const process = require("process"); | ||
const inquirer = require("inquirer"); | ||
const axios = require("axios"); | ||
const chalk = require("chalk"); | ||
const archiver = require("archiver"); | ||
const shell = require("shelljs"); | ||
const FormData = require("form-data"); | ||
const { questions } = require('./inquirer-questions'); | ||
const ora = require('ora'); | ||
const { questions } = require("./inquirer-questions"); | ||
const ora = require("ora"); | ||
@@ -17,43 +17,47 @@ let spinner = null; | ||
const registerUser = async () => { | ||
const userEmail = remakeCliConfig.get('user.email'); | ||
const authToken = remakeCliConfig.get('user.authToken'); | ||
const userEmail = remakeCliConfig.get("user.email"); | ||
const authToken = remakeCliConfig.get("user.authToken"); | ||
if (!userEmail || !authToken) { | ||
log(chalk.yellow(`Not logged in.`)); | ||
let loginAnswers = await inquirer.prompt([questions.NEW_USER, questions.INPUT_EMAIL, questions.INPUT_PASSWORD]); | ||
if (loginAnswers.existingUser.startsWith('Yes')) { | ||
let loginAnswers = await inquirer.prompt([ | ||
questions.NEW_USER, | ||
questions.INPUT_EMAIL, | ||
questions.INPUT_PASSWORD, | ||
]); | ||
if (loginAnswers.existingUser.startsWith("Yes")) { | ||
try { | ||
spinner = ora('Logging you in.').start(); | ||
spinner = ora("Logging you in.").start(); | ||
let res = await axios({ | ||
method: 'post', | ||
url: `${remakeServiceHost}/service/login`, | ||
method: "post", | ||
url: `${remakeServiceHost}/service/login`, | ||
data: { | ||
email: loginAnswers.email, | ||
password: loginAnswers.password, | ||
} | ||
}, | ||
}); | ||
remakeCliConfig.set('user.email', loginAnswers.email); | ||
remakeCliConfig.set('user.authToken', res.data.token); | ||
spinner.succeed('You are successfuly logged in.'); | ||
remakeCliConfig.set("user.email", loginAnswers.email); | ||
remakeCliConfig.set("user.authToken", res.data.token); | ||
spinner.succeed("You are successfuly logged in."); | ||
} catch (err) { | ||
spinner.fail('Could not log you in. Please try again.'); | ||
spinner.fail("Could not log you in. Please try again."); | ||
process.exit(); | ||
} | ||
} else { | ||
try{ | ||
spinner = ora('Creating your account').start(); | ||
try { | ||
spinner = ora("Creating your account").start(); | ||
let res = await axios({ | ||
method: 'post', | ||
url: `${remakeServiceHost}/service/signup`, | ||
method: "post", | ||
url: `${remakeServiceHost}/service/signup`, | ||
data: { | ||
email: loginAnswers.email, | ||
password: loginAnswers.password, | ||
} | ||
}, | ||
}); | ||
remakeCliConfig.set('user.email', loginAnswers.email); | ||
remakeCliConfig.set('user.authToken', res.data.token); | ||
spinner.succeed('Created your account and logged you in.'); | ||
remakeCliConfig.set("user.email", loginAnswers.email); | ||
remakeCliConfig.set("user.authToken", res.data.token); | ||
spinner.succeed("Created your account and logged you in."); | ||
} catch (err) { | ||
spinner.fail('Could not create your account. Please try again.'); | ||
spinner.fail("Could not create your account. Please try again."); | ||
process.exit(); | ||
@@ -63,3 +67,3 @@ } | ||
} | ||
} | ||
}; | ||
@@ -69,10 +73,10 @@ const checkSubdomain = async (subdomain) => { | ||
const availabilityRes = await axios({ | ||
method: 'get', | ||
url: `${remakeServiceHost}/service/subdomain/check`, | ||
method: "get", | ||
url: `${remakeServiceHost}/service/subdomain/check`, | ||
headers: { | ||
'Authorization': `Bearer ${remakeCliConfig.get('user.authToken')}` | ||
Authorization: `Bearer ${remakeCliConfig.get("user.authToken")}`, | ||
}, | ||
params: { | ||
subdomain, | ||
} | ||
}, | ||
}); | ||
@@ -84,3 +88,3 @@ if (availabilityRes.status === 200) return true; | ||
} | ||
} | ||
}; | ||
@@ -90,10 +94,10 @@ const registerSubdomain = async (subdomain) => { | ||
const domainRegistered = await axios({ | ||
method: 'post', | ||
url: `${remakeServiceHost}/service/subdomain/register`, | ||
method: "post", | ||
url: `${remakeServiceHost}/service/subdomain/register`, | ||
headers: { | ||
'Authorization': `Bearer ${remakeCliConfig.get('user.authToken')}` | ||
Authorization: `Bearer ${remakeCliConfig.get("user.authToken")}`, | ||
}, | ||
data: { | ||
subdomain, | ||
} | ||
}, | ||
}); | ||
@@ -104,12 +108,15 @@ if (domainRegistered.status === 200) return { success: true }; | ||
} | ||
} | ||
}; | ||
const createDeploymentZip = (projectName) => { | ||
const spinner = ora('Archiving files for upload.').start(); | ||
const spinner = ora("Archiving files for upload.").start(); | ||
return new Promise((resolve, reject) => { | ||
const cwd = process.cwd(); | ||
const output = fs.createWriteStream(path.join(cwd, `deployment-${projectName}.zip`), { encoding: 'base64' }); | ||
const archive = archiver('zip', { zlib: { level: 9 } }); | ||
const output = fs.createWriteStream( | ||
path.join(cwd, `deployment-${projectName}.zip`), | ||
{ encoding: "base64" } | ||
); | ||
const archive = archiver("zip", { zlib: { level: 9 } }); | ||
output.on('warning', (err) => { | ||
output.on("warning", (err) => { | ||
spinner.fail(); | ||
@@ -119,3 +126,3 @@ reject(err); | ||
output.on('error', (err) => { | ||
output.on("error", (err) => { | ||
spinner.fail(); | ||
@@ -125,60 +132,63 @@ reject(err); | ||
output.on('close', () => { | ||
spinner.succeed('Done archiving: ' + archive.pointer() + ' bytes.'); | ||
output.on("close", () => { | ||
spinner.succeed("Done archiving: " + archive.pointer() + " bytes."); | ||
resolve(); | ||
}) | ||
}); | ||
archive.pipe(output); | ||
archive.glob('app/**/*'); | ||
archive.glob('_remake/dist/**/*') | ||
// archive.glob('_remake-data/*/*.json'); | ||
archive.glob("app/data/bootstrap.json"); | ||
archive.glob("app/data/global.json"); | ||
archive.glob("app/assets/**/*"); | ||
archive.glob("app/layouts/**/*"); | ||
archive.glob("app/pages/**/*"); | ||
archive.glob("app/partials/**/*"); | ||
archive.finalize(); | ||
}) | ||
} | ||
}); | ||
}; | ||
const removeDeploymentZip = (projectName) => { | ||
spinner = ora('Cleaning up project directory.').start(); | ||
spinner = ora("Cleaning up project directory.").start(); | ||
const cwd = process.cwd(); | ||
shell.rm(path.join(cwd, `deployment-${projectName}.zip`)) | ||
shell.rm(path.join(cwd, `deployment-${projectName}.zip`)); | ||
spinner.succeed(); | ||
} | ||
}; | ||
const pushZipToServer = async (projectName) => { | ||
spinner = ora('Pushing your files to the deployment server.').start(); | ||
spinner = ora("Pushing your files to the deployment server.").start(); | ||
const cwd = process.cwd(); | ||
const zipPath = path.join(cwd, `deployment-${projectName}.zip`); | ||
const formData = new FormData(); | ||
formData.append('deployment', fs.readFileSync(zipPath), `${projectName}.zip`); | ||
formData.append('appName', projectName); | ||
formData.append("deployment", fs.readFileSync(zipPath), `${projectName}.zip`); | ||
formData.append("appName", projectName); | ||
try { | ||
const res = await axios({ | ||
method: 'POST', | ||
method: "POST", | ||
url: `${remakeServiceHost}/service/deploy`, | ||
headers: { | ||
...formData.getHeaders(), | ||
'Authorization': `Bearer ${remakeCliConfig.get('user.authToken')}`, | ||
Authorization: `Bearer ${remakeCliConfig.get("user.authToken")}`, | ||
}, | ||
data: formData.getBuffer() | ||
data: formData.getBuffer(), | ||
}); | ||
if (res.status === 200) | ||
spinner.succeed('Files successfully uploaded to server.') | ||
spinner.succeed("Files successfully uploaded to server."); | ||
else { | ||
spinner.fail('Could not upload your files to the server.'); | ||
throw new Error('Could not upload your files to the server.') | ||
spinner.fail("Could not upload your files to the server."); | ||
throw new Error("Could not upload your files to the server."); | ||
} | ||
} catch (err) { | ||
spinner.fail('Could not upload your files to the server.'); | ||
throw new Error(err) | ||
spinner.fail("Could not upload your files to the server."); | ||
throw new Error(err); | ||
} | ||
} | ||
}; | ||
const getAppsList = async () => { | ||
const spinner = ora('Getting your apps list.').start(); | ||
const spinner = ora("Getting your apps list.").start(); | ||
try { | ||
const appListResponse = await axios({ | ||
method: 'get', | ||
url: `${remakeServiceHost}/service/apps`, | ||
method: "get", | ||
url: `${remakeServiceHost}/service/apps`, | ||
headers: { | ||
'Authorization': `Bearer ${remakeCliConfig.get('user.authToken')}` | ||
Authorization: `Bearer ${remakeCliConfig.get("user.authToken")}`, | ||
}, | ||
@@ -197,25 +207,33 @@ }); | ||
} | ||
} | ||
}; | ||
const backupApp = async (appId) => { | ||
const spinner = ora('Generating and downlading zip.').start(); | ||
const spinner = ora("Generating and downlading zip.").start(); | ||
try { | ||
const backupResponse = await axios({ | ||
method: 'get', | ||
method: "get", | ||
url: `${remakeServiceHost}/service/backup`, | ||
responseType: 'stream', | ||
responseType: "stream", | ||
headers: { | ||
'Authorization': `Bearer ${remakeCliConfig.get('user.authToken')}` | ||
Authorization: `Bearer ${remakeCliConfig.get("user.authToken")}`, | ||
}, | ||
params: { | ||
appId, | ||
} | ||
}) | ||
}, | ||
}); | ||
if (backupResponse.status === 200) { | ||
const fileName = backupResponse.headers['content-disposition'].split('=')[1].replace(/\"/g, ''); | ||
const fileName = backupResponse.headers["content-disposition"] | ||
.split("=")[1] | ||
.replace(/\"/g, ""); | ||
const writer = fs.createWriteStream(fileName); | ||
backupResponse.data.pipe(writer); | ||
return new Promise((resolve, reject) => { | ||
writer.on('finish', () => { spinner.succeed(); resolve(); }); | ||
writer.on('error', () => { spinner.fail(); reject(); }); | ||
writer.on("finish", () => { | ||
spinner.succeed(); | ||
resolve(); | ||
}); | ||
writer.on("error", () => { | ||
spinner.fail(); | ||
reject(); | ||
}); | ||
}); | ||
@@ -230,3 +248,3 @@ } else { | ||
} | ||
} | ||
}; | ||
@@ -242,2 +260,2 @@ module.exports = { | ||
backupApp, | ||
} | ||
}; |
const validateSubdomain = (subdomain) => { | ||
const subdomainRegex = /^[a-z]+[a-z0-9\-]*$/ | ||
if (!subdomainRegex.test(subdomain)) | ||
return 'The project name should start with a lowercase letter and should contain only lowercase letters, numbers and dashes.' | ||
const subdomainRegex = /^[a-z]+[a-z0-9\-]*$/; | ||
if (!subdomainRegex.test(subdomain)) | ||
return "The project name should start with a lowercase letter and should contain only lowercase letters, numbers and dashes."; | ||
else return true; | ||
} | ||
}; | ||
const validateEmail = (email) => { | ||
// regex source: https://stackoverflow.com/a/46181 | ||
const emailRegex = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/ | ||
const emailRegex = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/; | ||
if (!emailRegex.test(email)) { | ||
return 'Please provide a valid email address.'; | ||
} | ||
else return true; | ||
} | ||
return "Please provide a valid email address."; | ||
} else return true; | ||
}; | ||
const validatePass = (pass) => { | ||
// at least one digit and one letter + at least 8 chars long | ||
const passRegex = /^(?=.*\d)(?=.*[a-z])[a-zA-Z0-9]{8,}$/ | ||
const passRegex = /^(?=.*\d)(?=.*[a-z])[a-zA-Z0-9]{8,}$/; | ||
if (!passRegex.test(pass)) | ||
return 'The password should be at least 8 characters long and it should contain at least one digit and a letter.'; | ||
return "The password should be at least 8 characters long and it should contain at least one digit and a letter."; | ||
else return true; | ||
} | ||
}; | ||
const questions = { | ||
CHOOSE_STARTER: { | ||
message: `Choose a starter template`, | ||
name: "starter", | ||
type: "list", | ||
choices: [ | ||
{ | ||
name: "Kanban starter", | ||
value: "https://github.com/remake/kanban-starter", | ||
}, | ||
{ | ||
name: "Default starter", | ||
value: "https://github.com/remake/default-starter", | ||
}, | ||
], | ||
}, | ||
NEW_USER: { | ||
message: `Did you log in previously?`, | ||
name: 'existingUser', | ||
type: 'list', | ||
choices: ['Yes, proceed to login', 'No, create new account'], | ||
name: "existingUser", | ||
type: "list", | ||
choices: ["Yes, proceed to login", "No, create new account"], | ||
}, | ||
INPUT_EMAIL: { | ||
message: `Email >`, | ||
name: 'email', | ||
type: 'input', | ||
name: "email", | ||
type: "input", | ||
validate: validateEmail, | ||
@@ -40,6 +54,6 @@ }, | ||
message: `Password >`, | ||
name: 'password', | ||
type: 'password', | ||
mask: '*', | ||
validate: validatePass | ||
name: "password", | ||
type: "password", | ||
mask: "*", | ||
validate: validatePass, | ||
}, | ||
@@ -51,19 +65,19 @@ INPUT_SUBDOMAIN: { | ||
> `, | ||
name: 'subdomain', | ||
type: 'input', | ||
validate: validateSubdomain | ||
name: "subdomain", | ||
type: "input", | ||
validate: validateSubdomain, | ||
}, | ||
CONFIRM_SUBDOMAIN: { | ||
name: 'deployOk', | ||
message: 'Subdomain is available. Do you want to proceed?', | ||
type: 'confirm' | ||
name: "deployOk", | ||
message: "Subdomain is available. Do you want to proceed?", | ||
type: "confirm", | ||
}, | ||
APP_BACKUP: { | ||
message: 'Which app do you want to back up?', | ||
name: 'appId', | ||
type: 'list', | ||
choices: [] | ||
} | ||
} | ||
message: "Which app do you want to back up?", | ||
name: "appId", | ||
type: "list", | ||
choices: [], | ||
}, | ||
}; | ||
module.exports = { questions }; | ||
module.exports = { questions }; |
@@ -1,4 +0,4 @@ | ||
const chalk = require('chalk'); | ||
const chalk = require("chalk"); | ||
function showSuccessfulCreationMessage (projectDir) { | ||
function showSuccessfulCreationMessage(projectDir) { | ||
log(` | ||
@@ -21,2 +21,2 @@ ${chalk.magenta.bold("Your new Remake project has been created!")} | ||
module.exports = {showSuccessfulCreationMessage}; | ||
module.exports = { showSuccessfulCreationMessage }; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
41150
17
680
15
1
111
5
+ Addedhusky@^4.3.0
+ Addedreplace@^1.2.0
+ Added@babel/code-frame@7.24.7(transitive)
+ Added@babel/helper-validator-identifier@7.24.7(transitive)
+ Added@babel/highlight@7.24.7(transitive)
+ Added@types/parse-json@4.0.2(transitive)
+ Addedcallsites@3.1.0(transitive)
+ Addedcamelcase@5.3.1(transitive)
+ Addedci-info@2.0.0(transitive)
+ Addedcliui@6.0.0(transitive)
+ Addedcompare-versions@3.6.0(transitive)
+ Addedcosmiconfig@7.1.0(transitive)
+ Addeddecamelize@1.2.0(transitive)
+ Addederror-ex@1.3.2(transitive)
+ Addedfind-up@4.1.05.0.0(transitive)
+ Addedfind-versions@4.0.0(transitive)
+ Addedget-caller-file@2.0.5(transitive)
+ Addedhusky@4.3.8(transitive)
+ Addedimport-fresh@3.3.0(transitive)
+ Addedis-arrayish@0.2.1(transitive)
+ Addedjs-tokens@4.0.0(transitive)
+ Addedjson-parse-even-better-errors@2.3.1(transitive)
+ Addedlines-and-columns@1.2.4(transitive)
+ Addedlocate-path@5.0.06.0.0(transitive)
+ Addedminimatch@3.0.5(transitive)
+ Addedopencollective-postinstall@2.0.3(transitive)
+ Addedp-limit@2.3.03.1.0(transitive)
+ Addedp-locate@4.1.05.0.0(transitive)
+ Addedp-try@2.2.0(transitive)
+ Addedparent-module@1.0.1(transitive)
+ Addedparse-json@5.2.0(transitive)
+ Addedpath-exists@4.0.0(transitive)
+ Addedpath-type@4.0.0(transitive)
+ Addedpicocolors@1.1.0(transitive)
+ Addedpkg-dir@5.0.0(transitive)
+ Addedplease-upgrade-node@3.2.0(transitive)
+ Addedreplace@1.2.2(transitive)
+ Addedrequire-directory@2.1.1(transitive)
+ Addedrequire-main-filename@2.0.0(transitive)
+ Addedresolve-from@4.0.0(transitive)
+ Addedsemver-compare@1.0.0(transitive)
+ Addedsemver-regex@3.1.4(transitive)
+ Addedset-blocking@2.0.0(transitive)
+ Addedslash@3.0.0(transitive)
+ Addedwhich-module@2.0.1(transitive)
+ Addedwhich-pm-runs@1.1.0(transitive)
+ Addedwrap-ansi@6.2.0(transitive)
+ Addedy18n@4.0.3(transitive)
+ Addedyaml@1.10.2(transitive)
+ Addedyargs@15.4.1(transitive)
+ Addedyargs-parser@18.1.3(transitive)
+ Addedyocto-queue@0.1.0(transitive)