Skip to content
This repository has been archived by the owner on Jul 19, 2021. It is now read-only.

Add Browsersync for mobile development testing #712

Merged
merged 3 commits into from
Sep 4, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Default to using local IP address instead of localhost
  • Loading branch information
t-kelly committed Aug 31, 2018
commit f0bf67ede1de8ff0d0c6c1999083753216585b7f
117 changes: 82 additions & 35 deletions packages/slate-tools/cli/commands/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,65 @@ const {event} = require('@shopify/slate-analytics');

const promptContinueIfPublishedTheme = require('../prompts/continue-if-published-theme');
const promptSkipSettingsData = require('../prompts/skip-settings-data');
const promptDisableExternalTesting = require('../prompts/confim-private-ip');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo on this file (confim => confirm)


const AssetServer = require('../../tools/asset-server');
const DevServer = require('../../tools/dev-server');
const webpackConfig = require('../../tools/webpack/config/dev');
const config = require('../../slate-tools.config');
const packageJson = require('../../package.json');
const {getAvailablePortSeries} = require('../../tools/utilities');

const options = {
env: argv.env,
skipFirstDeploy: argv.skipFirstDeploy,
webpackConfig,
};
const spinner = ora(chalk.magenta(' Compiling...'));

let firstSync = true;
let skipSettingsData = null;
let continueIfPublishedTheme = null;
let assetServer;
let devServer;
let previewUrl;

Promise.all([
getAvailablePortSeries(config.port, 3),
promptDisableExternalTesting(),
])
.then(([ports, domain]) => {
assetServer = new AssetServer({
env: argv.env,
skipFirstDeploy: argv.skipFirstDeploy,
webpackConfig,
port: ports[1],
domain,
});

const assetServer = new AssetServer(options);
const devServer = new DevServer();
const previewUrl = `https://${env.getStoreValue()}?preview_theme_id=${env.getThemeIdValue()}`;
devServer = new DevServer({
port: ports[0],
uiPort: ports[2],
domain,
});

assetServer.compiler.hooks.compile.tap('CLI', () => {
previewUrl = `https://${env.getStoreValue()}?preview_theme_id=${env.getThemeIdValue()}`;

assetServer.compiler.hooks.compile.tap('CLI', onCompilerCompile);
assetServer.compiler.hooks.done.tap('CLI', onCompilerDone);
assetServer.client.hooks.beforeSync.tapPromise('CLI', onClientBeforeSync);
assetServer.client.hooks.syncSkipped.tap('CLI', onClientSyncSkipped);
assetServer.client.hooks.sync.tap('CLI', onClientSync);
assetServer.client.hooks.syncDone.tap('CLI', onClientSyncDone);
assetServer.client.hooks.afterSync.tap('CLI', onClientAfterSync);

return assetServer.start();
})
.catch((error) => {
console.error(error);
});

function onCompilerCompile() {
clearConsole();
spinner.start();
});
}

assetServer.compiler.hooks.done.tap('CLI', (stats) => {
function onCompilerDone(stats) {
const statsJson = stats.toJson({}, true);

spinner.stop();
Expand Down Expand Up @@ -79,9 +112,9 @@ assetServer.compiler.hooks.done.tap('CLI', (stats) => {
1000}s!`,
);
}
});
}

assetServer.client.hooks.beforeSync.tapPromise('CLI', async (files) => {
async function onClientBeforeSync(files) {
if (firstSync && argv.skipFirstDeploy) {
assetServer.skipDeploy = true;

Expand Down Expand Up @@ -113,9 +146,9 @@ assetServer.client.hooks.beforeSync.tapPromise('CLI', async (files) => {
(file) => !file.endsWith('settings_data.json'),
);
}
});
}

assetServer.client.hooks.syncSkipped.tap('CLI', () => {
function onClientSyncSkipped() {
if (!(firstSync && argv.skipFirstDeploy)) return;

event('slate-tools:start:skip-first-deploy', {
Expand All @@ -127,22 +160,22 @@ assetServer.client.hooks.syncSkipped.tap('CLI', () => {
figures.info,
)} Skipping first deployment because --skipFirstDeploy flag`,
);
});
}

assetServer.client.hooks.sync.tap('CLI', () => {
function onClientSync() {
event('slate-tools:start:sync-start', {version: packageJson.version});
});
}

