From c4d8bd7af725eb1b97ded1fb835285d8ac2ff6ed Mon Sep 17 00:00:00 2001 From: Miguel Piedrafita Date: Tue, 21 Apr 2020 17:42:14 +0200 Subject: [PATCH] tag v1.0 --- cli.js | 10 +++ package.json | 2 +- readme.md | 12 ++-- src/client.js | 15 ++-- src/commands/auth/login.js | 11 ++- src/commands/auth/logout.js | 2 +- src/commands/auth/switch.js | 5 +- src/commands/deploy.js | 140 ++++++++++++++++++------------------ src/commands/init.js | 40 ++++++++--- src/config/config.md | 11 +++ src/config/global.js | 6 +- src/config/local.js | 54 +++++++++++--- 12 files changed, 197 insertions(+), 111 deletions(-) create mode 100644 src/config/config.md diff --git a/cli.js b/cli.js index eea58d6..c09bc92 100755 --- a/cli.js +++ b/cli.js @@ -1,6 +1,7 @@ #!/usr/bin/env node 'use strict'; const yargs = require('yargs') +const config = require('./src/config/local') yargs.scriptName('$ sitesauce') .usage('$0 [args]') @@ -11,4 +12,13 @@ yargs.scriptName('$ sitesauce') .recommendCommands() .demandCommand(1, '') .showHelpOnFail() + .onFinishCommand(cleanConfig) .argv + +function cleanConfig() { + if (config.empty()) { + config.deleteFile().removeDir() + } else { + config.addReadme().addGitignore() + } +} diff --git a/package.json b/package.json index 6a63501..26b83e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sitesauce-cli", - "version": "0.0.7", + "version": "1.0.0", "description": "The Sitesauce CLI.", "license": "MIT", "repository": "sitesauce/cli", diff --git a/readme.md b/readme.md index ca03dfe..776e50e 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,8 @@ # The Sitesauce CLI -> Deploy your local sites directly to Sitesauce +> Deploy sites running in your computer directly to Sitesauce ## Motivation -While Sitesauce aims to remove the need for hosting the dynamic version of your website, it still requires you to host it somewhere Sitesauce can acess so we can generate your static sites. The Sitesauce CLI allows you to deploy your sites directly from your computer, removing the requirement of paying for an external server. While this may not be the first option for everyone (as it doesn't allow you to share an admin panel with the rest of your team), it provides for an interesting alternative, and greatly reduces the friction (and cost) of publishing a new website. +While Sitesauce aims to remove the need for hosting the dynamic version of your website, it still requires you to host it somewhere acessible so we can generate your static sites. The Sitesauce CLI allows you to deploy your sites directly from your computer, completely removing the need for servers. While this may not be the first option for everyone (as it doesn't allow you to share an admin panel with the rest of your team), it provides for an interesting alternative, and greatly reduces the friction (and cost) of publishing a new website. ## Install @@ -32,11 +32,11 @@ If you want to log out, you can do so by running `$ sitesauce logout`. ## Configuring a project -Projects associate Sitesauce sites and the directories on your machine that contain those sites. To associate a directory with a site, open that directory in your terminal and run `$ sitesauce init`. This will create a `.sitesauce.json` config file that you can add to your `.gitignore` if you don't want to commit (there's no sensitive information, so it shouldn't really matter). +Projects associate Sitesauce sites and the directories on your machine that contain those sites. To associate a directory with a site, open that directory in your terminal and run `$ sitesauce init`. This will create a `.sitesauce` folder with your config. We'll also automatically add that folder to your `.gitignore` if you have one. ## Deploying a project -> NOTE: Make sure you've configured your project before attempting to deploy it. +> NOTE: Make sure you've configured your project deploying it. To deploy a project, open the project directory and run `$ sitesauce deploy`. This will ask you for the port your application is running in and open a secure tunnel between your computer and our server that will be closed as soon as the deployment is finished. @@ -50,6 +50,8 @@ $ sitesauce deploy --port 80 --host laravel.test You might also have noticed we're specifying the port via the `--port` flag, skipping the port prompt and making the whole process faster. +If you use port 80 but don't specify a host, you'll be asked if you want to use a virtual host. + ### Known limitations If your local server uses a self-signed certificate and forces HTTPS support, there's a chance the deployment will fail. To fix it, you can temporally unsecure your site while you deploy. For example, when deploying a Valet site you've used `valet secure` on, you might have to run `valet unsecure` before deploying. @@ -70,4 +72,4 @@ $ sitesauce deploy --help ## License -Licensed under the MIT license. For more information, [check the license file](license). +Licensed under the MIT license. For more information, [check the license file](LICENSE). diff --git a/src/client.js b/src/client.js index 31a6d18..8f98abb 100644 --- a/src/client.js +++ b/src/client.js @@ -7,7 +7,8 @@ class Client { baseURL: 'https://app.sitesauce.app/api/', headers: { Authorization: `Bearer ${config.get('token')}` - } + }, + }); } @@ -20,19 +21,19 @@ class Client { } getTeam() { - return this.client.get('team').then(response => response.data); + return this.client.get(`team/${config.get('teamId')}`).then(response => response.data); } getTeams() { - return this.client.get('teams').then(response => response.data); + return this.client.get('user/teams').then(response => response.data); } - switchTeam(teamId) { - return this.client.post(`teams/switch/${teamId}`).then(response => response.data); + getSites() { + return this.client.get(`team/${config.get('teamId')}/sites`).then(response => response.data); } - getSites() { - return this.client.get('team/sites').then(response => response.data); + createSite(opts) { + return this.client.post('sites/create', opts).then(response => response.data) } createDeployment(siteId, opts) { diff --git a/src/commands/auth/login.js b/src/commands/auth/login.js index 3626c4d..ace1764 100644 --- a/src/commands/auth/login.js +++ b/src/commands/auth/login.js @@ -4,12 +4,21 @@ const open = require('open'); const uuid = require('uuid/v1'); const hapi = require('@hapi/hapi'); const config = require('../../config/global'); +const client = require('../../client'); async function handler() { const spinner = ora('Logging you in...').start(); const token = await executeAuthFlow(); - config.set({token}); + config.set('token', token); + + try { + const user = await client.getUser() + config.set('teamId', user.current_team_id) + } catch (err) { + config.reset() + return spinner.fail('We had some problems logging you in. Please try again or contact us if the issue persists.') + } spinner.succeed('Successfully logged in to your Sitesauce account!'); } diff --git a/src/commands/auth/logout.js b/src/commands/auth/logout.js index f57ec33..0d69c58 100644 --- a/src/commands/auth/logout.js +++ b/src/commands/auth/logout.js @@ -1,7 +1,7 @@ const config = require('../../config/global'); function handler() { - config.delete('token'); + config.reset() console.log('You\'ve been logged out!'); } diff --git a/src/commands/auth/switch.js b/src/commands/auth/switch.js index 8e92af7..82616f2 100644 --- a/src/commands/auth/switch.js +++ b/src/commands/auth/switch.js @@ -1,5 +1,6 @@ const ora = require('ora') const inquirer = require('inquirer') +const config = require('../../config/global') const client = require('./../../client'); async function handler() { @@ -16,9 +17,7 @@ async function handler() { }]) const team = Object.values(teams).filter(team => team.id === teamId)[0] - - spinner = ora('Switching to team').start(); - await client.switchTeam(teamId) + config.set('teamId', teamId) spinner.succeed(`Successfully switched to ${team.is_personal ? 'your personal team' : team.name}.`); } diff --git a/src/commands/deploy.js b/src/commands/deploy.js index 0c66212..d92b717 100644 --- a/src/commands/deploy.js +++ b/src/commands/deploy.js @@ -6,40 +6,36 @@ const config = require('./../config/local') const isPortReachable = require('is-port-reachable') async function handler(argv) { - if (! config.get('init')) { - config.deleteFile() - console.log('This project has not been configured. Run sitesauce init to configure it.') - process.exit() - } + if (! config.get('siteId')) return ora().fail('This project has not been configured. Run sitesauce init to configure it.') + + const port = await getPort(argv) - let port = await validatePort(await getPort(argv)) + if (! isPortReachable(port)) return ora().fail('The port you specified is not reachable.') - const baseUrl = getBaseUrl(port, argv.host) + const { baseUrl, host } = await getBaseUrl(port, argv.host) let spinner = ora('Starting reverse tunnel...').start(); - const tunnel = await localtunnel({ - port, - host: 'https://tunnel.sitesauce.app', - local_host: argv.host ? argv.host : 'localhost', - allow_invalid_cert: true, - }).catch(() => { - spinner.fail() - console.error('There was an error when opening the reverse tunnel, please try again later.') - process.exit() - }); - spinner.succeed('Reverse tunnel started') - spinner = ora('Starting deployment...').start(); - let deployment = await startDeployment(tunnel.url, baseUrl) - spinner.succeed('Deployment started') + let tunnel; - deployment = await waitForDeployment(deployment.id, () => { - tunnel.close() - }) + try { + tunnel = await localtunnel({ + port, + host: 'https://tunnel.sitesauce.app', + local_host: host, + allow_invalid_cert: true, + }) - console.log(`Successfully deployed ${baseUrl} to ${deployment.provider}.`) + spinner.succeed('Reverse tunnel started') + } catch { + return spinner.fail('There was an error when opening the reverse tunnel, please try again later.') + } + + spinner = ora('Starting deployment...').start(); + let deployment = await startDeployment(tunnel.url, baseUrl) + spinner.text = 'Deploying your site...' - process.exit() + await waitForDeployment(deployment.id, spinner, tunnel) } async function getPort(argv) { @@ -49,21 +45,32 @@ async function getPort(argv) { type: 'number', name: 'port', message: 'What port is this project running its server on?', - default: 8080, + default: 3000, }]).then(answers => answers.port) } -async function validatePort(port) { - if (await isPortReachable(port)) return port +async function getBaseUrl(port, host) { + if (host) return { baseUrl : `http://${host}`, host } - console.log('The port you specified is not reachable.') - process.exit(1) -} + if (port !== 80) return { baseUrl: `http://localhost:${port}`, host: 'localhost' } + + const { shouldDemandHost } = await inquirer.prompt([{ + name: 'shouldDemandHost', + type: 'confirm', + message: "Do you want to specify a virtual host? Do this if you're using something like Laravel Valet to serve your sites", + default: false, + }]) -function getBaseUrl(port, host) { - if (host) return `http://${host}` + if (!shouldDemandHost) return { baseUrl: `http://localhost:${port}`, host: 'localhost' } - return `http://localhost:${port}` + host = await inquirer.prompt([{ + type: 'input', + name: 'host', + message: 'What virtual host should we connect to? (domain.tld)', + validate: host => host.trim().length > 0 && host.includes('.') && ! host.includes('/') + }]).then(answers => answers.host) + + return getBaseUrl(port, host) } function startDeployment(tunnelUrl, baseUrl) { @@ -73,46 +80,46 @@ function startDeployment(tunnelUrl, baseUrl) { }) } -function waitForDeployment(deploymentId, stopTunnel) { +function waitForDeployment(deploymentId, spinner, tunnel) { const deploymentPipeline = [ { stage: 'site_exists', - spinner: ora('Ensure Site Exists').start() + spinnerText:'Ensuring Site Exists...' }, { - spinner: ora('Initialize Deployment'), - stage: 'init' + stage: 'init', + spinnerText: 'Initializing Deployment...', }, { - spinner: ora('Generate Artifact'), - stage: 'generate_artifact' + stage: 'zeit_deploy', + spinnerText: 'Deploying to ZEIT', }, { - spinner: ora('Upload Artifact'), - stage: 'upload_artifact' + stage: 'zeit_build', + spinnerText: 'Building static site...', }, { - spinner: ora('Build pages'), - stage: 'provider_build', + stage: 'finalize', + spinnerText: 'Finishing up deployment...', }, { - spinner: ora('Finalize Deployment'), - stage: 'finalize' + stage: 'done', + spinnerText: 'Cleaning up the tunnel...', }, ] - const stages = deploymentPipeline.map(stage => stage.stage) - let oldStage; return new Promise((resolve, reject) => { const request = () => { client.getDeploymentInfo(config.get('siteId'), deploymentId).then(deployment => { - if (deployment.stage && deployment.stage !== oldStage) stageChanged(oldStage, deployment.stage) + if (deployment.stage) { + if (deployment.stage !== oldStage) stageChangedTo(deployment.stage) - if (deployment.stage && deployment.stage === 'done') resolve(deployment) + if (deployment.stage === 'done') resolve(deployment) - if (deployment.stage && deployment.stage.startsWith('failed_')) reject(deployment) + if (deployment.stage.startsWith('failed_')) reject(deployment) + } setTimeout(() => { request() @@ -122,32 +129,21 @@ function waitForDeployment(deploymentId, stopTunnel) { }) } - const stageChanged = (oldStage, newStage) => { - if (newStage.startsWith('failed_')) return; + const stageChangedTo = newStage => { + if (newStage.startsWith('failed_')) return spinner.fail(); - deploymentPipeline.filter(stage => { - return parseInt(Object.keys(deploymentPipeline).find(key => deploymentPipeline[key].stage === newStage)) > parseInt(Object.keys(deploymentPipeline).find(key => deploymentPipeline[key].stage === stage.stage)) - }).forEach(stage => { - if (stage.spinner.isSpinning) { - stage.spinner.succeed() - } - - if (stage.stage === 'generate_artifact') stopTunnel() - }) - - if(newStage === 'done') return; - - deploymentPipeline.find(stage => stage.stage == newStage).spinner.start() + spinner.text = deploymentPipeline.find(stage => stage.stage == newStage).spinnerText } request() + }).then(() => { + tunnel.close() + + spinner.succeed(`Successfully deployed to Sitesauce.`) }).catch(deployment => { if (deployment.stage && deployment.stage.startsWith('failed_')) { - deploymentPipeline.find(stage => stage.stage == deployment.stage.split('failed_').filter(str => str !== '')[0]).spinner.fail() + spinner.fail() } - - console.error('Failed to deploy to Sitesauce') - process.exit() }) } @@ -163,7 +159,7 @@ module.exports = { host: { alias: 'h', type: 'string', - description: 'The host to bind your server to.' + description: 'The virtual host to bind your server to.' }, }, handler diff --git a/src/commands/init.js b/src/commands/init.js index 637b13c..2c312dd 100644 --- a/src/commands/init.js +++ b/src/commands/init.js @@ -1,34 +1,56 @@ const inquirer = require('inquirer'); +const ora = require('ora') const client = require('./../client'); -const config = require('./../config/local'); +const { local: config, global: globalConfig } = require('./../config'); async function handler(argv) { await ensureOverwrite(argv); const sites = await client.getSites(); - const {siteId} = await inquirer.prompt([{ + let { siteId } = await inquirer.prompt([{ type: 'list', name: 'siteId', message: 'What site should we associate this project with?', choices: [ - ...sites.map(site => ({name: `${site.name} (${site.url})`, value: site.id})), + ...sites.map(site => ({name: `${site.name} (${site.cli ? 'CLI-only' : site.url})`, value: site.id})), new inquirer.Separator(), { name: 'Create New Site', - disabled: 'soon' + value: 'create' } ] }]); + if (siteId === 'create') siteId = await createSite() + config.set({ - init: true, - siteId + siteId, + teamId: globalConfig.get('teamId') }); + + ora('Associating your site').succeed('Site associated successfully') +} + +async function createSite() { + const { siteName } = await inquirer.prompt([ + { + type: 'input', + name: 'siteName', + message: 'How should this site be called?', + validate: (siteName) => siteName.trim().length > 0, + }, + ]); + + const spinner = ora(`Creating ${siteName}...`).start(); + const site = await client.createSite({ name: siteName, cli: true }) + spinner.succeed('Site created successfully') + + return site.id } async function ensureOverwrite(argv) { - if (config.get('init') && !argv.force) { + if (config.get('teamId') && !argv.force) { const {overwrite} = await inquirer.prompt([{ type: 'confirm', name: 'overwrite', @@ -36,9 +58,7 @@ async function ensureOverwrite(argv) { default: false }]); - if (!overwrite) { - process.exit(); - } + if (!overwrite) return config.reset(); } diff --git a/src/config/config.md b/src/config/config.md new file mode 100644 index 0000000..1b33b68 --- /dev/null +++ b/src/config/config.md @@ -0,0 +1,11 @@ +> Why do I have a folder named ".sitesauce" in my project? +The ".sitesauce" folder is created when you link a directory to a Sitesauce site using the Sitesauce CLI. + +> What does the "site.json" file contain? +The "site.json" file contains: +- The ID of the site that you linked ("siteId") +- The ID of the team your Sitesauce site is owned by ("teamId") + +> Should I commit the ".sitesauce" folder? +No, you should not share the ".sitesauce" folder with anyone. +Upon creation, it will be automatically added to your ".gitignore" file. diff --git a/src/config/global.js b/src/config/global.js index bdb4752..643436e 100644 --- a/src/config/global.js +++ b/src/config/global.js @@ -3,12 +3,16 @@ const Conf = require('conf'); const schema = { token: { type: 'string' + }, + teamId: { + type: 'integer' } }; const config = new Conf({ schema, - projectName: 'sitesauce' + projectName: 'sitesauce-cli', + projectSuffix: '' }); module.exports = config; diff --git a/src/config/local.js b/src/config/local.js index bea0d11..3bca18a 100644 --- a/src/config/local.js +++ b/src/config/local.js @@ -1,28 +1,62 @@ -const Conf = require('conf'); +const Conf = require('conf') const fs = require('fs') +const path = require('path') const schema = { - init: { - type: 'boolean', - default: false - }, siteId: { type: 'integer' - } + }, + teamId: { + type: 'integer' + }, }; class LocalConf extends Conf { + empty() { + return this.get('siteId', false) === false + } + deleteFile() { - fs.unlinkSync(`${process.cwd()}/.sitesauce.json`) + if (fs.existsSync(this.path)) fs.unlinkSync(this.path) + + return this + } + + removeDir() { + fs.rmdirSync(this._options.cwd) + } + + addReadme() { + const readmePath = path.join(this._options.cwd, 'README.md') + + if (fs.existsSync(readmePath)) return this + + fs.copyFileSync(path.join(__dirname, 'config.md'), readmePath) + + return this + } + + async addGitignore() { + try { + const gitIgnorePath = path.join(process.cwd(), '.gitignore'); + + const gitIgnore = fs.readFileSync(gitIgnorePath).toString() + + if (!gitIgnore || !gitIgnore.split('\n').includes('.sitesauce')) { + fs.writeFileSync(gitIgnorePath, gitIgnore ? `${gitIgnore}\n.sitesauce` : '.sitesauce'); + } + } catch (error) { + // ignore errors since this is non-critical + } } } const config = new LocalConf({ schema, - configName: '.sitesauce', + configName: 'site', projectName: 'sitesauce', - cwd: process.cwd(), - clearInvalidConfig: false + cwd: path.join(process.cwd(), '.sitesauce'), + clearInvalidConfig: true }); module.exports = config;