573 lines
17 KiB
JavaScript
573 lines
17 KiB
JavaScript
"use strict";
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
|
|
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
|
|
|
|
const url = require(`url`);
|
|
|
|
const glob = require(`glob`);
|
|
|
|
const fs = require(`fs`);
|
|
|
|
const openurl = require(`better-opn`);
|
|
|
|
const chokidar = require(`chokidar`);
|
|
|
|
const express = require(`express`);
|
|
|
|
const graphqlHTTP = require(`express-graphql`);
|
|
|
|
const graphqlPlayground = require(`graphql-playground-middleware-express`).default;
|
|
|
|
const graphiqlExplorer = require(`gatsby-graphiql-explorer`);
|
|
|
|
const {
|
|
formatError
|
|
} = require(`graphql`);
|
|
|
|
const got = require(`got`);
|
|
|
|
const rl = require(`readline`);
|
|
|
|
const webpack = require(`webpack`);
|
|
|
|
const webpackConfig = require(`../utils/webpack.config`);
|
|
|
|
const bootstrap = require(`../bootstrap`);
|
|
|
|
const {
|
|
store,
|
|
emitter
|
|
} = require(`../redux`);
|
|
|
|
const {
|
|
syncStaticDir
|
|
} = require(`../utils/get-static-dir`);
|
|
|
|
const buildHTML = require(`./build-html`);
|
|
|
|
const {
|
|
withBasePath
|
|
} = require(`../utils/path`);
|
|
|
|
const report = require(`gatsby-cli/lib/reporter`);
|
|
|
|
const launchEditor = require(`react-dev-utils/launchEditor`);
|
|
|
|
const formatWebpackMessages = require(`react-dev-utils/formatWebpackMessages`);
|
|
|
|
const chalk = require(`chalk`);
|
|
|
|
const address = require(`address`);
|
|
|
|
const cors = require(`cors`);
|
|
|
|
const telemetry = require(`gatsby-telemetry`);
|
|
|
|
const WorkerPool = require(`../utils/worker/pool`);
|
|
|
|
const withResolverContext = require(`../schema/context`);
|
|
|
|
const sourceNodes = require(`../utils/source-nodes`);
|
|
|
|
const websocketManager = require(`../utils/websocket-manager`);
|
|
|
|
const getSslCert = require(`../utils/get-ssl-cert`);
|
|
|
|
const slash = require(`slash`);
|
|
|
|
const {
|
|
initTracer
|
|
} = require(`../utils/tracer`);
|
|
|
|
const apiRunnerNode = require(`../utils/api-runner-node`);
|
|
|
|
const db = require(`../db`);
|
|
|
|
const detectPortInUseAndPrompt = require(`../utils/detect-port-in-use-and-prompt`);
|
|
|
|
const onExit = require(`signal-exit`);
|
|
|
|
const queryUtil = require(`../query`);
|
|
|
|
const queryQueue = require(`../query/queue`);
|
|
|
|
const queryWatcher = require(`../query/query-watcher`);
|
|
|
|
const requiresWriter = require(`../bootstrap/requires-writer`); // const isInteractive = process.stdout.isTTY
|
|
// Watch the static directory and copy files to public as they're added or
|
|
// changed. Wait 10 seconds so copying doesn't interfere with the regular
|
|
// bootstrap.
|
|
|
|
|
|
setTimeout(() => {
|
|
syncStaticDir();
|
|
}, 10000);
|
|
const rlInterface = rl.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout
|
|
}); // Quit immediately on hearing ctrl-c
|
|
|
|
rlInterface.on(`SIGINT`, () => {
|
|
process.exit();
|
|
});
|
|
onExit(() => {
|
|
telemetry.trackCli(`DEVELOP_STOP`);
|
|
});
|
|
|
|
const waitJobsFinished = () => new Promise((resolve, reject) => {
|
|
const onEndJob = () => {
|
|
if (store.getState().jobs.active.length === 0) {
|
|
resolve();
|
|
emitter.off(`END_JOB`, onEndJob);
|
|
}
|
|
};
|
|
|
|
emitter.on(`END_JOB`, onEndJob);
|
|
onEndJob();
|
|
});
|
|
|
|
async function startServer(program) {
|
|
const directory = program.directory;
|
|
const directoryPath = withBasePath(directory);
|
|
const workerPool = WorkerPool.create();
|
|
|
|
const createIndexHtml = async () => {
|
|
try {
|
|
await buildHTML.buildPages({
|
|
program,
|
|
stage: `develop-html`,
|
|
pagePaths: [`/`],
|
|
workerPool
|
|
});
|
|
} catch (err) {
|
|
if (err.name !== `WebpackError`) {
|
|
report.panic(err);
|
|
return;
|
|
}
|
|
|
|
report.panic(report.stripIndent`
|
|
There was an error compiling the html.js component for the development server.
|
|
|
|
See our docs page on debugging HTML builds for help https://gatsby.dev/debug-html
|
|
`, err);
|
|
}
|
|
};
|
|
|
|
await createIndexHtml();
|
|
const devConfig = await webpackConfig(program, directory, `develop`, program.port);
|
|
const compiler = webpack(devConfig);
|
|
/**
|
|
* Set up the express app.
|
|
**/
|
|
|
|
const app = express();
|
|
app.use(telemetry.expressMiddleware(`DEVELOP`));
|
|
app.use(require(`webpack-hot-middleware`)(compiler, {
|
|
log: false,
|
|
path: `/__webpack_hmr`,
|
|
heartbeat: 10 * 1000
|
|
}));
|
|
app.use(cors());
|
|
/**
|
|
* Pattern matching all endpoints with graphql or graphiql with 1 or more leading underscores
|
|
*/
|
|
|
|
const graphqlEndpoint = `/_+graphi?ql`;
|
|
|
|
if (process.env.GATSBY_GRAPHQL_IDE === `playground`) {
|
|
app.get(graphqlEndpoint, graphqlPlayground({
|
|
endpoint: `/___graphql`
|
|
}), () => {});
|
|
} else {
|
|
graphiqlExplorer(app, {
|
|
graphqlEndpoint
|
|
});
|
|
}
|
|
|
|
app.use(graphqlEndpoint, graphqlHTTP(() => {
|
|
const {
|
|
schema,
|
|
schemaCustomization
|
|
} = store.getState();
|
|
return {
|
|
schema,
|
|
graphiql: false,
|
|
context: withResolverContext({}, schema, schemaCustomization.context),
|
|
|
|
formatError(err) {
|
|
return Object.assign({}, formatError(err), {
|
|
stack: err.stack ? err.stack.split(`\n`) : []
|
|
});
|
|
}
|
|
|
|
};
|
|
}));
|
|
/**
|
|
* Refresh external data sources.
|
|
* This behavior is disabled by default, but the ENABLE_REFRESH_ENDPOINT env var enables it
|
|
* If no GATSBY_REFRESH_TOKEN env var is available, then no Authorization header is required
|
|
**/
|
|
|
|
const REFRESH_ENDPOINT = `/__refresh`;
|
|
app.use(REFRESH_ENDPOINT, express.json());
|
|
app.post(REFRESH_ENDPOINT, (req, res) => {
|
|
const enableRefresh = process.env.ENABLE_GATSBY_REFRESH_ENDPOINT;
|
|
const refreshToken = process.env.GATSBY_REFRESH_TOKEN;
|
|
const authorizedRefresh = !refreshToken || req.headers.authorization === refreshToken;
|
|
|
|
if (enableRefresh && authorizedRefresh) {
|
|
console.log(`Refreshing source data`);
|
|
sourceNodes({
|
|
webhookBody: req.body
|
|
});
|
|
}
|
|
|
|
res.end();
|
|
});
|
|
app.get(`/__open-stack-frame-in-editor`, (req, res) => {
|
|
launchEditor(req.query.fileName, req.query.lineNumber);
|
|
res.end();
|
|
}); // Disable directory indexing i.e. serving index.html from a directory.
|
|
// This can lead to serving stale html files during development.
|
|
//
|
|
// We serve by default an empty index.html that sets up the dev environment.
|
|
|
|
app.use(require(`./develop-static`)(`public`, {
|
|
index: false
|
|
}));
|
|
app.use(require(`webpack-dev-middleware`)(compiler, {
|
|
logLevel: `trace`,
|
|
publicPath: devConfig.output.publicPath,
|
|
stats: `errors-only`
|
|
})); // Expose access to app for advanced use cases
|
|
|
|
const {
|
|
developMiddleware
|
|
} = store.getState().config;
|
|
|
|
if (developMiddleware) {
|
|
developMiddleware(app, program);
|
|
} // Set up API proxy.
|
|
|
|
|
|
const {
|
|
proxy
|
|
} = store.getState().config;
|
|
|
|
if (proxy) {
|
|
const {
|
|
prefix,
|
|
url
|
|
} = proxy;
|
|
app.use(`${prefix}/*`, (req, res) => {
|
|
const proxiedUrl = url + req.originalUrl;
|
|
const {
|
|
// remove `host` from copied headers
|
|
// eslint-disable-next-line no-unused-vars
|
|
method
|
|
} = req,
|
|
headers = (0, _objectWithoutPropertiesLoose2.default)(req.headers, ["host"]);
|
|
req.pipe(got.stream(proxiedUrl, {
|
|
headers,
|
|
method,
|
|
decompress: false
|
|
}).on(`response`, response => res.writeHead(response.statusCode, response.headers)).on(`error`, (err, _, response) => {
|
|
if (response) {
|
|
res.writeHead(response.statusCode, response.headers);
|
|
} else {
|
|
const message = `Error when trying to proxy request "${req.originalUrl}" to "${proxiedUrl}"`;
|
|
report.error(message, err);
|
|
res.sendStatus(500);
|
|
}
|
|
})).pipe(res);
|
|
});
|
|
}
|
|
|
|
await apiRunnerNode(`onCreateDevServer`, {
|
|
app
|
|
}); // In case nothing before handled hot-update - send 404.
|
|
// This fixes "Unexpected token < in JSON at position 0" runtime
|
|
// errors after restarting development server and
|
|
// cause automatic hard refresh in the browser.
|
|
|
|
app.use(/.*\.hot-update\.json$/i, (req, res) => {
|
|
res.status(404).end();
|
|
}); // Render an HTML page and serve it.
|
|
|
|
app.use((req, res, next) => {
|
|
res.sendFile(directoryPath(`public/index.html`), err => {
|
|
if (err) {
|
|
res.status(500).end();
|
|
}
|
|
});
|
|
});
|
|
/**
|
|
* Set up the HTTP server and socket.io.
|
|
**/
|
|
|
|
let server = require(`http`).Server(app); // If a SSL cert exists in program, use it with `createServer`.
|
|
|
|
|
|
if (program.ssl) {
|
|
server = require(`https`).createServer(program.ssl, app);
|
|
}
|
|
|
|
websocketManager.init({
|
|
server,
|
|
directory: program.directory
|
|
});
|
|
const socket = websocketManager.getSocket();
|
|
const listener = server.listen(program.port, program.host, err => {
|
|
if (err) {
|
|
if (err.code === `EADDRINUSE`) {
|
|
// eslint-disable-next-line max-len
|
|
report.panic(`Unable to start Gatsby on port ${program.port} as there's already a process listening on that port.`);
|
|
return;
|
|
}
|
|
|
|
report.panic(`There was a problem starting the development server`, err);
|
|
}
|
|
}); // Register watcher that rebuilds index.html every time html.js changes.
|
|
|
|
const watchGlobs = [`src/html.js`, `plugins/**/gatsby-ssr.js`].map(path => slash(directoryPath(path)));
|
|
chokidar.watch(watchGlobs).on(`change`, async () => {
|
|
await createIndexHtml();
|
|
socket.to(`clients`).emit(`reload`);
|
|
});
|
|
return [compiler, listener];
|
|
}
|
|
|
|
module.exports = async program => {
|
|
initTracer(program.openTracingConfigFile);
|
|
telemetry.trackCli(`DEVELOP_START`);
|
|
telemetry.startBackgroundUpdate();
|
|
const port = typeof program.port === `string` ? parseInt(program.port, 10) : program.port; // In order to enable custom ssl, --cert-file --key-file and -https flags must all be
|
|
// used together
|
|
|
|
if ((program[`cert-file`] || program[`key-file`]) && !program.https) {
|
|
report.panic(`for custom ssl --https, --cert-file, and --key-file must be used together`);
|
|
} // Check if https is enabled, then create or get SSL cert.
|
|
// Certs are named after `name` inside the project's package.json.
|
|
// Scoped names are converted from @npm/package-name to npm--package-name
|
|
|
|
|
|
if (program.https) {
|
|
program.ssl = await getSslCert({
|
|
name: program.sitePackageJson.name.replace(`@`, ``).replace(`/`, `--`),
|
|
certFile: program[`cert-file`],
|
|
keyFile: program[`key-file`],
|
|
directory: program.directory
|
|
});
|
|
}
|
|
|
|
program.port = await detectPortInUseAndPrompt(port, rlInterface); // Start bootstrap process.
|
|
|
|
const {
|
|
graphqlRunner
|
|
} = await bootstrap(program); // Start the createPages hot reloader.
|
|
|
|
require(`../bootstrap/page-hot-reloader`)(graphqlRunner);
|
|
|
|
const queryIds = queryUtil.calcInitialDirtyQueryIds(store.getState());
|
|
const {
|
|
staticQueryIds,
|
|
pageQueryIds
|
|
} = queryUtil.groupQueryIds(queryIds);
|
|
let activity = report.activityTimer(`run static queries`);
|
|
activity.start();
|
|
await queryUtil.processStaticQueries(staticQueryIds, {
|
|
activity,
|
|
state: store.getState()
|
|
});
|
|
activity.end();
|
|
activity = report.activityTimer(`run page queries`);
|
|
activity.start();
|
|
await queryUtil.processPageQueries(pageQueryIds, {
|
|
activity
|
|
});
|
|
activity.end();
|
|
|
|
require(`../redux/actions`).boundActionCreators.setProgramStatus(`BOOTSTRAP_QUERY_RUNNING_FINISHED`);
|
|
|
|
await waitJobsFinished();
|
|
requiresWriter.startListener();
|
|
db.startAutosave();
|
|
queryUtil.startListening(queryQueue.createDevelopQueue());
|
|
queryWatcher.startWatchDeletePage();
|
|
const [compiler] = await startServer(program);
|
|
|
|
function prepareUrls(protocol, host, port) {
|
|
const formatUrl = hostname => url.format({
|
|
protocol,
|
|
hostname,
|
|
port,
|
|
pathname: `/`
|
|
});
|
|
|
|
const prettyPrintUrl = hostname => url.format({
|
|
protocol,
|
|
hostname,
|
|
port: chalk.bold(port),
|
|
pathname: `/`
|
|
});
|
|
|
|
const isUnspecifiedHost = host === `0.0.0.0` || host === `::`;
|
|
let lanUrlForConfig, lanUrlForTerminal;
|
|
|
|
if (isUnspecifiedHost) {
|
|
try {
|
|
// This can only return an IPv4 address
|
|
lanUrlForConfig = address.ip();
|
|
|
|
if (lanUrlForConfig) {
|
|
// Check if the address is a private ip
|
|
// https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
|
|
if (/^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(lanUrlForConfig)) {
|
|
// Address is private, format it for later use
|
|
lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
|
|
} else {
|
|
// Address is not private, so we will discard it
|
|
lanUrlForConfig = undefined;
|
|
}
|
|
}
|
|
} catch (_e) {// ignored
|
|
}
|
|
} // TODO collect errors (GraphQL + Webpack) in Redux so we
|
|
// can clear terminal and print them out on every compile.
|
|
// Borrow pretty printing code from webpack plugin.
|
|
|
|
|
|
const localUrlForTerminal = prettyPrintUrl(host);
|
|
const localUrlForBrowser = formatUrl(host);
|
|
return {
|
|
lanUrlForConfig,
|
|
lanUrlForTerminal,
|
|
localUrlForTerminal,
|
|
localUrlForBrowser
|
|
};
|
|
}
|
|
|
|
function printInstructions(appName, urls, useYarn) {
|
|
report._setStage({
|
|
stage: `DevelopBootstrapFinished`,
|
|
context: {
|
|
url: urls.localUrlForBrowser,
|
|
appName
|
|
}
|
|
});
|
|
|
|
console.log(`You can now view ${chalk.bold(appName)} in the browser.`);
|
|
console.log();
|
|
|
|
if (urls.lanUrlForTerminal) {
|
|
console.log(` ${chalk.bold(`Local:`)} ${urls.localUrlForTerminal}`);
|
|
console.log(` ${chalk.bold(`On Your Network:`)} ${urls.lanUrlForTerminal}`);
|
|
} else {
|
|
console.log(` ${urls.localUrlForTerminal}`);
|
|
}
|
|
|
|
console.log();
|
|
console.log(`View ${process.env.GATSBY_GRAPHQL_IDE === `playground` ? `the GraphQL Playground` : `GraphiQL`}, an in-browser IDE, to explore your site's data and schema`);
|
|
console.log();
|
|
|
|
if (urls.lanUrlForTerminal) {
|
|
console.log(` ${chalk.bold(`Local:`)} ${urls.localUrlForTerminal}___graphql`);
|
|
console.log(` ${chalk.bold(`On Your Network:`)} ${urls.lanUrlForTerminal}___graphql`);
|
|
} else {
|
|
console.log(` ${urls.localUrlForTerminal}___graphql`);
|
|
}
|
|
|
|
console.log();
|
|
console.log(`Note that the development build is not optimized.`);
|
|
console.log(`To create a production build, use ` + `${chalk.cyan(`npm run build`)}`);
|
|
console.log();
|
|
}
|
|
|
|
function printDeprecationWarnings() {
|
|
const deprecatedApis = [`boundActionCreators`, `pathContext`];
|
|
const fixMap = {
|
|
boundActionCreators: {
|
|
newName: `actions`,
|
|
docsLink: `https://gatsby.dev/boundActionCreators`
|
|
},
|
|
pathContext: {
|
|
newName: `pageContext`,
|
|
docsLink: `https://gatsby.dev/pathContext`
|
|
}
|
|
};
|
|
const deprecatedLocations = {};
|
|
deprecatedApis.forEach(api => deprecatedLocations[api] = []);
|
|
glob.sync(`{,!(node_modules|public)/**/}*.js`, {
|
|
nodir: true
|
|
}).forEach(file => {
|
|
const fileText = fs.readFileSync(file);
|
|
const matchingApis = deprecatedApis.filter(api => fileText.indexOf(api) !== -1);
|
|
matchingApis.forEach(api => deprecatedLocations[api].push(file));
|
|
});
|
|
deprecatedApis.forEach(api => {
|
|
if (deprecatedLocations[api].length) {
|
|
console.log(`%s %s %s %s`, chalk.cyan(api), chalk.yellow(`is deprecated. Please use`), chalk.cyan(fixMap[api].newName), chalk.yellow(`instead. For migration instructions, see ${fixMap[api].docsLink}\nCheck the following files:`));
|
|
console.log();
|
|
deprecatedLocations[api].forEach(file => console.log(file));
|
|
console.log();
|
|
}
|
|
});
|
|
}
|
|
|
|
let isFirstCompile = true; // "done" event fires when Webpack has finished recompiling the bundle.
|
|
// Whether or not you have warnings or errors, you will get this event.
|
|
|
|
compiler.hooks.done.tapAsync(`print getsby instructions`, (stats, done) => {
|
|
// We have switched off the default Webpack output in WebpackDevServer
|
|
// options so we are going to "massage" the warnings and errors and present
|
|
// them in a readable focused way.
|
|
const messages = formatWebpackMessages(stats.toJson({}, true));
|
|
const urls = prepareUrls(program.ssl ? `https` : `http`, program.host, program.port);
|
|
const isSuccessful = !messages.errors.length; // if (isSuccessful) {
|
|
// console.log(chalk.green(`Compiled successfully!`))
|
|
// }
|
|
// if (isSuccessful && (isInteractive || isFirstCompile)) {
|
|
|
|
if (isSuccessful && isFirstCompile) {
|
|
printInstructions(program.sitePackageJson.name, urls, program.useYarn);
|
|
printDeprecationWarnings();
|
|
|
|
if (program.open) {
|
|
Promise.resolve(openurl(urls.localUrlForBrowser)).catch(err => console.log(`${chalk.yellow(`warn`)} Browser not opened because no browser was found`));
|
|
}
|
|
}
|
|
|
|
isFirstCompile = false; // If errors exist, only show errors.
|
|
// if (messages.errors.length) {
|
|
// // Only keep the first error. Others are often indicative
|
|
// // of the same problem, but confuse the reader with noise.
|
|
// if (messages.errors.length > 1) {
|
|
// messages.errors.length = 1
|
|
// }
|
|
// console.log(chalk.red("Failed to compile.\n"))
|
|
// console.log(messages.errors.join("\n\n"))
|
|
// return
|
|
// }
|
|
// Show warnings if no errors were found.
|
|
// if (messages.warnings.length) {
|
|
// console.log(chalk.yellow("Compiled with warnings.\n"))
|
|
// console.log(messages.warnings.join("\n\n"))
|
|
// // Teach some ESLint tricks.
|
|
// console.log(
|
|
// "\nSearch for the " +
|
|
// chalk.underline(chalk.yellow("keywords")) +
|
|
// " to learn more about each warning."
|
|
// )
|
|
// console.log(
|
|
// "To ignore, add " +
|
|
// chalk.cyan("// eslint-disable-next-line") +
|
|
// " to the line before.\n"
|
|
// )
|
|
// }
|
|
|
|
done();
|
|
});
|
|
};
|
|
//# sourceMappingURL=develop.js.map
|