assetServer.client.hooks.syncDone.tap('CLI', () => {
function onClientSyncDone() {
event('slate-tools:start:sync-end', {version: packageJson.version});

process.stdout.write(consoleControl.previousLine(4));
process.stdout.write(consoleControl.eraseData());

console.log(`\n${chalk.green(figures.tick)} Files uploaded successfully!`);
});
}

assetServer.client.hooks.afterSync.tap('CLI', async () => {
async function onClientAfterSync() {
if (firstSync) {
firstSync = false;
await devServer.start();
Expand All @@ -166,26 +199,40 @@ assetServer.client.hooks.afterSync.tap('CLI', async () => {
console.log(
` ${chalk.cyan(urls.get('local'))} ${chalk.grey('(Local)')}`,
);

if (devServer.domain !== 'localhost') {
console.log(
` ${chalk.cyan(urls.get('external'))} ${chalk.grey('(External)')}`,
);
}
console.log();
console.log(` Assets are being served from:\n`);

console.log(
` ${chalk.cyan(urls.get('external'))} ${chalk.grey('(External)')}`,
` ${chalk.cyan(`https://localhost:${assetServer.port}`)} ${chalk.grey(
'(Local)',
)}`,
);
console.log();
console.log(` Local assets are being served from:\n`);

console.log(` ${chalk.cyan(`https://localhost:${assetServer.port}`)}`);
if (assetServer.domain !== 'localhost') {
console.log(
` ${chalk.cyan(
`https://${assetServer.domain}:${assetServer.port}`,
)} ${chalk.grey('(External)')}`,
);
}

console.log();
console.log(` The Browsersync control panel is available at:\n`);
console.log(` ${chalk.cyan(urls.get('ui'))} ${chalk.grey('(Local)')}`);
console.log(
` ${chalk.cyan(urls.get('ui-external'))} ${chalk.grey('(External)')}`,
);

console.log(chalk.magenta('\nWatching for changes...'));
});

event('slate-tools:start:start', {version: packageJson.version});
if (devServer.domain !== 'localhost') {
console.log(
` ${chalk.cyan(urls.get('ui-external'))} ${chalk.grey(
'(External)',
)}`,
);
}

assetServer.start().catch((error) => {
console.error(error);
});
console.log(chalk.magenta('\nWatching for changes...'));
}
40 changes: 40 additions & 0 deletions packages/slate-tools/cli/prompts/confim-private-ip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable */
const chalk = require('chalk');
const ip = require('ip');
const inquirer = require('inquirer');
const slateEnv = require('@shopify/slate-env');
const {event} = require('@shopify/slate-analytics');
const {fetchMainThemeId} = require('@shopify/slate-sync');
const figures = require('figures');
const {argv} = require('yargs');

const question = {
type: 'confirm',
name: 'disableExternalTesting',
message: ' Continue with external device testing disabled?',
default: true,
};

module.exports = async function promptDisableExternalTesting() {
let address = ip.address();

if (!ip.isPrivate(address)) {
console.log(
`\n${chalk.yellow(
figures.warning
)} It looks like you are connected to the internet with the IP address,
'${chalk.green(address)}', which is publically accessible. This could result
in security vulnerabilities to your development machine if you want to test
your dev store from an external device, e.g. your phone. We recommend you
proceed with external testing disabled until you are connected to the internet
with a private IP address, e.g. connected to a router which assigns your
device a private IP.\n`
);

const answer = await inquirer.prompt([question]);

address = answer.disableExternalTesting ? 'localhost' : address;
}

return address;
};
3 changes: 2 additions & 1 deletion packages/slate-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@
"jest": "22.4.2",
"minimatch": "^3.0.4",
"minimist": "1.2.0",
"node-ip": "^0.1.2",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is meant to be "ip": "^1.1.5", or the prompts/confim-private-ip.js needs to have it's require() change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Got mixed up there. Thanks.

"node-sass": "^4.7.2",
"ora": "^2.0.0",
"portfinder": "^1.0.17",
"portscanner": "^2.2.0",
"postcss-loader": "2.1.1",
"postcss-reporter": "^5.0.0",
"prettier": "^1.10.2",
Expand Down
6 changes: 1 addition & 5 deletions packages/slate-tools/slate-tools.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,9 @@ module.exports = generate({
},
],
},
{
id: 'domain',
default: 'https://localhost',
},
{
id: 'port',
default: 3001,
default: 3000,
},
{
id: 'regex',
Expand Down
10 changes: 6 additions & 4 deletions packages/slate-tools/tools/asset-server/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const fs = require('fs');
const portfinder = require('portfinder');
const webpack = require('webpack');
const {createServer} = require('https');
const createHash = require('crypto').createHash;
Expand All @@ -10,12 +9,16 @@ const {sslKeyCert} = require('../utilities');
const config = require('../../slate-tools.config');
const setEnvironment = require('../../tools/webpack/set-slate-env');

portfinder.basePort = config.port;

module.exports = class DevServer {
constructor(options) {
options.webpackConfig.output.publicPath = `https://${options.domain}:${
options.port
}/`;

this.assetHashes = {};
this.domain = options.domain;
this.options = options;
this.port = options.port;
this.env = setEnvironment(options.env);
this.compiler = webpack(options.webpackConfig);
this.app = new App(this.compiler);
Expand All @@ -33,7 +36,6 @@ module.exports = class DevServer {
);
this.ssl = sslKeyCert();
this.server = createServer(this.ssl, this.app);
this.port = await portfinder.getPortPromise();

this.server.listen(this.port);
}
Expand Down
29 changes: 22 additions & 7 deletions packages/slate-tools/tools/dev-server/index.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
const browserSync = require('browser-sync');
const {getStoreValue, getThemeIdValue} = require('@shopify/slate-env');
const {getSSLKeyPath, getSSLCertPath} = require('../utilities');
const config = require('../../slate-tools.config');

class DevServer {
constructor() {
constructor(options) {
this.bs = browserSync.create();
this.target = `https://${getStoreValue()}`;
this.themeId = getThemeIdValue();
this.port = options.port;
this.domain = options.domain;
this.uiPort = options.uiPort;
this.proxyTarget =
this.target +
(this.themeId === 'live' ? '' : `?preview_theme_id=${this.themeId}`);
}
start() {
const bsConfig = {
port: this.port,
proxy: {
target: this.target,
proxyReq: (req) => {
target: this.proxyTarget,
middleware: (req, res, next) => {
// Shopify sites with redirection enabled for custom domains force redirection
// to that domain. `?_fd=0` prevents that forwarding.
const prefix = req.url.indexOf('?') > -1 ? '&' : '?';
const queryStringComponents = ['_fd=0'];

req.params = Object.assign(
{_fd: 0, preview_theme_id: getThemeIdValue()}, // eslint-disable-line camelcase
req.params,
);
req.url += prefix + queryStringComponents.join('&');
next();
},
},
https: {key: getSSLKeyPath(), cert: getSSLCertPath()},
logLevel: 'silent',
socket: {
domain: `${this.domain}:${this.port}`,
},
ui: {
port: this.uiPort,
},
};

return new Promise((resolve) => {
Expand Down
32 changes: 31 additions & 1 deletion packages/slate-tools/tools/utilities/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
const path = require('path');
const {promisify} = require('util');
const {existsSync, readFileSync} = require('fs');
const portscanner = require('portscanner');
const config = require('../../slate-tools.config');

const findAPortInUse = promisify(portscanner.findAPortInUse);

function sslKeyCert() {
const key = readFileSync(getSSLKeyPath());
const cert = readFileSync(getSSLCertPath());
Expand All @@ -21,4 +25,30 @@ function getSSLCertPath() {
: path.join(__dirname, './server.pem');
}

module.exports = {sslKeyCert, getSSLKeyPath, getSSLCertPath};
// Finds a series of available ports of length quantity, starting at a given
// port number and incrementing up. Returns an array of port numbers.
function getAvailablePortSeries(start, quantity, increment = 1) {
const startPort = start;
const endPort = start + (quantity - 1);

return findAPortInUse(startPort, endPort, '127.0.0.1').then((port) => {
if (typeof port === 'number') {
return getAvailablePortSeries(port++, quantity);
}

const ports = [];

for (let i = startPort; i <= endPort; i += increment) {
ports.push(i);
}

return ports;
});
}

module.exports = {
sslKeyCert,
getSSLKeyPath,
getSSLCertPath,
getAvailablePortSeries,
};
3 changes: 0 additions & 3 deletions packages/slate-tools/tools/webpack/config/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ const config = require('../../../slate-tools.config');
const {templateFiles, layoutFiles} = require('../entrypoints');
const HtmlWebpackIncludeLiquidStylesPlugin = require('../html-webpack-include-chunks');

// so that everything is absolute
webpackCoreConfig.output.publicPath = `${config.domain}:${config.port}/`;

// add hot-reload related code to entry chunks
Object.keys(webpackCoreConfig.entry).forEach((name) => {
webpackCoreConfig.entry[name] = [
Expand Down
Loading