"use strict";

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");
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);

  // 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);
      knownFunctions.push({
        functionRoute: finalName,
        pluginName: glob.pluginName,
        originalAbsoluteFilePath,
        originalRelativeFilePath,
        relativeCompiledFilePath: compiledFunctionName,
        absoluteCompiledFilePath: compiledPath,
        matchPath: (0, _gatsbyCoreUtils.getMatchPath)(finalName),
        functionId: (0, _kebabCase2.default)(compiledFunctionName)
      });
    });
    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 => {
    // 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;
    // if function has dynamic path, we inject it with webpack loader via query param
    // see match-path-webpack-loader.ts for more info
    if (functionObj.matchPath) {
      entryToTheFunction += `?matchPath=` + functionObj.matchPath;
    }
    entries[compiledNameWithoutExtension] = entryToTheFunction;
  });
  activeEntries = entries;
  const stage = isProductionEnv ? `functions-production` : `functions-development`;
  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(`./match-path-webpack-loader`)
        }
      }, {
        test: /\.mjs$/i,
        resolve: {
          byDependency: {
            esm: {
              fullySpecified: false
            }
          }
        }
      }, {
        test: /\.js$/i,
        descriptionData: {
          type: `module`
        },
        resolve: {
          byDependency: {
            esm: {
              fullySpecified: false
            }
          }
        },
        use: {
          loader: `babel-loader`,
          options: {
            presets: [`@babel/typescript`]
          }
        }
      }, {
        test: [/.js$/, /.ts$/],
        exclude: /node_modules/,
        use: {
          loader: `babel-loader`,
          options: {
            presets: [`@babel/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