// this should be loaded first
const opentelemetry = require('./lib/opentelemetry');
const path = require('path');
const url = require('url');
const _ = require('lodash');
const argv = require('boring')({ end: true });
const fs = require('fs');
const { stripIndent } = require('common-tags');
const cluster = require('cluster');
const { cpus } = require('os');
const process = require('process');
const npmResolve = require('resolve');
const glob = require('./lib/glob.js');
const moogRequire = require('./lib/moog-require');
let defaults = require('./defaults.js');

// ## Top-level options
//
// `cluster`
//
// If set to `true`, Apostrophe will spawn as many processes as
// there are CPU cores on the server, or a minimum of 2, and balance
// incoming connections among them. This ensures availability while one
// process is restarting due to a crash and also increases scalability if
// the server has multiple CPU cores.
//
// If set to an object with a `processes` property, that many
// processes are started. If `processes` is 0 or a negative number,
// it is added to the number of CPU cores reported by the server.
// Notably, `-1` can be a good way to reserve one CPU core for MongoDB
// in a single-server deployment.
//
// However when in cluster mode no fewer than 2 processes will be
// started as there is no availability benefit without at least 2.
//
// If a child process exits with a failure status code it will be
// restarted. However, if it exits in less than 20 seconds after
// startup there will be a 20 second delay to avoid flooding logs
// and pinning the CPU.
//
// Alternatively the `APOS_CLUSTER_PROCESSES` environment variable
// can be set to a number, which will effectively set the cluster
// option to `cluster: { processes: n }`.
//
// `openTelemetryProvider`
//
// If set, Apostrophe will register it as a global OpenTelemetry tracer
// provider. The expected value is an object, an instance of TracerProvider. If
// the Node SDK is used in the application instead of manual configuration, the
// provider instance is only available as a private property:
// `sdkInstance._tracerProvider`. An issue can be opened to discuss the exposure
// of a public getter with the OpenTelemetry developers.
//
// `beforeExit`
//
// If set, Apostrophe will invoke it (await) before invoking process.exit.
// `beforeExit` may be an async function, will be awaited, and takes no
// arguments.
//
// `pnpm`
// A boolean to force on or off the pnpm related build routines. If not set,
// an automated check will be performed to determine if pnpm is in use. We offer
// an option, because automated check is not 100% reliable. Monorepo tools are
// often hiding package management specifics (lock files, node_module
// structure, etc.) in a centralized store.
//
// ## Awaiting the Apostrophe function
//
// The apos function is async, but in typical cases you do not
// need to await it. If you simply call it, Apostrophe will
// start up and listen for connections forever, or run a
// task and exit, as appropriate. On failure, the error is
// printed to stderr and the process exits.
//
// If you do `await` the function, then your code will continue
// after apostrophe successfully begins listening for
// connections. However note it will still exit on errors.
//
// To avoid exiting on errors, pass the `exit: false` option.
// This can option also can be used to allow awaiting a command line
// task, as they also normally exit on completion.
//
// If `options.cluster` is truthy, the function quickly resolves to
// `null` in the primary process. In the child process it resolves as
// documented above.

