gatsby
Version:
Blazing fast modern site generator for React
452 lines (437 loc) • 17.2 kB
JavaScript
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.onCreateDevServer = onCreateDevServer;
exports.onPreBootstrap = onPreBootstrap;
var _unionBy2 = _interopRequireDefault(require("lodash/unionBy"));
var _kebabCase2 = _interopRequireDefault(require("lodash/kebabCase"));
var _union2 = _interopRequireDefault(require("lodash/union"));
var _fsExtra = _interopRequireDefault(require("fs-extra"));
var _glob = _interopRequireDefault(require("glob"));
var _path2 = _interopRequireDefault(require("path"));
var _webpack = _interopRequireDefault(require("webpack"));
var _gatsbyCoreUtils = require("gatsby-core-utils");
var _formatWebpackMessages = _interopRequireDefault(require("react-dev-utils/formatWebpackMessages"));
var _dotenv = _interopRequireDefault(require("dotenv"));
var _chokidar = _interopRequireDefault(require("chokidar"));
var _webpackErrorUtils = require("../../utils/webpack-error-utils");
var _actions = require("../../redux/actions");
var _middleware = require("./middleware");
var _module = _interopRequireDefault(require("module"));
const isProductionEnv = process.env.gatsby_executing_command !== `develop`;
// During development, we lazily compile functions only when they're requested.
// Here we keep track of which functions have been requested so are "active"
const activeDevelopmentFunctions = new Set();
let activeEntries = {};
async function ensureFunctionIsCompiled(functionObj, compiledFunctionsDir) {
// stat the compiled function. If it's there, then return.
let compiledFileExists = false;
try {
compiledFileExists = !!(await _fsExtra.default.stat(functionObj.absoluteCompiledFilePath));
} catch (e) {
// ignore
}
if (compiledFileExists) {
return;
} else {
// Otherwise, restart webpack by touching the file and watch for the file to be
// compiled.
const time = new Date();
_fsExtra.default.utimesSync(functionObj.originalAbsoluteFilePath, time, time);
await new Promise(resolve => {
const watcher = _chokidar.default
// Watch the root of the compiled function directory in .cache as chokidar
// can't watch files in directories that don't yet exist.
.watch(compiledFunctionsDir).on(`add`, async _path => {
if (_path === functionObj.absoluteCompiledFilePath) {
await watcher.close();
resolve(null);
}
});
});
}
}
// Create glob type w/ glob, plugin name, root path
const createGlobArray = (siteDirectoryPath, plugins) => {
const globs = [];
function globIgnorePatterns(root, pluginName) {
const nestedFolder = pluginName ? `/${pluginName}/**/` : `/**/`;
return [`${root}/src/api${nestedFolder}__tests__/**/*.+(js|ts)`,
// Jest tests
`${root}/src/api${nestedFolder}+(*.)+(spec|test).+(js|ts)`, `${root}/src/api${nestedFolder}+(*.)+(d).ts` // .d.ts files
];
}
// Add the default site src/api directory.
globs.push({
globPattern: `${siteDirectoryPath}/src/api/**/*.{js,ts}`,
ignorePattern: globIgnorePatterns(siteDirectoryPath),
rootPath: _path2.default.join(siteDirectoryPath, `src/api`),
pluginName: `default-site-plugin`
});
// Add each plugin
plugins.forEach(plugin => {
// Ignore the "default" site plugin (aka the src tree) as we're
// already watching that.
if (plugin.name === `default-site-plugin`) {
return;
}
// Ignore any plugins we include by default. In the very unlikely case
// we want to ship default functions, we'll special case add them. In the
// meantime, we'll avoid extra FS IO.
if (plugin.resolve.includes(`internal-plugin`)) {
return;
}
if (plugin.resolve.includes(`gatsby-plugin-typescript`)) {
return;
}
if (plugin.resolve.includes(`gatsby-plugin-page-creator`)) {
return;
}
const glob = {
globPattern: `${plugin.resolve}/src/api/${plugin.name}/**/*.{js,ts}`,
ignorePattern: globIgnorePatterns(plugin.resolve, plugin.name),
rootPath: _path2.default.join(plugin.resolve, `src/api`),
pluginName: plugin.name
};
globs.push(glob);
});
// Only return unique paths
return (0, _union2.default)(globs);
};
async function globAsync(pattern, options = {}) {
return await new Promise((resolve, reject) => {
(0, _glob.default)(pattern, options, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});
}
const createWebpackConfig = async ({
siteDirectoryPath,
store,
reporter
}) => {
const compiledFunctionsDir = _path2.default.join(siteDirectoryPath, `.cache`, `functions`);
const globs = createGlobArray(siteDirectoryPath, store.getState().flattenedPlugins);
const seenFunctionIds = new Set();
// Glob and return object with relative/absolute paths + which plugin
// they belong to.
const allFunctions = await Promise.all(globs.map(async glob => {
const knownFunctions = [];
const files = await globAsync(glob.globPattern, {
ignore: glob.ignorePattern
});
files.map(file => {
const originalAbsoluteFilePath = file;
const originalRelativeFilePath = _path2.default.relative(glob.rootPath, file);
const {
dir,
name
} = _path2.default.parse(originalRelativeFilePath);
// Ignore the original extension as all compiled functions now end with js.
const compiledFunctionName = _path2.default.join(dir, name + `.js`);
const compiledPath = _path2.default.join(compiledFunctionsDir, compiledFunctionName);
const finalName = (0, _gatsbyCoreUtils.urlResolve)(dir, name === `index` ? `` : name);
// functionId should have only alphanumeric characters and dashes
const functionIdBase = (0, _kebabCase2.default)(compiledFunctionName).replace(/[^a-zA-Z0-9-]/g, `-`);
let functionId = functionIdBase;
if (seenFunctionIds.has(functionId)) {
let counter = 2;
do {
functionId = `${functionIdBase}-${counter}`;
counter++;
} while (seenFunctionIds.has(functionId));
}
knownFunctions.push({
functionRoute: finalName,
pluginName: glob.pluginName,
originalAbsoluteFilePath,
originalRelativeFilePath,
relativeCompiledFilePath: compiledFunctionName,
absoluteCompiledFilePath: compiledPath,
matchPath: (0, _gatsbyCoreUtils.getMatchPath)(finalName),
functionId
});
});
return knownFunctions;
}));
// Combine functions by the route name so that functions in the default
// functions directory can override the plugin's implementations.
// @ts-ignore - Seems like a TS bug: https://github.com/microsoft/TypeScript/issues/28010#issuecomment-713484584
const knownFunctions = (0, _unionBy2.default)(...allFunctions, func => func.functionRoute);
store.dispatch(_actions.internalActions.setFunctions(knownFunctions));
// Write out manifest for use by `gatsby serve` and plugins
_fsExtra.default.writeFileSync(_path2.default.join(compiledFunctionsDir, `manifest.json`), JSON.stringify(knownFunctions, null, 4));
const {
config: {
pathPrefix
},
program
} = store.getState();
// Load environment variables from process.env.* and .env.* files.
// Logic is shared with webpack.config.js
// node env should be DEVELOPMENT | PRODUCTION as these are commonly used in node land
const nodeEnv = process.env.NODE_ENV || `development`;
// config env is dependent on the env that it's run, this can be anything from staging-production
// this allows you to set use different .env environments or conditions in gatsby files
const configEnv = process.env.GATSBY_ACTIVE_ENV || nodeEnv;
const envFile = _path2.default.join(siteDirectoryPath, `./.env.${configEnv}`);
let parsed = {};
try {
parsed = _dotenv.default.parse(_fsExtra.default.readFileSync(envFile, {
encoding: `utf8`
}));
} catch (err) {
if (err.code !== `ENOENT`) {
reporter.error(`There was a problem processing the .env file (${envFile})`, err);
}
}
const envObject = Object.keys(parsed).reduce((acc, key) => {
acc[key] = JSON.stringify(parsed[key]);
return acc;
}, {});
const varsFromProcessEnv = Object.keys(process.env).reduce((acc, key) => {
acc[key] = JSON.stringify(process.env[key]);
return acc;
}, {});
// Don't allow overwriting of NODE_ENV, PUBLIC_DIR as to not break gatsby things
envObject.NODE_ENV = JSON.stringify(nodeEnv);
envObject.PUBLIC_DIR = JSON.stringify(`${siteDirectoryPath}/public`);
const mergedEnvVars = Object.assign(envObject, varsFromProcessEnv);
const processEnvVars = Object.keys(mergedEnvVars).reduce((acc, key) => {
acc[`process.env.${key}`] = mergedEnvVars[key];
return acc;
}, {
"process.env": `({})`
});
const entries = {};
const precompileDevFunctions = isProductionEnv || process.env.GATSBY_PRECOMPILE_DEVELOP_FUNCTIONS === `true` || process.env.GATSBY_PRECOMPILE_DEVELOP_FUNCTIONS === `1`;
const functionsList = precompileDevFunctions ? knownFunctions : activeDevelopmentFunctions;
functionsList.forEach(functionObj => {
var _functionObj$matchPat;
// Get path without the extension (as it could be ts or js)
const parsedFile = _path2.default.parse(functionObj.originalRelativeFilePath);
const compiledNameWithoutExtension = _path2.default.join(parsedFile.dir, parsedFile.name);
let entryToTheFunction = functionObj.originalAbsoluteFilePath;
// we wrap user defined function with our preamble that handles matchPath as well as body parsing
// see api-function-webpack-loader.ts for more info
entryToTheFunction += `?matchPath=` + ((_functionObj$matchPat = functionObj.matchPath) !== null && _functionObj$matchPat !== void 0 ? _functionObj$matchPat : ``);
entries[compiledNameWithoutExtension] = entryToTheFunction;
});
activeEntries = entries;
const stage = isProductionEnv ? `functions-production` : `functions-development`;
const gatsbyPluginTSRequire = _module.default.createRequire(require.resolve(`gatsby-plugin-typescript`));
return {
entry: entries,
output: {
path: compiledFunctionsDir,
filename: `[name].js`,
libraryTarget: `commonjs2`
},
target: `node`,
// Minification is expensive and not as helpful for serverless functions.
optimization: {
minimize: false
},
// Resolve files ending with .ts and the default extensions of .js, .json, .wasm
resolve: {
extensions: [`.ts`, `...`]
},
// Have webpack save its cache to the .cache/webpack directory
cache: {
type: `filesystem`,
name: stage,
cacheLocation: _path2.default.join(siteDirectoryPath, `.cache`, `webpack`, `stage-` + stage)
},
mode: isProductionEnv ? `production` : `development`,
// watch: !isProductionEnv,
module: {
rules: [
// Webpack expects extensions when importing ESM modules as that's what the spec describes.
// Not all libraries have adapted so we don't enforce its behaviour
// @see https://github.com/webpack/webpack/issues/11467
{
test: /\.[tj]sx?$/,
resourceQuery: /matchPath/,
use: {
loader: require.resolve(`./api-function-webpack-loader`)
}
}, {
test: /\.mjs$/i,
resolve: {
byDependency: {
esm: {
fullySpecified: false
}
}
}
}, {
test: /\.js$/i,
descriptionData: {
type: `module`
},
resolve: {
byDependency: {
esm: {
fullySpecified: false
}
}
},
use: {
loader: require.resolve(`babel-loader`),
options: {
presets: [gatsbyPluginTSRequire.resolve(`@babel/preset-typescript`)]
}
}
}, {
test: [/.js$/, /.ts$/],
exclude: /node_modules/,
use: {
loader: require.resolve(`babel-loader`),
options: {
presets: [gatsbyPluginTSRequire.resolve(`@babel/preset-typescript`)]
}
}
}]
},
plugins: [new _webpack.default.DefinePlugin({
PREFIX_TO_STRIP: JSON.stringify(program.prefixPaths ? pathPrefix === null || pathPrefix === void 0 ? void 0 : pathPrefix.replace(/(^\/+|\/+$)/g, ``) : ``),
...processEnvVars
}), new _webpack.default.IgnorePlugin({
checkResource(resource) {
if (resource === `lmdb`) {
reporter.warn(`LMDB and other modules with native dependencies are not supported in Gatsby Functions.\nIf you are importing utils from \`gatsby-core-utils\`, make sure to import from a specific module (for example \`gatsby-core-utils/create-content-digest\`).`);
return true;
}
return false;
}
})]
};
};
let isFirstBuild = true;
async function onPreBootstrap({
reporter,
store,
parentSpan
}) {
const activity = reporter.activityTimer(`Compiling Gatsby Functions`, {
parentSpan
});
activity.start();
const {
program: {
directory: siteDirectoryPath
}
} = store.getState();
const compiledFunctionsDir = _path2.default.join(siteDirectoryPath, `.cache`, `functions`);
await _fsExtra.default.ensureDir(compiledFunctionsDir);
await _fsExtra.default.emptyDir(compiledFunctionsDir);
try {
// We do this ungainly thing as we need to make accessible
// the resolve/reject functions to our shared callback function
// eslint-disable-next-line
await new Promise(async (resolve, reject) => {
const config = await createWebpackConfig({
siteDirectoryPath,
store,
reporter
});
function callback(err, stats) {
const rawMessages = stats === null || stats === void 0 ? void 0 : stats.toJson({
all: false,
warnings: true,
errors: true
});
if (rawMessages !== null && rawMessages !== void 0 && rawMessages.warnings && rawMessages.warnings.length > 0) {
(0, _webpackErrorUtils.reportWebpackWarnings)(rawMessages.warnings, reporter);
}
if (err) return reject(err);
const errors = (stats === null || stats === void 0 ? void 0 : stats.compilation.errors) || [];
// If there's errors, reject in production and print to the console
// in development.
if (isProductionEnv) {
if (errors.length > 0) return reject(errors);
} else {
const formatted = (0, _formatWebpackMessages.default)({
errors: rawMessages !== null && rawMessages !== void 0 && rawMessages.errors ? rawMessages.errors.map(e => e.message) : [],
warnings: []
});
reporter.error(formatted.errors);
}
// Log success in dev
if (!isProductionEnv) {
if (isFirstBuild) {
isFirstBuild = false;
} else {
reporter.success(`Re-building functions`);
}
}
return resolve(null);
}
if (isProductionEnv) {
(0, _webpack.default)(config).run(callback);
} else {
// When in watch mode, you call things differently
let compiler = (0, _webpack.default)(config).watch({}, callback);
const globs = createGlobArray(siteDirectoryPath, store.getState().flattenedPlugins);
// Watch for env files to change and restart the webpack watcher.
_chokidar.default.watch([`${siteDirectoryPath}/.env*`, ...globs.map(glob => glob.globPattern)], {
ignoreInitial: true
}).on(`all`, async (event, path) => {
// Ignore change events from the API directory for functions we're
// already watching.
if (event === `change` && Object.values(activeEntries).includes(path) && path.includes(`/src/api/`)) {
return;
}
reporter.log(`Restarting function watcher due to change to "${path}"`);
// Otherwise, restart the watcher
compiler.close(async () => {
const config = await createWebpackConfig({
siteDirectoryPath,
store,
reporter
});
compiler = (0, _webpack.default)(config).watch({}, callback);
});
});
}
});
} catch (error) {
activity.panic({
id: `11332`,
error,
context: {}
});
}
activity.end();
}
async function onCreateDevServer({
reporter,
app,
store
}) {
reporter.verbose(`Attaching functions to development server`);
const {
program: {
directory: siteDirectoryPath
}
} = store.getState();
const compiledFunctionsDir = _path2.default.join(siteDirectoryPath, `.cache`, `functions`);
app.use(`/api/*`, ...(0, _middleware.functionMiddlewares)({
getFunctions() {
const {
functions
} = store.getState();
return functions;
},
async prepareFn(functionObj) {
activeDevelopmentFunctions.add(functionObj);
await ensureFunctionIsCompiled(functionObj, compiledFunctionsDir);
},
showDebugMessageInResponse: true
}));
}
//# sourceMappingURL=gatsby-node.js.map
;