// The actual entry point, a wrapper that enables the telemetry and starts the
// root span
module.exports = async function(options) {
  const telemetry = opentelemetry(options);
  let spanName = 'apostrophe:boot';
  const guardTime = 20000;

  // Detect cluster options
  if (process.env.APOS_CLUSTER_PROCESSES) {
    options.cluster = {
      processes: parseInt(process.env.APOS_CLUSTER_PROCESSES)
    };
  }
  if (options.cluster && (process.env.NODE_ENV !== 'production')) {
    console.log('NODE_ENV is not set to production, disabling cluster mode');
    options.cluster = false;
  }

  // Execute if cluster enabled
  if (options.cluster && !argv._.length) {
    // For bc with node 14 and below we need to check both
    if (cluster.isPrimary || cluster.isMaster) {
      // Activate and return the callback return value
      return telemetry.startActiveSpan(`${spanName}:primary`, async (span) => {
        let processes = options.cluster.processes || cpus().length;
        if (processes <= 0) {
          processes = cpus().length + processes;
        }
        let capped = '';
        if (processes > cpus().length) {
          processes = cpus().length;
          capped = ' (capped to number of CPU cores)';
        }
        if (processes < 2) {
          processes = 2;
          if (capped) {
            capped = ' (less than 2 cores, capped to minimum of 2)';
          } else {
            capped = ' (using minimum of 2)';
          }
        }
        console.log(`Starting ${processes} cluster child processes${capped}`);
        for (let i = 0; i < processes; i++) {
          clusterFork();
        }
        cluster.on('exit', (worker, code, signal) => {
          if (code !== 0) {
            if ((Date.now() - worker.bornAt) < guardTime) {
              console.error(`Worker process ${worker.process.pid} failed in ${seconds(Date.now() - worker.bornAt)}, waiting ${seconds(guardTime)} before restart`);
              setTimeout(() => {
                respawn(worker);
              }, guardTime);
            } else {
              respawn(worker);
            }
          }
        });
        span.end();
        if (typeof options.beforeExit === 'function') {
          await options.beforeExit();
        }
        return null;
      });
    } else {
      // continue as a worker operation, the pid should be recorded
      // by the auto instrumentation
      spanName += ':worker';
      console.log(`Cluster worker ${process.pid} started`);
    }
  }

  // Create and activate the root span for the boot tracer
  const self = await telemetry.startActiveSpan(spanName, async (span) => {
    const res = await apostrophe(options, telemetry, span);
    span.setStatus(telemetry.api.SpanStatusCode.OK);
    span.end();
    return res;
  });

  return self;
};

// The actual apostrophe bootstrap
async function apostrophe(options, telemetry, rootSpan) {
  // The core is not a true moog object but it must look enough like one
  // to participate as an async event emitter
  const self = {
    __meta: {
      name: 'apostrophe'
    }
  };

  // Terminates the process. Emits the `apostrophe:beforeExit` async event;
  // use this mechanism to invoke any pre-exit application level tasks. Any
  // `beforeExit` handler errors will be ignored.
  // Invokes and awaits `options.beforeExit` function if available,
  // passing as arguments the exit code and message (if any).
  self._exit = async function(code = 0, message) {
    try {
      if (self.emit) {
        await self.emit('beforeExit');
      }
    } catch (e) {
      // we are at the point where errors are ignored,
      // if emitter is already registered, all handler errors
      // are already recorded by the event module instrumentation
      console.error('beforeExit emit error', e);
    }

    if (code !== 0) {
      telemetry.handleError(rootSpan, message);
    } else {
      rootSpan.setStatus({
        code: telemetry.api.SpanStatusCode.OK,
        message
      });
    }
    rootSpan.end();

    if (typeof options.beforeExit === 'function') {
      try {
        await options.beforeExit(code, message);
      } catch (e) {
        console.error('beforeExit handler error', e);
      }
    }
    process.exit(code);
  };

  try {
    const matches = process.version.match(/^v(\d+)/);
    const version = parseInt(matches[1]);
    if (version < 18) {
      throw new Error('Apostrophe requires at least Node.js 18.x.');
    }
    // The core must have a reference to itself in order to use the
    // promise event emitter code
    self.apos = self;

    // Register the telemetry API as a pseudo module
    self.apos.telemetry = telemetry;

    Object.assign(self, require('./modules/@apostrophecms/module/lib/events.js')(self));

    // Determine root module and root directory

    const {
      root,
      rootDir,
      npmRootDir,
      selfDir
    } = buildRoot(options);
    self.root = root;
    self.rootDir = rootDir;
    self.npmRootDir = npmRootDir;
    self.selfDir = selfDir;
    self.getNpmPath = (name) => {
      try {
        return getNpmPath(name, self.npmRootDir);
      } catch (e) {
        // Not found via npm. This does not mean it doesn't
        // exist as a project-level thing
        return null;
      }
    };

    // Signals to various (build related) places that we are running a pnpm
    // installation. The relevant option, if set, has a higher precedence over
    // the automated check.
    self.isPnpm = options.pnpm ??
      fs.existsSync(path.join(self.npmRootDir, 'pnpm-lock.yaml'));

    testModule();

    self.options = await mergeConfiguration(options, defaults);
    await autodetectBundles();
    acceptGlobalOptions();

    // Module-based async events (self.on and self.emit of each module,
    // handlers are usually registered via `handlers` in the module
    // definition rather than `self.on`)
    self.eventHandlers = {};

    // Destroys the Apostrophe object, freeing resources such as
    // HTTP server ports and database connections. Does **not**
    // delete any data; the persistent database and media files
    // remain available for the next startup. Emits the
    // `apostrophe:destroy` async event; use this mechanism to free your own
    // server-side resources that could prevent garbage
    // collection by the JavaScript engine, such as timers
    // and intervals.
    self.destroy = async function() {
      await self.emit('destroy');
    };

    // Returns true if Apostrophe is running as a command line task
    // rather than as a server
    self.isTask = function() {
      return !!self.argv._.length;
    };

    // Returns an array of modules that are instances of the given
    // module name, i.e. they are of that type or they extend it.
    // For instance, `apos.instancesOf('@apostrophecms/piece-type')` returns
    // an array of active modules in your project that extend
    // pieces, such as `@apostrophecms/user` and
    // your own piece types

    self.instancesOf = function(name) {
      return _.filter(self.modules, function(apostropheModule) {
        return self.synth.instanceOf(apostropheModule, name);
      });
    };

    // Returns true if the object is an instance of the given
    // moog class name or a subclass thereof. A convenience wrapper
    // for `apos.synth.instanceOf`

    self.instanceOf = function(object, name) {
      return self.synth.instanceOf(object, name);
    };

    // So the asset module can figure out what other modules
    // are out there and what icons they need without
    // actually instantiating them
    self.modulesToBeInstantiated = modulesToBeInstantiated;
    self.eventAliases = {};
    self.aliasEvent('modulesReady', 'modulesRegistered');
    self.aliasEvent('afterInit', 'ready');

    await defineModules();

    await instantiateModules();
    await lintModules();
    await self.emit('modulesRegistered'); // formerly modulesReady
    self.apos.schema.validateAllSchemas();
    self.apos.schema.registerAllSchemas();
    await self.apos.lock.withLock('@apostrophecms/migration:migrate', async () => {
      await self.apos.migration.migrate(self.argv);
      // Inserts the global doc in the default locale if it does not exist;
      // same for other singleton piece types registered by other modules
      for (const apostropheModule of Object.values(self.modules)) {
        if (self.instanceOf(apostropheModule, '@apostrophecms/piece-type') && apostropheModule.options.singletonAuto) {
          await apostropheModule.insertIfMissing();
        }
      }
      await self.apos.page.implementParkAllInDefaultLocale();
      await self.apos.doc.replicate(); // emits beforeReplicate and afterReplicate events
      // Replicate will have created the parked pages across locales if needed,
      // but we may still need to reset parked properties
      await self.apos.page.implementParkAllInOtherLocales();
    });
    await self.emit('ready'); // formerly afterInit

    if (self.taskRan) {
      await self._exit();
    } else {
      const after = { exit: null };
      await self.emit('run', self.isTask(), after);
      if (after.exit !== null) {
        await self._exit(after.exit);
      }
    }

    return self;
  } catch (e) {
    if (options.exit === false) {
      console.error('apostrophe: error occurred during startup, continuing:');
      console.error(e);
      // returns undefined, for legacy reasons
    } else if (options.exit === 'throw') {
      // A more sensible approach for those who want to do something
      // if initialization fails
      throw e;
    } else {
      // Longstanding default behavior
      console.error(e);
      await self._exit(1, e);
    }
  }

  // SUPPORTING FUNCTIONS BEGIN HERE

  // Merge configuration from defaults, data/local.js and app.js
  async function mergeConfiguration(options, defaults) {
    let config = {};
    let local = {};
    const localPath = options.__localPath || '/data/local.js';
    const reallyLocalPath = self.rootDir + localPath;

    if (fs.existsSync(reallyLocalPath)) {
      local = await self.root.import(reallyLocalPath);
    }

    // Otherwise making a second apos instance
    // uses the same modified defaults object

    config = _.cloneDeep(options.__testDefaults || defaults);

    _.merge(config, options);

    if (typeof (local) === 'function') {
      if (local.length === 1) {
        _.merge(config, local(self));
      } else if (local.length === 2) {
        local(self, config);
      } else {
        throw new Error('data/local.js may export an object, a function that takes apos as an argument and returns an object, OR a function that takes apos and config as objects and directly modifies config');
      }
    } else {
      _.merge(config, local || {});
    }

    return config;
  }

  async function nestedModuleSubdirs() {
    if (!options.nestedModuleSubdirs) {
      return;
    }
    const configs = glob(self.localModules + '/**/modules.js', { follow: true });
    for (const config of configs) {
      try {
        _.merge(self.options.modules, await self.root.import(config));
      } catch (e) {
        console.error(stripIndent`
          When nestedModuleSubdirs is active, any modules.js file beneath:

          ${self.localModules}

          must export an object containing configuration for Apostrophe modules.

          The file:

          ${config}

          did not parse.
        `);
        throw e;
      }
    }
  }

  async function autodetectBundles() {
    const apostropheModules = Object.keys(self.options.modules);
    for (const apostropheModuleName of apostropheModules) {
      const npmPath = self.getNpmPath(apostropheModuleName);
      if (!npmPath) {
        continue;
      }

      const apostropheModule = await self.root.import(npmPath);
      if (apostropheModule.bundle) {
        self.options.bundles = (self.options.bundles || []).concat(apostropheModuleName);
        const bundleModules = apostropheModule.bundle.modules;
        for (const bundleModuleName of bundleModules) {
          if (!apostropheModules.includes(bundleModuleName)) {
            const bundledModule = await self.root.import(
              path.resolve(
                path.dirname(npmPath),
                apostropheModule.bundle.directory,
                bundleModuleName,
                'index.js'
              )
            );
            if (bundledModule.improve) {
              self.options.modules[bundleModuleName] = {};
            }
          }
        }
      }
    }
  }

  function acceptGlobalOptions() {
    // Truly global options not specific to a module
    if (options.testModule) {
      // Test command lines have arguments not
      // intended as command line task arguments
      self.argv = {
        _: []
      };
      self.options.shortName = self.options.shortName || 'test';
    } else if (options.argv) {
      // Allow injection of any set of command line arguments.
      // Useful with multiple instances
      self.argv = options.argv;
    } else {
      self.argv = argv;
    }

    self.shortName = self.options.shortName;
    if (!self.shortName) {
      throw 'Specify the `shortName` option and set it to the name of your project\'s repository or folder';
    }
    self.title = self.options.title;
    // Environment variable override
    self.options.baseUrl = process.env.APOS_BASE_URL || self.options.baseUrl;
    self.baseUrl = self.options.baseUrl;
    self.prefix = self.options.prefix || '';
  }

  // Tweak the Apostrophe environment suitably for
  // unit testing a separate npm module that extends
  // Apostrophe, like @apostrophecms/workflow. For instance,
  // a node_modules subdirectory with a symlink to the
  // module itself is created so that the module can
  // be found by Apostrophe during testing. Invoked
  // when options.testModule is true. There must be a
  // test/ or tests/ subdir of the module containing
  // a test.js file that runs under mocha via devDependencies.
  // If `options.testModule` is a string it will be used as a
  // namespace for the test module.

  function testModule() {
    if (!options.testModule) {
      return;
    }
    if (!options.shortName) {
      options.shortName = 'test';
    }
    defaults = _.cloneDeep(defaults);
    _.defaults(defaults, {
      '@apostrophecms/express': {}
    });
    _.defaults(defaults['@apostrophecms/express'], {
      port: 7900,
      secret: 'irrelevant'
    });
    const m = self.root;
    checkTestModule();
    // Allow tests to be in test/ or in tests/
    const testDir = path.dirname(m.filename);
    const moduleDir = testDir.replace(/\/tests?$/, '');
    if (testDir === moduleDir) {
      throw new Error('Test file must be in test/ or tests/ subdirectory of module');
    }

    const pkgName = require(`${moduleDir}/package.json`).name;
    let pkgNamespace = '';
    if (pkgName.includes('/')) {
      const parts = pkgName.split('/');
      pkgNamespace = '/' + parts.slice(0, parts.length - 1).join('/');
    }

    if (!fs.existsSync(testDir + '/node_modules')) {
      fs.mkdirSync(testDir + '/node_modules' + pkgNamespace, { recursive: true });
      fs.symlinkSync(moduleDir, testDir + '/node_modules/' + pkgName, 'dir');
    }
    // Makes sure we encounter mocha along the way
    // and throws an exception if we don't
    function checkTestModule() {
      const testFor = `node_modules${path.sep}mocha`;
      if (!require.main.filename.includes(testFor)) {
        throw new Error('mocha does not seem to be running, is this really a test?');
      }
    }
  }

  async function defineModules() {
    // Set moog-require up to create our module manager objects

    self.localModules = self.options.modulesSubdir || self.options.__testLocalModules || (self.rootDir + '/modules');
    const synth = await moogRequire({
      root: self.root,
      bundles: [ 'apostrophe' ].concat(self.options.bundles || []),
      localModules: self.localModules,
      defaultBaseClass: '@apostrophecms/module',
      sections: [ 'helpers', 'handlers', 'routes', 'apiRoutes', 'restApiRoutes', 'renderRoutes', 'middleware', 'customTags', 'components', 'tasks' ],
      nestedModuleSubdirs: self.options.nestedModuleSubdirs,
      unparsedSections: [
        'queries',
        'extendQueries',
        'icons',
        'i18n',
        'webpack',
        'build',
        'commands',
        'before'
      ]
    });

    self.synth = synth;

    // Just like on the browser side, we can
    // call apos.define rather than apos.synth.define
    self.define = self.synth.define;
    self.redefine = self.synth.redefine;
    self.create = self.synth.create;

    await nestedModuleSubdirs();

    for (const [ name, options ] of Object.entries(self.options.modules)) {
      await synth.define(name, options);
    }

    // Apostrophe prefers that any improvements to @apostrophecms/global
    // be applied before any project level version of @apostrophecms/global
    synth.applyImprovementsBeforeProjectLevel();

    return synth;
  }

  // Reorder modules based on their `before` property.
  async function sortModules(moduleNames) {
    // The module names that have a `before` property
    const beforeModules = [];
    // The metadata quick access of all modules
    const modules = {};
    // Recursion guard
    const recursionGuard = {};
    // The sorted modules result
    const sorted = [];

    // The base module sort metadata
    for (const name of moduleNames) {
      const metadata = await self.synth.getMetadata(name);
      const before = Object.values(metadata.before).reverse().find(name => typeof name === 'string');
      if (before) {
        beforeModules.push(name);
      }
      modules[name] = {
        before,
        beforeSelf: []
      };
    }

    // Loop through the modules that have a `before` property,
    // validate and fill the initial `beforeSelf` metadata (first pass).
    for (const name of beforeModules) {
      const m = modules[name];
      const before = m.before;
      if (m.before === name) {
        throw new Error(`Module "${name}" has a 'before' property that references itself.`);
      }
      if (!modules[before]) {
        throw new Error(`Module "${name}" has a 'before' property that references a non-existent module: "${before}".`);
      }
      // Add the current module name to the target's beforeSelf.
      modules[before].beforeSelf.push(name);
    }

    // Loop through the modules that have a `before` properties
    // now that we have the initial metadata (second pass).
    // This takes care of edge cases like `before` that points to another module
    // that has a `before` property itself, circular `before` references, etc.
    // in a very predictable way.
    for (const name of beforeModules) {
      const m = modules[name];
      const target = modules[m.before];
      if (!target) {
        continue;
      }
      // Add all the modules that want to be before this one to the target's
      // beforeSelf. Do this recursively for every module from the beforeSelf
      // array that has own `beforeSelf` members.
      addBeforeSelfRecursive(name, m.beforeSelf, target.beforeSelf);
    }

    // Fill in the sorted array, first wins when uniquefy-ing.
    for (const name of moduleNames) {
      sorted.push(...modules[name].beforeSelf, name);
    }

    // A unique array of sorted module names.
    return [ ...new Set(sorted) ];

    function addBeforeSelfRecursive(moduleName, beforeSelf, target) {
      if (beforeSelf.length === 0) {
        return;
      }
      if (recursionGuard[moduleName]) {
        return;
      }
      recursionGuard[moduleName] = true;

      beforeSelf.forEach((name) => {
        if (recursionGuard[name]) {
          return;
        }
        target.unshift(name);
        addBeforeSelfRecursive(name, modules[name].beforeSelf, target);
      });
    }
  }

  async function instantiateModules() {
    self.modules = {};
    const sorted = await sortModules(modulesToBeInstantiated());
    for (const item of sorted) {
      // module registers itself in self.modules
      const apostropheModule = await self.synth.create(item, { apos: self });
      await apostropheModule.emit('moduleReady');
    }
  }

  function modulesToBeInstantiated() {
    return Object.keys(self.options.modules).filter(name => {
      const improvement = self.synth.isImprovement(name);
      return !(self.options.modules[name] &&
        (improvement || self.options.modules[name].instantiate === false));
    });
  }

  async function lintModules() {
    const validSteps = [];
    for (const apostropheModule of Object.values(self.modules)) {
      for (const step of apostropheModule.__meta.chain) {
        validSteps.push(step.name);
      }
    }

    if (!fs.existsSync(self.localModules)) {
      return;
    }

    const dirs = fs.readdirSync(self.localModules);
    for (const dir of dirs) {
      if (dir.match(/^@/)) {
        const nsDirs = fs.readdirSync(`${self.localModules}/${dir}`);
        for (let nsDir of nsDirs) {
          nsDir = `${dir}/${nsDir}`;
          await testDir(nsDir);
        }
      } else {
        testDir(dir);
      }
    }
    async function testDir(name) {
      if (name.startsWith('.')) {
        return;
      }
      // Projects that have different theme modules activated at different times
      // are a frequent source of false positives for this warning, so ignore
      // seemingly unused modules with "theme" in the name
      if (!validSteps.includes(name)) {
        try {
          // It's a project level modules definition, skip it.
          if (fs.existsSync(path.resolve(self.localModules, name, 'modules.js'))) {
            return;
          }
          const submodule = await self.root.import(path.resolve(self.localModules, name, 'index.js'));
          if (
            submodule &&
            submodule.options &&
            submodule.options.ignoreUnusedFolderWarning
          ) {
            return;
          }
        } catch (e) {
          // index.js might not exist, that's fine for our purposes
        }
        if (name.match(/^apostrophe-/)) {
          warn(
            'namespace-apostrophe-modules',
            stripIndent`
              You have a ${self.localModules}/${name} folder.
              You are probably trying to configure an official Apostrophe module, but those
              are namespaced now. Your directory should be renamed
              ${self.localModules}/${name.replace(/^apostrophe-/, '@apostrophecms/')}

              If you get this warning for your own, original module, do not use the
              "apostrophe-" prefix. It is reserved.
            `
          );
        } else {
          warn('orphan-modules', `You have a ${self.localModules}/${name} folder, but that module is not activated in app.js\nand it is not a base class of any other active module. Right now that code doesn't do anything.`);
        }
      }
      function warn(name, message) {
        if (self.util) {
          self.util.warnDevOnce(name, message);
        } else {
          // apos.util not in play, this can be the case in our bootstrap tests
          if (self.argv[`ignore-${name}`]) {
            return;
          }

          console.warn(message);
        }
      }
    }

    for (const [ name, apostropheModule ] of Object.entries(self.modules)) {
      if (name.match(/^apostrophe-/)) {
        self.util.warnDevOnce(
          'namespace-apostrophe-modules',
          stripIndent`
            You have configured an ${name} module.
            You are probably trying to configure an official Apostrophe module, but those
            are namespaced now. Your module should be renamed ${name.replace(/^apostrophe-/, '@apostrophecms/')}

            If you get this warning for your own original module, do not use the
            "apostrophe-" prefix. It is reserved.
          `
        );
      }
      const moduleNameRegex = /\./;
      if (name.match(moduleNameRegex)) {
        self.util.warnDevOnce(
          'module-name-periods',
          stripIndent`
            You have configured a module named ${name}.
            Modules names may not include periods. Please change this to avoid bugs.
          `
        );
      }

      if (apostropheModule.options.extends && ((typeof apostropheModule.options.extends) === 'string')) {
        lint(`The module ${name} contains an "extends" option. This is probably a\nmistake. In Apostrophe "extend" is used to extend other modules.`);
      }
      if (
        apostropheModule.options.singletonWarningIfNot &&
        (name !== apostropheModule.options.singletonWarningIfNot)
      ) {
        lint(`The module ${name} extends ${apostropheModule.options.singletonWarningIfNot}, which is normally\na singleton (Apostrophe creates only one instance of it). Two competing\ninstances will lead to problems. If you are adding project-level code to it,\njust use modules/${apostropheModule.options.singletonWarningIfNot}/index.js and do not use "extend".\nIf you are improving it via an npm module, use "improve" rather than "extend".\nIf neither situation applies you should probably just make a new module that does\nnot extend anything.\n\nIf you are sure you know what you are doing, you can set the\nsingletonWarningIfNot: false option for this module.`);
      }
      if (name.match(/-widget$/) && (!extending(apostropheModule)) && (!apostropheModule.options.ignoreNoExtendWarning)) {
        lint(`The module ${name} does not extend anything.\n\nA -widget module usually extends @apostrophecms/widget-type or another widget type.\nOr possibly you forgot to npm install something.\n\nIf you are sure you are doing the right thing, set the\nignoreNoExtendWarning option to true for this module.`);
      } else if (name.match(/-page$/) && (name !== '@apostrophecms/page') && (!extending(apostropheModule)) && (!apostropheModule.options.ignoreNoExtendWarning)) {
        lint(`The module ${name} does not extend anything.\n\nA -page module usually extends @apostrophecms/page-type or\n@apostrophecms/piece-page-type or another page type.\nOr possibly you forgot to npm install something.\n\nIf you are sure you are doing the right thing, set the\nignoreNoExtendWarning option to true for this module.`);
      } else if (
        !extending(apostropheModule) &&
        !hasCode(name) &&
        !isBundle(name) &&
        !apostropheModule.options.ignoreNoCodeWarning
      ) {
        lint(`The module ${name} does not extend anything and does not have any code.\n\nThis usually means that you:\n\n1. Forgot to "extend" another module\n2. Configured a module that comes from npm without npm installing it\n3. Simply haven't written your "index.js" yet\n\nIf you really want a module with no code, set the ignoreNoCodeWarning option\nto true for this module.`);
      }
    }
    function hasCode(name) {
      let d = self.synth.definitions[name];
      if (doesWork(d)) {
        return true;
      }
      if (self.synth.isMy(d.__meta.name)) {
        // None at project level, but maybe at npm level, look there
        d = d.extend;
      }
      // If we got to the base class of all modules, the module
      // has no construct of its own
      if (self.synth.myToOriginal(d.__meta.name) === '@apostrophecms/module') {
        return false;
      }
      return doesWork(d);
    }
    function doesWork(d) {
      const countsAsWork = [ 'routes', 'apiRoutes', 'renderRoutes', 'renderRoutes', 'init', 'methods', 'beforeSuperClass', 'handlers', 'helpers', 'restApiRoutes', 'middleware', 'customTags', 'components', 'tasks' ];
      const code = countsAsWork.find(property => d[property]);
      if (code) {
        return true;
      }
      const subdirs = [ 'ui/apos', 'ui/src', 'ui/public', 'public', 'i18n' ];
      if (d.__meta.dirname && subdirs.find(dir => fs.existsSync(`${d.__meta.dirname}/${dir}`))) {
        // Assets that will be bundled, or localizations, instead of server code
        return true;
      }
      return false;
    }
    function isBundle(name) {
      const d = self.synth.definitions[name];
      return d.bundle || (d.extend && d.extend.bundle);
    }
    function extending(apostropheModule) {
      // If the module extends no other module, then it will
      // have up to four entries in its inheritance chain:
      // project level self, npm level self, `apostrophe-modules`
      // project-level and `apostrophe-modules` npm level.
      return apostropheModule.__meta.chain.length > 4;
    }

    function lint(s) {
      self.util.warnDev(stripIndent`
        It looks like you may have made a mistake in your code:\n${s}
      `);
    }
  }

};

const abstractClasses = [ '@apostrophecms/module', '@apostrophecms/widget-type', '@apostrophecms/page-type', '@apostrophecms/piece-type', '@apostrophecms/piece-page-type', '@apostrophecms/doc-type' ];

module.exports.bundle = {
  modules: abstractClasses.concat(_.keys(defaults.modules)),
  directory: 'modules'
};

function seconds(msec) {
  return (Math.round(msec / 100) / 10) + ' seconds';
}

function clusterFork() {
  const worker = cluster.fork();
  worker.bornAt = Date.now();
}

function respawn(worker) {
  console.error(`Respawning worker process ${worker.process.pid}`);
  clusterFork();
}

module.exports.buildRoot = buildRoot;

function buildRoot(options) {
  const root = getRoot(options);
  const rootDir = options.rootDir || path.dirname(root.filename);
  const npmRootDir = options.npmRootDir || rootDir;
  const selfDir = __dirname;

  return {
    root,
    rootDir,
    npmRootDir,
    selfDir
  };
}
function getRoot(options) {
  const root = options.root;
  if (root?.filename && root?.require) {
    return {
      filename: root.filename,
      import: async (id) => root.require(id),
      require: (id) => root.require(id)
    };
  }

  if (root?.url) {
    // Apostrophe was started from an ESM project
    const filename = url.fileURLToPath(root.url);
    const dynamicImport = async (id) => {
      const { default: defaultExport, ...rest } = await import(id);

      return defaultExport || rest;
    };

    return {
      filename,
      import: dynamicImport,
      require: (id) => {
        console.warn(`self.apos.root.require is now async, please verify that you await the promise (${id})`);

        return dynamicImport(id);
      }
    };
  }

  // Legacy commonjs logic
  function getLegacyRoot() {
    let _module = module;
    let m = _module;
    while (m.parent && m.parent.filename) {
      // The test file is the root as far as we are concerned,
      // not mocha itself
      if (m.parent.filename.match(/\/node_modules\/mocha\//)) {
        return m;
      }
      m = m.parent;
      _module = m;
    }
    return _module;
  }
  const legacyRoot = getLegacyRoot();
  return {
    filename: legacyRoot.filename,
    import: async (id) => legacyRoot.require(id),
    require: (id) => legacyRoot.require(id)
  };
};

module.exports.getNpmPath = getNpmPath;

function getNpmPath(name, baseDir) {
  return npmResolve.sync(name, { basedir: path.resolve(baseDir) });
}
